Skip to content

Commit

Permalink
feat: implement bouncer
Browse files Browse the repository at this point in the history
This service authorizes calls and converts extensions to users. This
information can be used in Asterisk to take action.
  • Loading branch information
crazybolillo committed Aug 4, 2024
1 parent 1227ce7 commit b962118
Show file tree
Hide file tree
Showing 7 changed files with 214 additions and 13 deletions.
5 changes: 5 additions & 0 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package main
import (
"context"
"fmt"
"github.com/crazybolillo/eryth/internal/bouncer"
"github.com/crazybolillo/eryth/internal/handler"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
Expand Down Expand Up @@ -75,6 +76,10 @@ func serve(ctx context.Context) error {
endpoint := handler.Endpoint{Conn: conn}
r.Mount("/endpoint", endpoint.Router())

checker := &bouncer.Bouncer{Conn: conn}
authorization := handler.Authorization{Bouncer: checker}
r.Mount("/bouncer", authorization.Router())

slog.Info("Listening on :8080")
return http.ListenAndServe(":8080", r)
}
54 changes: 49 additions & 5 deletions docs/swagger.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,18 @@
definitions:
bouncer.Response:
properties:
allow:
type: boolean
destination:
type: string
type: object
handler.AuthorizationRequest:
properties:
endpoint:
type: string
extension:
type: string
type: object
handler.createEndpointRequest:
properties:
codecs:
Expand All @@ -20,13 +34,20 @@ definitions:
transport:
type: string
type: object
handler.listEndpointsRequest:
properties:
endpoints:
items:
$ref: '#/definitions/sqlc.ListEndpointsRow'
type: array
type: object
sqlc.ListEndpointsRow:
properties:
context:
type: string
id:
extension:
type: string
transport:
id:
type: string
type: object
host: localhost:8080
Expand All @@ -36,6 +57,31 @@ info:
title: Asterisk Administration API
version: "1.0"
paths:
/bouncer:
post:
consumes:
- application/json
parameters:
- description: Action to be reviewed
in: body
name: payload
required: true
schema:
$ref: '#/definitions/handler.AuthorizationRequest'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/bouncer.Response'
"400":
description: Bad Request
"500":
description: Internal Server Error
summary: Determine whether the specified action (call) is allowed or not.
tags:
- bouncer
/endpoint:
post:
consumes:
Expand Down Expand Up @@ -89,9 +135,7 @@ paths:
"200":
description: OK
schema:
items:
$ref: '#/definitions/sqlc.ListEndpointsRow'
type: array
$ref: '#/definitions/handler.listEndpointsRequest'
"400":
description: Bad Request
"500":
Expand Down
43 changes: 43 additions & 0 deletions internal/bouncer/bouncer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package bouncer

import (
"context"
"github.com/crazybolillo/eryth/internal/db"
"github.com/crazybolillo/eryth/internal/sqlc"
"github.com/jackc/pgx/v5"
"log/slog"
)

type Response struct {
Allow bool `json:"allow"`
Destination string `json:"destination"`
}

type Bouncer struct {
*pgx.Conn
}

func (b *Bouncer) Check(ctx context.Context, endpoint, dialed string) Response {
result := Response{
Allow: false,
Destination: "",
}

tx, err := b.Begin(ctx)
if err != nil {
slog.Error("Unable to start transaction", slog.String("reason", err.Error()))
return result
}

queries := sqlc.New(tx)
destination, err := queries.GetEndpointByExtension(ctx, db.Text(dialed))
if err != nil {
slog.Error("Failed to retrieve endpoint", slog.String("dialed", dialed), slog.String("reason", err.Error()))
return result
}

return Response{
Allow: true,
Destination: destination,
}
}
63 changes: 63 additions & 0 deletions internal/handler/authorization.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package handler

import (
"context"
"encoding/json"
"github.com/crazybolillo/eryth/internal/bouncer"
"github.com/go-chi/chi/v5"
"log/slog"
"net/http"
)

type CallBouncer interface {
Check(ctx context.Context, endpoint, dialed string) bouncer.Response
}

type Authorization struct {
Bouncer CallBouncer
}

type AuthorizationRequest struct {
From string `json:"endpoint"`
Extension string `json:"extension"`
}

func (e *Authorization) Router() chi.Router {
r := chi.NewRouter()
r.Post("/", e.post)

return r
}

// @Summary Determine whether the specified action (call) is allowed or not.
// @Accept json
// @Produce json
// @Param payload body AuthorizationRequest true "Action to be reviewed"
// @Success 200 {object} bouncer.Response
// @Failure 400
// @Failure 500
// @Tags bouncer
// @Router /bouncer [post]
func (e *Authorization) post(w http.ResponseWriter, r *http.Request) {
var payload AuthorizationRequest
decoder := json.NewDecoder(r.Body)
err := decoder.Decode(&payload)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}

response := e.Bouncer.Check(r.Context(), payload.From, payload.Extension)
content, err := json.Marshal(response)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}

w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "application/json")
_, err = w.Write(content)
if err != nil {
slog.Error("Failed to write response", slog.String("path", r.URL.Path), slog.String("reason", err.Error()))
}
}
16 changes: 14 additions & 2 deletions internal/handler/endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ type createEndpointRequest struct {
Extension string `json:"extension,omitempty"`
}

type listEndpointsRequest struct {
Endpoints []sqlc.ListEndpointsRow `json:"endpoints"`
}

func (e *Endpoint) Router() chi.Router {
r := chi.NewRouter()
r.Post("/", e.create)
Expand All @@ -41,7 +45,7 @@ func (e *Endpoint) Router() chi.Router {
// @Summary List existing endpoints.
// @Param limit query int false "Limit the amount of endpoints returned" default(15)
// @Produce json
// @Success 200 {object} []sqlc.ListEndpointsRow
// @Success 200 {object} listEndpointsRequest
// @Failure 400
// @Failure 500
// @Tags endpoints
Expand All @@ -61,14 +65,18 @@ func (e *Endpoint) list(w http.ResponseWriter, r *http.Request) {
queries := sqlc.New(e.Conn)
endpoints, err := queries.ListEndpoints(r.Context(), int32(limit))
if err != nil {
slog.Error("Query execution failed", slog.String("path", r.URL.Path), slog.String("msg", err.Error()))
w.WriteHeader(http.StatusInternalServerError)
return
}
if endpoints == nil {
endpoints = []sqlc.ListEndpointsRow{}
}

content, err := json.Marshal(endpoints)
response := listEndpointsRequest{
Endpoints: endpoints,
}
content, err := json.Marshal(response)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
Expand Down Expand Up @@ -149,6 +157,10 @@ func (e *Endpoint) create(w http.ResponseWriter, r *http.Request) {
EndpointID: sid,
Extension: db.Text(payload.Extension),
})
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
}

err = tx.Commit(r.Context())
Expand Down
29 changes: 25 additions & 4 deletions internal/sqlc/queries.sql.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 15 additions & 2 deletions queries.sql
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,26 @@ DELETE FROM ps_auths WHERE id = $1;

-- name: ListEndpoints :many
SELECT
id, context, transport
pe.id, pe.context, ee.extension
FROM
ps_endpoints
ps_endpoints pe
LEFT JOIN
ery_extension ee
ON ee.endpoint_id = pe.sid
LIMIT $1;

-- name: NewExtension :exec
INSERT INTO ery_extension
(endpoint_id, extension)
VALUES
($1, $2);

-- name: GetEndpointByExtension :one
SELECT
ps_endpoints.id
FROM
ps_endpoints
INNER JOIN
ery_extension ee on ps_endpoints.sid = ee.endpoint_id
WHERE
ee.extension = $1;

0 comments on commit b962118

Please sign in to comment.