package management import ( "context" "crypto/tls" "crypto/x509" "database/sql" "encoding/json" "errors" "fmt" "io" "net/http" "net/url" "os" "strconv" "strings" "time" "github.com/gofiber/fiber/v3" fiberCors "github.com/gofiber/fiber/v3/middleware/cors" "github.com/sirupsen/logrus" ) type principal struct { Token string TokenSpec } type Server struct { cfg Config store *Store app *fiber.App log *logrus.Logger } func NewServer(cfg Config, store *Store) *Server { if store == nil { panic("management: nil store") } logger := logrus.New() logger.SetFormatter(&logrus.JSONFormatter{}) s := &Server{cfg: cfg, store: store, log: logger} s.app = fiber.New(fiber.Config{BodyLimit: 1 << 20, ErrorHandler: s.errorHandler}) s.app.Use(s.originGuard) s.app.Use(fiberCors.New(fiberCors.Config{AllowOriginsFunc: s.originAllowed, AllowMethods: []string{http.MethodGet, http.MethodPut, http.MethodPost, http.MethodOptions}, AllowHeaders: []string{"Authorization", "Content-Type", "If-Match", "X-Request-ID"}, AllowCredentials: true})) s.app.Use(func(c fiber.Ctx) error { started := time.Now() err := c.Next() s.log.WithFields(logrus.Fields{"method": c.Method(), "path": c.Path(), "status": c.Response().StatusCode(), "duration_ms": time.Since(started).Milliseconds(), "request_id": c.Get("X-Request-ID")}).Debug("management request") return err }) s.registerRoutes() if err := s.Recover(context.Background()); err != nil { s.log.WithError(err).Warn("operation recovery requires operator reconciliation") } return s } func (s *Server) App() *fiber.App { return s.app } func (s *Server) Listen() error { return s.app.Listen(s.cfg.Addr) } func (s *Server) errorHandler(c fiber.Ctx, err error) error { if appErr, ok := err.(*AppError); ok { return s.fail(c, appErr) } return s.fail(c, newAppError(http.StatusInternalServerError, "INTERNAL_ERROR", "internal server error", nil)) } func (s *Server) registerRoutes() { s.app.Get("/healthz/live", s.live) s.app.Get("/admin/v1/providers", s.listProviders) s.app.Get("/admin/v1/providers/:provider_id", s.getProvider) s.app.Put("/admin/v1/providers/:provider_id", s.putProvider) s.app.Get("/admin/v1/trunks", s.listTrunks) s.app.Get("/admin/v1/trunks/:trunk_id", s.getTrunk) s.app.Put("/admin/v1/trunks/:trunk_id", s.putTrunk) s.app.Post("/admin/v1/trunks/:trunk_id/validate", s.validateTrunk) s.app.Post("/admin/v1/trunks/:trunk_id/verifications", s.addVerification) s.app.Post("/admin/v1/trunks/:trunk_id/publish", s.publishTrunk) s.app.Post("/admin/v1/trunks/:trunk_id/disable", s.disableTrunk) s.app.Post("/admin/v1/trunks/:trunk_id/rollback", s.rollbackTrunk) s.app.Get("/admin/v1/trunks/:trunk_id/publications", s.listPublications) s.app.Get("/admin/v1/trunks/:trunk_id/audit", s.listTrunkAudit) s.app.Get("/admin/v1/cells", s.listCells) s.app.Get("/admin/v1/cells/:cell_id", s.getCell) s.app.Put("/admin/v1/cells/:cell_id", s.putCell) s.app.Post("/admin/v1/cells/:cell_id/observations", s.ingestObservation) s.app.Get("/admin/v1/egress-pools", s.listEgressPools) s.app.Get("/admin/v1/sip-status", s.sipStatus) s.app.Get("/admin/v1/cells/:cell_id/sip-status", s.cellSIPStatus) s.app.Get("/admin/v1/providers/:provider_id/status", s.providerStatus) s.app.Get("/admin/v1/statistics/outbound/summary", s.outboundSummary) s.app.Get("/admin/v1/statistics/outbound/timeseries", s.outboundTimeseries) s.app.Get("/admin/v1/call-attempts", s.callAttempts) s.app.Get("/admin/v1/call-attempts/:attempt_id", s.callAttempt) s.app.Get("/admin/v1/audit", s.audit) s.app.Get("/admin/v1/operations/:operation_id", s.getOperation) s.app.Get("/readonly/v1/sip/trunks", s.listReadonlyTrunks) s.app.Get("/readonly/v1/sip/trunks/:trunk_id", s.getReadonlyTrunk) s.app.Use(func(c fiber.Ctx) error { return s.fail(c, newAppError(http.StatusNotFound, "NOT_FOUND", "resource not found", nil)) }) } func (s *Server) originAllowed(origin string) bool { if origin == "" || origin == "*" { return false } allowed := s.cfg.AllowedOrigins if len(allowed) == 0 { allowed = DefaultConfig().AllowedOrigins } return allowed[origin] && origin != "*" } func (s *Server) originGuard(c fiber.Ctx) error { origin := strings.TrimSpace(c.Get("Origin")) if origin != "" && !s.originAllowed(origin) { return s.fail(c, newAppError(http.StatusForbidden, "ORIGIN_FORBIDDEN", "browser origin is not allowlisted", nil)) } return c.Next() } func (s *Server) live(c fiber.Ctx) error { return c.JSON(map[string]any{"status": "ok", "mode": s.cfg.PublicMode()}) } func (s *Server) authorize(c fiber.Ctx, realm, scope, resourceType, resourceID string) (principal, *AppError) { header := strings.TrimSpace(c.Get("Authorization")) if !strings.HasPrefix(header, "Bearer ") || strings.TrimSpace(strings.TrimPrefix(header, "Bearer ")) == "" { return principal{}, newAppError(http.StatusUnauthorized, "UNAUTHORIZED", "a bearer token is required", nil) } token := strings.TrimSpace(strings.TrimPrefix(header, "Bearer ")) spec, ok := s.cfg.Tokens[token] if !ok || spec.Realm != realm { return principal{}, newAppError(http.StatusUnauthorized, "UNAUTHORIZED", "token is not valid for this API", nil) } if !containsString(spec.Scopes, scope) { return principal{}, newAppError(http.StatusForbidden, "FORBIDDEN", "credential lacks the required scope", map[string]any{"required_scope": scope}) } if resourceID != "" && !resourceAllowed(spec, resourceType, resourceID) { return principal{}, newAppError(http.StatusForbidden, "RESOURCE_FORBIDDEN", "credential is not authorized for this resource", map[string]any{"resource_type": resourceType, "resource_id": resourceID}) } return principal{Token: token, TokenSpec: spec}, nil } func resourceAllowed(spec TokenSpec, resourceType, resourceID string) bool { allowed, ok := spec.Resources[resourceType] if !ok || len(allowed) == 0 { return true } return containsString(allowed, "*") || containsString(allowed, resourceID) } func (s *Server) checkFilterResources(c fiber.Ctx, p principal) *AppError { for _, id := range parseCSV(c.Query("cell_ids")) { if !resourceAllowed(p.TokenSpec, "cell", id) { return newAppError(http.StatusForbidden, "RESOURCE_FORBIDDEN", "one or more requested Cells are outside the authorized scope", map[string]any{"cell_id": id}) } } for _, id := range parseCSV(c.Query("cell_id")) { if !resourceAllowed(p.TokenSpec, "cell", id) { return newAppError(http.StatusForbidden, "RESOURCE_FORBIDDEN", "requested Cell is outside the authorized scope", map[string]any{"cell_id": id}) } } if id := c.Query("provider_id"); id != "" && !resourceAllowed(p.TokenSpec, "provider", id) { return newAppError(http.StatusForbidden, "RESOURCE_FORBIDDEN", "requested provider is outside the authorized scope", map[string]any{"provider_id": id}) } if id := c.Query("trunk_id"); id != "" && !resourceAllowed(p.TokenSpec, "trunk", id) { return newAppError(http.StatusForbidden, "RESOURCE_FORBIDDEN", "requested trunk is outside the authorized scope", map[string]any{"trunk_id": id}) } if id := c.Query("egress_pool_id"); id != "" && !resourceAllowed(p.TokenSpec, "egress_pool", id) { return newAppError(http.StatusForbidden, "RESOURCE_FORBIDDEN", "requested egress pool is outside the authorized scope", map[string]any{"egress_pool_id": id}) } return nil } func (s *Server) fail(c fiber.Ctx, err *AppError) error { if err == nil { err = newAppError(http.StatusInternalServerError, "INTERNAL_ERROR", "internal server error", nil) } requestID := strings.TrimSpace(c.Get("X-Request-ID")) body := errorResponse{Error: ErrorBody{Code: err.Code, Message: err.Message, Fields: err.Fields, RequestID: requestID}} return c.Status(err.Status).JSON(body) } func (s *Server) context(_ fiber.Ctx) (context.Context, context.CancelFunc) { return context.WithTimeout(context.Background(), 10*time.Second) } func requireHeader(c fiber.Ctx, name string) (string, *AppError) { value := strings.TrimSpace(c.Get(name)) if value == "" { status := http.StatusPreconditionRequired code := "PRECONDITION_REQUIRED" if name == "X-Request-ID" { status = http.StatusBadRequest code = "REQUEST_ID_REQUIRED" } return "", newAppError(status, code, name+" header is required", nil) } if name == "X-Request-ID" && !validRequestID(value) { return "", newAppError(http.StatusBadRequest, "INVALID_REQUEST_ID", "X-Request-ID is invalid", nil) } return strings.Trim(value, `"`), nil } func parseIfMatch(c fiber.Ctx) (int64, *AppError) { value, err := requireHeader(c, "If-Match") if err != nil { return 0, err } value = strings.TrimSpace(strings.Trim(value, `"`)) if value == "" || value == "*" { return 0, newAppError(http.StatusPreconditionRequired, "IF_MATCH_REQUIRED", "If-Match must contain an exact revision", nil) } revision, parseErr := strconv.ParseInt(value, 10, 64) if parseErr != nil || revision < 0 { return 0, newAppError(http.StatusBadRequest, "INVALID_IF_MATCH", "If-Match must be a non-negative revision", nil) } return revision, nil } func decodeJSON(c fiber.Ctx, dst any) *AppError { body := c.Body() if len(body) == 0 { return newAppError(http.StatusBadRequest, "INVALID_JSON", "request body is required", nil) } decoder := json.NewDecoder(strings.NewReader(string(body))) decoder.DisallowUnknownFields() if err := decoder.Decode(dst); err != nil { return newAppError(http.StatusBadRequest, "INVALID_JSON", "request body is invalid", nil) } var extra any if err := decoder.Decode(&extra); err != io.EOF { return newAppError(http.StatusBadRequest, "INVALID_JSON", "request body must contain one JSON value", nil) } return nil } func writeStored(c fiber.Ctx, status int, raw string) error { c.Status(status) c.Set("Content-Type", "application/json; charset=utf-8") return c.SendString(raw) } func (s *Server) beginOperation(ctx context.Context, p principal, action, resourceType, resourceID, requestID string, payload any, expected int64) (Operation, bool, *AppError) { digest, _, err := hashJSON(payload) if err != nil { return Operation{}, false, newAppError(500, "INTERNAL_ERROR", "could not create request digest", nil) } op, replay, err := s.store.CreateOperation(ctx, p.Principal, action, resourceType, resourceID, requestID, digest, expected) if err != nil { return Operation{}, false, asAppError(err) } if !replay { return op, false, nil } if op.RequestDigest != digest { return Operation{}, false, newAppError(http.StatusConflict, "IDEMPOTENCY_CONFLICT", "the same request ID was used with a different payload", map[string]any{"operation_id": op.OperationID}) } if op.State == "succeeded" && op.ResponseJSON != "" { return op, true, nil } if (op.State == "failed" || op.State == "pending" || op.State == "needs_reconciliation") && op.ErrorJSON != "" { return op, true, nil } return op, true, newAppError(http.StatusConflict, "OPERATION_IN_PROGRESS", "the operation is still in progress; query it by operation_id", map[string]any{"operation_id": op.OperationID}) } func (s *Server) replayOrError(c fiber.Ctx, op Operation, beginErr *AppError) error { if beginErr != nil { return s.fail(c, beginErr) } if op.State == "succeeded" && op.ResponseJSON != "" { return writeStored(c, op.HTTPStatus, op.ResponseJSON) } if op.ErrorJSON != "" { return writeStored(c, op.HTTPStatus, op.ErrorJSON) } return s.fail(c, newAppError(http.StatusConflict, "OPERATION_IN_PROGRESS", "the operation is still in progress; query it by operation_id", map[string]any{"operation_id": op.OperationID})) } func (s *Server) finishSuccess(ctx context.Context, op Operation, status int, value any) error { body, err := json.Marshal(value) if err != nil { return err } return s.store.FinishOperation(ctx, op.OperationID, "succeeded", status, string(body), "") } func (s *Server) finishError(ctx context.Context, op Operation, appErr *AppError, c fiber.Ctx) error { requestID := strings.TrimSpace(c.Get("X-Request-ID")) body, _ := json.Marshal(errorResponse{Error: ErrorBody{Code: appErr.Code, Message: appErr.Message, Fields: appErr.Fields, RequestID: requestID}}) state := "failed" if appErr.Code == "PUBLISH_PENDING" || appErr.Code == "PUBLISH_PARTIAL" || appErr.Code == "OPERATION_IN_PROGRESS" { state = "pending" } if err := s.store.FinishOperation(ctx, op.OperationID, state, appErr.Status, "", string(body)); err != nil { return err } return s.fail(c, appErr) } func (s *Server) listProviders(c fiber.Ctx) error { p, authErr := s.authorize(c, "admin", "sip.provider.read", "", "") if authErr != nil { return s.fail(c, authErr) } _ = p ctx, cancel := s.context(c) defer cancel() providers, err := s.store.ListProviders(ctx) if err != nil { return s.fail(c, asAppError(err)) } visible := providers[:0] for _, provider := range providers { if resourceAllowed(p.TokenSpec, "provider", provider.ProviderID) { visible = append(visible, provider) } } return c.JSON(map[string]any{"mode": s.cfg.PublicMode(), "providers": visible}) } func (s *Server) getProvider(c fiber.Ctx) error { id := c.Params("provider_id") p, authErr := s.authorize(c, "admin", "sip.provider.read", "provider", id) if authErr != nil { return s.fail(c, authErr) } _ = p ctx, cancel := s.context(c) defer cancel() provider, err := s.store.GetProvider(ctx, id) if errors.Is(err, sql.ErrNoRows) { return s.fail(c, newAppError(404, "PROVIDER_NOT_FOUND", "provider does not exist", nil)) } if err != nil { return s.fail(c, asAppError(err)) } return c.JSON(map[string]any{"mode": s.cfg.PublicMode(), "provider": provider}) } func (s *Server) putProvider(c fiber.Ctx) error { id := c.Params("provider_id") p, authErr := s.authorize(c, "admin", "sip.provider.write", "provider", id) if authErr != nil { return s.fail(c, authErr) } expected, err := parseIfMatch(c) if err != nil { return s.fail(c, err) } requestID, err := requireHeader(c, "X-Request-ID") if err != nil { return s.fail(c, err) } var input ProviderInput if err := decodeJSON(c, &input); err != nil { return s.fail(c, err) } ctx, cancel := s.context(c) defer cancel() op, replay, beginErr := s.beginOperation(ctx, p, "provider.upsert", "provider", id, requestID, map[string]any{"if_match": expected, "input": input}, expected) if replay || beginErr != nil { return s.replayOrError(c, op, beginErr) } provider, status, opErr := s.store.UpsertProvider(ctx, id, input, expected, p.Principal, requestID) if opErr != nil { appErr := asAppError(opErr) _ = s.store.FinishOperation(ctx, op.OperationID, "failed", appErr.Status, "", errorJSON(appErr, requestID)) return s.fail(c, appErr) } body := map[string]any{"mode": s.cfg.PublicMode(), "provider": provider} if err := s.finishSuccess(ctx, op, status, body); err != nil { return s.fail(c, asAppError(err)) } return c.Status(status).JSON(body) } func errorJSON(err *AppError, requestID string) string { b, _ := json.Marshal(errorResponse{Error: ErrorBody{Code: err.Code, Message: err.Message, Fields: err.Fields, RequestID: requestID}}) return string(b) } func (s *Server) listTrunks(c fiber.Ctx) error { p, authErr := s.authorize(c, "admin", "sip.trunk.read", "", "") if authErr != nil { return s.fail(c, authErr) } if err := s.checkFilterResources(c, p); err != nil { return s.fail(c, err) } ctx, cancel := s.context(c) defer cancel() trunks, err := s.store.ListTrunks(ctx, s.cfg.PublicMode(), c.Query("provider_id")) if err != nil { return s.fail(c, asAppError(err)) } if cellID := c.Query("cell_id"); cellID != "" { filtered := trunks[:0] for _, trunk := range trunks { if containsString(trunk.CompatibleCellIDs, cellID) { filtered = append(filtered, trunk) } } trunks = filtered } if state := c.Query("status"); state != "" { filtered := trunks[:0] for _, trunk := range trunks { if trunk.Status == state || trunk.ActiveStatus == state { filtered = append(filtered, trunk) } } trunks = filtered } visible := trunks[:0] for _, trunk := range trunks { if resourceAllowed(p.TokenSpec, "trunk", trunk.TrunkID) && resourceAllowed(p.TokenSpec, "provider", trunk.ProviderID) { visible = append(visible, trunk) } } return c.JSON(map[string]any{"mode": s.cfg.PublicMode(), "trunks": visible}) } func (s *Server) getTrunk(c fiber.Ctx) error { id := c.Params("trunk_id") p, authErr := s.authorize(c, "admin", "sip.trunk.read", "trunk", id) if authErr != nil { return s.fail(c, authErr) } _ = p ctx, cancel := s.context(c) defer cancel() trunk, err := s.store.GetTrunk(ctx, id, s.cfg.PublicMode()) if errors.Is(err, sql.ErrNoRows) { return s.fail(c, newAppError(404, "TRUNK_NOT_FOUND", "trunk does not exist", nil)) } if err != nil { return s.fail(c, asAppError(err)) } return c.JSON(trunk) } func (s *Server) putTrunk(c fiber.Ctx) error { id := c.Params("trunk_id") p, authErr := s.authorize(c, "admin", "sip.trunk.write", "trunk", id) if authErr != nil { return s.fail(c, authErr) } expected, err := parseIfMatch(c) if err != nil { return s.fail(c, err) } requestID, err := requireHeader(c, "X-Request-ID") if err != nil { return s.fail(c, err) } var input TrunkConfig if err := decodeJSON(c, &input); err != nil { return s.fail(c, err) } ctx, cancel := s.context(c) defer cancel() op, replay, beginErr := s.beginOperation(ctx, p, "trunk.upsert", "trunk", id, requestID, map[string]any{"if_match": expected, "input": input}, expected) if replay || beginErr != nil { return s.replayOrError(c, op, beginErr) } trunk, status, opErr := s.store.PutTrunk(ctx, id, input, expected, p.Principal, requestID) if opErr != nil { appErr := asAppError(opErr) _ = s.store.FinishOperation(ctx, op.OperationID, "failed", appErr.Status, "", errorJSON(appErr, requestID)) return s.fail(c, appErr) } trunk.Mode = s.cfg.PublicMode() body := map[string]any{"mode": s.cfg.PublicMode(), "trunk": trunk} if err := s.finishSuccess(ctx, op, status, body); err != nil { return s.fail(c, asAppError(err)) } return c.Status(status).JSON(body) } func (s *Server) validateTrunk(c fiber.Ctx) error { id := c.Params("trunk_id") p, authErr := s.authorize(c, "admin", "sip.trunk.write", "trunk", id) if authErr != nil { return s.fail(c, authErr) } _ = p var body struct { Revision int64 `json:"revision"` } if len(c.Body()) > 0 { if err := decodeJSON(c, &body); err != nil { return s.fail(c, err) } } ctx, cancel := s.context(c) defer cancel() if body.Revision == 0 { trunk, err := s.store.GetTrunk(ctx, id, s.cfg.PublicMode()) if err != nil { return s.fail(c, asAppError(err)) } body.Revision = trunk.LatestRevision } result, err := s.store.BuildValidation(ctx, id, body.Revision, s.cfg.PublicMode()) if errors.Is(err, sql.ErrNoRows) { return s.fail(c, newAppError(404, "VERSION_NOT_FOUND", "trunk version does not exist", nil)) } if err != nil { return s.fail(c, asAppError(err)) } return c.JSON(result) } func (s *Server) listCells(c fiber.Ctx) error { p, authErr := s.authorize(c, "admin", "sip.cell.read", "", "") if authErr != nil { return s.fail(c, authErr) } _ = p ctx, cancel := s.context(c) defer cancel() cells, err := s.store.ListCells(ctx) if err != nil { return s.fail(c, asAppError(err)) } visible := cells[:0] for i := range cells { if resourceAllowed(p.TokenSpec, "cell", cells[i].CellID) { cells[i].Mode = s.cfg.PublicMode() visible = append(visible, cells[i]) } } return c.JSON(map[string]any{"mode": s.cfg.PublicMode(), "cells": visible}) } func (s *Server) getCell(c fiber.Ctx) error { id := c.Params("cell_id") p, authErr := s.authorize(c, "admin", "sip.cell.read", "cell", id) if authErr != nil { return s.fail(c, authErr) } _ = p ctx, cancel := s.context(c) defer cancel() cell, err := s.store.GetCell(ctx, id) if errors.Is(err, sql.ErrNoRows) { return s.fail(c, newAppError(404, "CELL_NOT_FOUND", "cell does not exist", nil)) } if err != nil { return s.fail(c, asAppError(err)) } cell.Mode = s.cfg.PublicMode() return c.JSON(cell) } func (s *Server) putCell(c fiber.Ctx) error { id := c.Params("cell_id") p, authErr := s.authorize(c, "admin", "sip.cell.write", "cell", id) if authErr != nil { return s.fail(c, authErr) } expected, err := parseIfMatch(c) if err != nil { return s.fail(c, err) } requestID, err := requireHeader(c, "X-Request-ID") if err != nil { return s.fail(c, err) } var input struct { CellConfig CloudInstanceID string `json:"cloud_instance_id,omitempty"` InstanceName string `json:"instance_name,omitempty"` Region string `json:"region,omitempty"` } if err := decodeJSON(c, &input); err != nil { return s.fail(c, err) } if input.ManagementURL != "" { allowPrivate := s.cfg.AllowPrivateAgent allowlist := s.cfg.AgentAllowlist if s.cfg.Mode != ModeReal { allowPrivate = true allowlist = nil } if err := validateManagementURL(input.ManagementURL, allowPrivate, allowlist); err != nil { return s.fail(c, newAppError(422, "INVALID_MANAGEMENT_URL", err.Error(), nil)) } } ctx, cancel := s.context(c) defer cancel() op, replay, beginErr := s.beginOperation(ctx, p, "cell.upsert", "cell", id, requestID, map[string]any{"if_match": expected, "input": input}, expected) if replay || beginErr != nil { return s.replayOrError(c, op, beginErr) } cell, status, opErr := s.store.UpsertCell(ctx, id, input.CellConfig, expected, p.Principal, requestID, input.CloudInstanceID, input.InstanceName, input.Region) if opErr != nil { appErr := asAppError(opErr) _ = s.store.FinishOperation(ctx, op.OperationID, "failed", appErr.Status, "", errorJSON(appErr, requestID)) return s.fail(c, appErr) } cell.Mode = s.cfg.PublicMode() body := map[string]any{"mode": s.cfg.PublicMode(), "cell": cell} if err := s.finishSuccess(ctx, op, status, body); err != nil { return s.fail(c, asAppError(err)) } return c.Status(status).JSON(body) } func (s *Server) listEgressPools(c fiber.Ctx) error { p, authErr := s.authorize(c, "admin", "sip.cell.read", "", "") if authErr != nil { return s.fail(c, authErr) } _ = p ctx, cancel := s.context(c) defer cancel() pools, err := s.store.ListEgressPools(ctx) if err != nil { return s.fail(c, asAppError(err)) } visible := pools[:0] for _, pool := range pools { if resourceAllowed(p.TokenSpec, "egress_pool", fmt.Sprint(pool["egress_pool_id"])) { visible = append(visible, pool) } } return c.JSON(map[string]any{"mode": s.cfg.PublicMode(), "egress_pools": visible}) } func (s *Server) listPublications(c fiber.Ctx) error { id := c.Params("trunk_id") p, authErr := s.authorize(c, "admin", "sip.trunk.read", "trunk", id) if authErr != nil { return s.fail(c, authErr) } _ = p ctx, cancel := s.context(c) defer cancel() rows, err := s.store.GetPublicationRows(ctx, id, nil) if errors.Is(err, sql.ErrNoRows) { return s.fail(c, newAppError(404, "TRUNK_NOT_FOUND", "trunk does not exist", nil)) } if err != nil { return s.fail(c, asAppError(err)) } return c.JSON(map[string]any{"mode": s.cfg.PublicMode(), "publications": rows}) } func (s *Server) listTrunkAudit(c fiber.Ctx) error { id := c.Params("trunk_id") p, authErr := s.authorize(c, "admin", "sip.audit.read", "trunk", id) if authErr != nil { return s.fail(c, authErr) } _ = p ctx, cancel := s.context(c) defer cancel() rows, err := s.store.ListAudit(ctx, id, "", "", 200) if err != nil { return s.fail(c, asAppError(err)) } return c.JSON(map[string]any{"mode": s.cfg.PublicMode(), "audit": rows}) } func (s *Server) getOperation(c fiber.Ctx) error { id := c.Params("operation_id") p, authErr := s.authorize(c, "admin", "sip.operation.read", "", "") if authErr != nil { return s.fail(c, authErr) } _ = p ctx, cancel := s.context(c) defer cancel() op, err := s.store.GetOperation(ctx, id) if errors.Is(err, sql.ErrNoRows) { return s.fail(c, newAppError(404, "OPERATION_NOT_FOUND", "operation does not exist", nil)) } if err != nil { return s.fail(c, asAppError(err)) } if !resourceAllowed(p.TokenSpec, op.ResourceType, op.ResourceID) { return s.fail(c, newAppError(403, "RESOURCE_FORBIDDEN", "operation is outside the authorized scope", nil)) } out := map[string]any{"operation_id": op.OperationID, "action": op.Action, "resource_type": op.ResourceType, "resource_id": op.ResourceID, "request_id": op.RequestID, "state": op.State, "http_status": op.HTTPStatus, "created_at": op.CreatedAt, "updated_at": op.UpdatedAt, "expires_at": op.ExpiresAt} if op.ResponseJSON != "" { var v any _ = json.Unmarshal([]byte(op.ResponseJSON), &v) out["response"] = v } if op.ErrorJSON != "" { var v any _ = json.Unmarshal([]byte(op.ErrorJSON), &v) out["error"] = v } return c.JSON(out) } func (s *Server) listReadonlyTrunks(c fiber.Ctx) error { p, authErr := s.authorize(c, "saas", "sip.trunk.read", "", "") if authErr != nil { return s.fail(c, authErr) } ctx, cancel := s.context(c) defer cancel() trunks, err := s.store.ListTrunks(ctx, s.cfg.PublicMode(), "") if err != nil { return s.fail(c, asAppError(err)) } out := make([]ReadonlyTrunk, 0, len(trunks)) for _, trunk := range trunks { if trunk.ActiveRevision == 0 || trunk.ActiveStatus != "published" || !resourceAllowed(p.TokenSpec, "trunk", trunk.TrunkID) || !resourceAllowed(p.TokenSpec, "provider", trunk.ProviderID) { continue } out = append(out, ReadonlyTrunk{Mode: s.cfg.PublicMode(), TrunkID: trunk.TrunkID, ProviderID: trunk.ProviderID, Revision: trunk.ActiveRevision, Status: "published", UpdatedAt: trunk.UpdatedAt, Config: trunk.Active}) } return c.JSON(map[string]any{"mode": s.cfg.PublicMode(), "trunks": out}) } func (s *Server) getReadonlyTrunk(c fiber.Ctx) error { id := c.Params("trunk_id") p, authErr := s.authorize(c, "saas", "sip.trunk.read", "trunk", id) if authErr != nil { return s.fail(c, authErr) } _ = p ctx, cancel := s.context(c) defer cancel() trunk, err := s.store.GetTrunk(ctx, id, s.cfg.PublicMode()) if errors.Is(err, sql.ErrNoRows) || trunk.ActiveRevision == 0 || trunk.ActiveStatus != "published" { return s.fail(c, newAppError(404, "TRUNK_NOT_FOUND", "published trunk does not exist", nil)) } return c.JSON(ReadonlyTrunk{Mode: s.cfg.PublicMode(), TrunkID: id, ProviderID: trunk.ProviderID, Revision: trunk.ActiveRevision, Status: "published", UpdatedAt: trunk.UpdatedAt, Config: trunk.Active}) } // httpClient is intentionally used only for the real Cell Agent protocol. Mock mode never dials a network address. func (s *Server) httpClient() (*http.Client, error) { rootPEM, certFile, keyFile := os.Getenv("MANAGEMENT_AGENT_CA"), os.Getenv("MANAGEMENT_AGENT_CLIENT_CERT"), os.Getenv("MANAGEMENT_AGENT_CLIENT_KEY") if rootPEM == "" || certFile == "" || keyFile == "" { return nil, fmt.Errorf("mTLS client certificate is not configured") } cert, err := tls.LoadX509KeyPair(certFile, keyFile) if err != nil { return nil, fmt.Errorf("load mTLS client certificate: %w", err) } pool := x509.NewCertPool() if rootPEM != "" { pemBytes, err := os.ReadFile(rootPEM) if err != nil { return nil, fmt.Errorf("read Cell CA: %w", err) } if !pool.AppendCertsFromPEM(pemBytes) { return nil, fmt.Errorf("invalid Cell CA") } } return &http.Client{Timeout: 10 * time.Second, CheckRedirect: func(_ *http.Request, _ []*http.Request) error { return http.ErrUseLastResponse }, Transport: &http.Transport{TLSClientConfig: &tls.Config{MinVersion: tls.VersionTLS13, Certificates: []tls.Certificate{cert}, RootCAs: pool, ServerName: ""}}}, nil } func validateAgentURL(raw string, cfg Config) (*url.URL, error) { u, err := url.Parse(raw) if err != nil { return nil, err } if err := validateManagementURL(raw, cfg.AllowPrivateAgent, cfg.AgentAllowlist); err != nil { return nil, err } return u, nil }