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>
This commit is contained in:
Claude (gsc-ops-api init)
2026-06-01 11:13:33 +02:00
parent 3847eb2036
commit 9fd11afa00
9 changed files with 912 additions and 11 deletions

View File

@@ -13,6 +13,10 @@ import (
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
@@ -42,8 +46,20 @@ func Setup(app *fiber.App, cfg *Config) {
app.Get("/health", cfg.Health.Liveness)
app.Get("/ready", cfg.Health.Readiness)
// API v1 routes (API key required)
api := app.Group("/api/v1", middleware.APIKey(cfg.APIKeys))
// 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")