From 3265600b463ba8cafc66293a1178cbd4b4486da3 Mon Sep 17 00:00:00 2001 From: CrazyBolillo Date: Mon, 29 Jul 2024 23:47:33 -0600 Subject: [PATCH] feat: implement bouncer This service authorizes calls and converts extensions to users. This information can be used in Asterisk to take action. --- cmd/main.go | 5 +++ docs/swagger.yaml | 39 +++++++++++++++++++ internal/bouncer/bouncer.go | 43 +++++++++++++++++++++ internal/handler/authorization.go | 63 +++++++++++++++++++++++++++++++ internal/sqlc/queries.sql.go | 18 +++++++++ queries.sql | 10 +++++ 6 files changed, 178 insertions(+) create mode 100644 internal/bouncer/bouncer.go create mode 100644 internal/handler/authorization.go diff --git a/cmd/main.go b/cmd/main.go index da70f64..5124677 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -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" @@ -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) } diff --git a/docs/swagger.yaml b/docs/swagger.yaml index b5ac2d8..97ef3bf 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -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: @@ -36,6 +50,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: diff --git a/internal/bouncer/bouncer.go b/internal/bouncer/bouncer.go new file mode 100644 index 0000000..5a98305 --- /dev/null +++ b/internal/bouncer/bouncer.go @@ -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) + endpoint, 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: endpoint, + } +} diff --git a/internal/handler/authorization.go b/internal/handler/authorization.go new file mode 100644 index 0000000..19aebab --- /dev/null +++ b/internal/handler/authorization.go @@ -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())) + } +} diff --git a/internal/sqlc/queries.sql.go b/internal/sqlc/queries.sql.go index a767e92..84d7dec 100644 --- a/internal/sqlc/queries.sql.go +++ b/internal/sqlc/queries.sql.go @@ -38,6 +38,24 @@ func (q *Queries) DeleteEndpoint(ctx context.Context, id string) error { return err } +const getEndpointByExtension = `-- 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 +` + +func (q *Queries) GetEndpointByExtension(ctx context.Context, extension pgtype.Text) (string, error) { + row := q.db.QueryRow(ctx, getEndpointByExtension, extension) + var id string + err := row.Scan(&id) + return id, err +} + const listEndpoints = `-- name: ListEndpoints :many SELECT id, context, transport diff --git a/queries.sql b/queries.sql index 8825860..c69534e 100644 --- a/queries.sql +++ b/queries.sql @@ -38,3 +38,13 @@ 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;