Files
gsc-ops-api/internal/router/router.go
Claude (gsc-ops-api init) 9fd11afa00 feat: dynamic, scoped API keys (+ restore cmd/server entrypoint)
Validate X-API-Key against a DB-backed, self-managed key store in addition
to the static Infisical keys, so new consumers (e.g. gsc_admin) no longer
require a rebuild. Keys carry scopes (e.g. {ldap:read}); the required scope
is derived per-request from path + method and enforced by ScopeEnforce.
Static Infisical keys keep an implicit wildcard scope (no regression).

- service/apikey.go: DB store (admin.api_keys, SHA-256 hashes only), 30s
  validation cache, generate/list/revoke. EnsureSchema is existence-first
  (to_regclass) so a least-privilege DB role starts cleanly when the table
  is provisioned out-of-band; startup is non-fatal if the store is absent.
- handler/apikeys.go + routes: POST/GET/DELETE /api/v1/admin/api-keys.
- middleware/apikey.go: APIKeyWithValidator + Principal + ScopeEnforce.
- pkg/types/scopes.go: scope vocabulary + matching.
- migrations/002_api_keys.sql.

Also restore cmd/server/main.go, which the `.gitignore` `server` pattern
was silently excluding (it matched cmd/server/); anchored that pattern and
`gsc-ops-api` to the repo root so only the built binaries are ignored.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 11:13:33 +02:00

242 lines
9.0 KiB
Go

package router
import (
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/recover"
"github.com/rs/zerolog"
"github.com/gosec/gsc-ops-api/internal/handler"
"github.com/gosec/gsc-ops-api/internal/middleware"
)
// Config holds all handler dependencies for route registration
type Config struct {
Logger zerolog.Logger
APIKeys []string
// APIKeyValidate backs the static APIKeys list with a dynamic, self-managed
// key store so new consumers can be added at runtime without a rebuild.
APIKeyValidate middleware.APIKeyValidator
APIKeysAdmin *handler.APIKeyHandler
Health *handler.HealthHandler
LDAPUsers *handler.LDAPUserHandler
LDAPGroups *handler.LDAPGroupHandler
LDAPEntities *handler.LDAPEntityHandler
DNSZones *handler.DNSZoneHandler
DNSRecords *handler.DNSRecordHandler
DBTenants *handler.DBTenantHandler
DBUsers *handler.DBUserHandler
Certs *handler.CertHandler
PGP *handler.PGPHandler
CardDAV *handler.CardDAVHandler
PBX *handler.PBXHandler
VoiceAgent *handler.VoiceAgentHandler
PersonalAgent *handler.PersonalAgentHandler
Persona *handler.PersonaHandler
}
// Setup registers all routes on the Fiber app
func Setup(app *fiber.App, cfg *Config) {
// Global middleware
app.Use(recover.New())
app.Use(middleware.RequestID())
app.Use(middleware.Logging(cfg.Logger))
app.Use(middleware.JWTExtract())
// Health endpoints (no API key required)
app.Get("/health", cfg.Health.Liveness)
app.Get("/ready", cfg.Health.Readiness)
// API v1 routes: authenticate (static keys + dynamic key store), then
// enforce per-key scopes. Static keys carry the wildcard scope and bypass.
api := app.Group("/api/v1",
middleware.APIKeyWithValidator(cfg.APIKeys, cfg.APIKeyValidate),
middleware.ScopeEnforce(),
)
// Dynamic API-key management (bootstrapped by any existing valid key)
if cfg.APIKeysAdmin != nil {
apiKeys := api.Group("/admin/api-keys")
apiKeys.Get("/", cfg.APIKeysAdmin.List)
apiKeys.Post("/", cfg.APIKeysAdmin.Create)
apiKeys.Delete("/:id", cfg.APIKeysAdmin.Revoke)
}
// LDAP Users
ldapUsers := api.Group("/ldap/users")
ldapUsers.Get("/", cfg.LDAPUsers.List)
ldapUsers.Get("/:uid", cfg.LDAPUsers.Get)
ldapUsers.Post("/", cfg.LDAPUsers.Create)
ldapUsers.Put("/:uid", cfg.LDAPUsers.Update)
ldapUsers.Delete("/:uid", cfg.LDAPUsers.Delete)
ldapUsers.Post("/:uid/password", cfg.LDAPUsers.ResetPassword)
ldapUsers.Get("/:uid/groups", cfg.LDAPUsers.ListGroups)
ldapUsers.Get("/:uid/services", cfg.LDAPUsers.ListServices)
ldapUsers.Get("/:uid/services/:domain", cfg.LDAPUsers.GetService)
// LDAP Groups
ldapGroups := api.Group("/ldap/groups")
ldapGroups.Get("/", cfg.LDAPGroups.List)
ldapGroups.Get("/:cn", cfg.LDAPGroups.Get)
ldapGroups.Post("/", cfg.LDAPGroups.Create)
ldapGroups.Put("/:cn", cfg.LDAPGroups.Update)
ldapGroups.Delete("/:cn", cfg.LDAPGroups.Delete)
ldapGroups.Get("/:cn/members", cfg.LDAPGroups.ListMembers)
ldapGroups.Post("/:cn/members", cfg.LDAPGroups.AddMembers)
ldapGroups.Delete("/:cn/members/:uid", cfg.LDAPGroups.RemoveMember)
// LDAP Entities (generic CRUD)
ldapEntities := api.Group("/ldap/entities")
ldapEntities.Get("/", cfg.LDAPEntities.ListTypes)
ldapEntities.Get("/:type", cfg.LDAPEntities.List)
ldapEntities.Post("/:type", cfg.LDAPEntities.Create)
ldapEntities.Get("/:type/:rdn", cfg.LDAPEntities.Get)
ldapEntities.Put("/:type/:rdn", cfg.LDAPEntities.Update)
ldapEntities.Delete("/:type/:rdn", cfg.LDAPEntities.Delete)
// DNS Zones
dnsZones := api.Group("/dns/zones")
dnsZones.Get("/", cfg.DNSZones.List)
dnsZones.Get("/:zoneId", cfg.DNSZones.Get)
dnsZones.Post("/", cfg.DNSZones.Create)
dnsZones.Put("/:zoneId", cfg.DNSZones.Update)
dnsZones.Delete("/:zoneId", cfg.DNSZones.Delete)
dnsZones.Post("/:zoneId/notify", cfg.DNSZones.Notify)
// DNS Records
dnsZones.Get("/:zoneId/records", cfg.DNSRecords.List)
dnsZones.Post("/:zoneId/records", cfg.DNSRecords.Create)
dnsZones.Put("/:zoneId/records", cfg.DNSRecords.Replace)
dnsZones.Delete("/:zoneId/records", cfg.DNSRecords.Delete)
// DNS Domains (orchestrated)
dnsDomains := api.Group("/dns/domains")
dnsDomains.Post("/setup", cfg.DNSRecords.DomainSetup)
dnsDomains.Post("/verify", cfg.DNSRecords.DomainVerify)
// DB Tenants
dbTenants := api.Group("/db/tenants")
dbTenants.Get("/", cfg.DBTenants.List)
dbTenants.Get("/:id", cfg.DBTenants.Get)
dbTenants.Post("/", cfg.DBTenants.Create)
dbTenants.Put("/:id", cfg.DBTenants.Update)
dbTenants.Delete("/:id", cfg.DBTenants.Delete)
// DB Users
dbUsers := api.Group("/db/users")
dbUsers.Get("/", cfg.DBUsers.List)
dbUsers.Get("/:id", cfg.DBUsers.Get)
dbUsers.Post("/", cfg.DBUsers.Create)
dbUsers.Put("/:id", cfg.DBUsers.Update)
dbUsers.Delete("/:id", cfg.DBUsers.Delete)
// Certificates
certs := api.Group("/certs")
certs.Get("/", cfg.Certs.List)
certs.Get("/:serialNumber", cfg.Certs.Get)
certs.Post("/request", cfg.Certs.Request)
certs.Post("/:serialNumber/renew", cfg.Certs.Renew)
certs.Post("/:serialNumber/revoke", cfg.Certs.Revoke)
// PGP Keys
pgp := api.Group("/pgp/keys")
pgp.Get("/", cfg.PGP.Search)
pgp.Get("/:keyId", cfg.PGP.Get)
pgp.Post("/", cfg.PGP.Upload)
pgp.Delete("/:keyId", cfg.PGP.Delete)
// PBX
if cfg.PBX != nil {
pbxTrunks := api.Group("/pbx/trunks")
pbxTrunks.Get("/", cfg.PBX.ListTrunks)
pbxTrunks.Post("/", cfg.PBX.CreateTrunk)
pbxTrunks.Get("/:id", cfg.PBX.GetTrunk)
pbxTrunks.Put("/:id", cfg.PBX.UpdateTrunk)
pbxTrunks.Delete("/:id", cfg.PBX.DeleteTrunk)
pbxTrunks.Post("/:id/activate", cfg.PBX.ActivateTrunk)
pbxTrunks.Post("/:id/deactivate", cfg.PBX.DeactivateTrunk)
pbxTrunks.Get("/:id/dids", cfg.PBX.ListTrunkDIDs)
pbxTrunks.Post("/:id/dids", cfg.PBX.CreateTrunkDID)
pbxTrunks.Delete("/:id/dids/:didId", cfg.PBX.DeleteTrunkDID)
pbxExts := api.Group("/pbx/extensions")
pbxExts.Get("/", cfg.PBX.ListExtensions)
pbxExts.Post("/", cfg.PBX.CreateExtension)
pbxExts.Get("/:id", cfg.PBX.GetExtension)
pbxExts.Put("/:id", cfg.PBX.UpdateExtension)
pbxExts.Delete("/:id", cfg.PBX.DeleteExtension)
pbxInbound := api.Group("/pbx/inbound-routes")
pbxInbound.Get("/", cfg.PBX.ListInboundRoutes)
pbxInbound.Post("/", cfg.PBX.CreateInboundRoute)
pbxInbound.Get("/:id", cfg.PBX.GetInboundRoute)
pbxInbound.Put("/:id", cfg.PBX.UpdateInboundRoute)
pbxInbound.Delete("/:id", cfg.PBX.DeleteInboundRoute)
pbxOutbound := api.Group("/pbx/outbound-routes")
pbxOutbound.Get("/", cfg.PBX.ListOutboundRoutes)
pbxOutbound.Post("/", cfg.PBX.CreateOutboundRoute)
pbxOutbound.Get("/:id", cfg.PBX.GetOutboundRoute)
pbxOutbound.Put("/:id", cfg.PBX.UpdateOutboundRoute)
pbxOutbound.Delete("/:id", cfg.PBX.DeleteOutboundRoute)
api.Get("/pbx/status", cfg.PBX.Status)
api.Post("/pbx/reload", cfg.PBX.Reload)
}
// Voice Agents
if cfg.VoiceAgent != nil {
voiceAgents := api.Group("/voice-agents")
voiceAgents.Get("/", cfg.VoiceAgent.ListConfigs)
voiceAgents.Post("/", cfg.VoiceAgent.CreateConfig)
voiceAgents.Get("/sessions/:sessionId", cfg.VoiceAgent.GetSession)
voiceAgents.Get("/:id", cfg.VoiceAgent.GetConfig)
voiceAgents.Put("/:id", cfg.VoiceAgent.UpdateConfig)
voiceAgents.Delete("/:id", cfg.VoiceAgent.DeleteConfig)
voiceAgents.Get("/:id/sessions", cfg.VoiceAgent.ListSessions)
}
// Personal Agents (user agent configs)
if cfg.PersonalAgent != nil {
agents := api.Group("/agents")
agents.Get("/me", cfg.PersonalAgent.GetMyConfig)
agents.Put("/me", cfg.PersonalAgent.UpsertMyConfig)
agents.Delete("/me", cfg.PersonalAgent.DeleteMyConfig)
}
// Personas
if cfg.Persona != nil {
personas := api.Group("/personas")
personas.Get("/", cfg.Persona.ListPersonas)
personas.Post("/", cfg.Persona.CreatePersona)
personas.Get("/:id", cfg.Persona.GetPersona)
personas.Put("/:id", cfg.Persona.UpdatePersona)
personas.Delete("/:id", cfg.Persona.DeletePersona)
personas.Get("/:id/self-model", cfg.Persona.GetSelfModel)
personas.Get("/:id/experiences", cfg.Persona.GetExperiences)
personas.Get("/:id/evaluations/:sessionId", cfg.Persona.GetEvaluations)
personas.Get("/:id/moral-pattern/:sessionId", cfg.Persona.GetMoralPattern)
}
// CardDAV
if cfg.CardDAV != nil {
cardDAVPrincipals := api.Group("/carddav/principals")
cardDAVPrincipals.Get("/", cfg.CardDAV.ListPrincipals)
cardDAVPrincipals.Get("/:username", cfg.CardDAV.GetPrincipal)
cardDAVPrincipals.Post("/", cfg.CardDAV.CreatePrincipal)
cardDAVPrincipals.Delete("/:username", cfg.CardDAV.DeletePrincipal)
cardDAVBooks := api.Group("/carddav/addressbooks")
cardDAVBooks.Get("/", cfg.CardDAV.ListAddressBooks)
cardDAVBooks.Get("/:id", cfg.CardDAV.GetAddressBook)
cardDAVBooks.Post("/", cfg.CardDAV.CreateAddressBook)
cardDAVBooks.Put("/:id", cfg.CardDAV.UpdateAddressBook)
cardDAVBooks.Delete("/:id", cfg.CardDAV.DeleteAddressBook)
cardDAVBooks.Get("/:id/contacts", cfg.CardDAV.ListContacts)
cardDAVBooks.Get("/:id/contacts/:uri", cfg.CardDAV.GetContact)
cardDAVBooks.Post("/:id/contacts", cfg.CardDAV.CreateContact)
cardDAVBooks.Put("/:id/contacts/:uri", cfg.CardDAV.UpdateContact)
cardDAVBooks.Delete("/:id/contacts/:uri", cfg.CardDAV.DeleteContact)
}
}