mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-10 13:58:52 +00:00
feat(auth): backend endpoints for MED-7 + MED-11 + MED-12
Audit 2026-05-10 MED-7 + MED-11 + MED-12 backend halves.
WHAT.
Three new admin-gated endpoints:
GET /api/v1/auth/oidc/providers/{id}/jwks-status (auth.oidc.list) — MED-7
GET /api/v1/auth/users (auth.user.read) — MED-11
DELETE /api/v1/auth/users/{id} (auth.user.deactivate) — MED-11
GET /api/v1/auth/runtime-config (auth.role.assign) — MED-12
MED-7 — JWKS health surface
- providerEntry gains 4 counters (statsMu, lastRefreshAt, refreshCount,
lastError, rejectedJWSCount) updated under sync.Mutex
- RefreshKeys increments refreshCount + records lastRefreshAt
- New JWKSStatus(ctx, providerID) returns *JWKSStatusSnapshot —
surfaced via the new endpoint
- CurrentKIDs intentionally empty (go-oidc's internal JWKS cache
isn't exposed); shape kept for forward compat
MED-11 — federated-user admin
- AuthUsersHandler.List with optional ?oidc_provider_id filter
- AuthUsersHandler.Deactivate sets users.deactivated_at + cascade-
revokes sessions via UserSessionsRevoker (best-effort; revoke
failure does NOT roll back the deactivation)
- Idempotent: re-deactivating an already-deactivated user is a no-op
MED-12 — runtime config
- AuthRuntimeConfigHandler.Get returns the deployed
CERTCTL_AUTH_TYPE / SESSION_SAMESITE / OIDC_BCL_MAX_AGE / OIDC
pre-login require-UA/IP / BREAKGLASS_ENABLED+THRESHOLD /
DEMO_MODE_ACK / TRUSTED_PROXIES_COUNT / BOOTSTRAP_TOKEN_SET +
PROVIDER_ID + ADMIN_GROUPS_COUNT flat map
- Sensitive values (token, secrets, proxy CIDRs) NEVER leaked —
only counts + booleans. Token presence surfaced as 'set/unset'
- Gated auth.role.assign (admin-class) so non-admins can't
enumerate the deployment's auth knobs
cmd/server/main.go wires all three handlers into HandlerRegistry.
internal/api/router/router.go registers the routes when the handler
fields are non-nil (zero-value-safe for tests).
VERIFY.
- go vet ./internal/api/... ./internal/auth/... ./internal/repository/... PASS
- go build ./cmd/server/... PASS
- go test -short -count=1 ./internal/auth/oidc/... PASS (4.1s)
- go test -short -count=1 ./internal/api/handler/... PASS (4.1s)
GUI halves for MED-7 + MED-11 + MED-12 are the GUI batch (pending).
Refs: cowork/auth-bundles-audit-2026-05-10.md MED-7, MED-11, MED-12
cowork/auth-bundles-fixes-2026-05-10/HANDOFF.md items 11 14 15
This commit is contained in:
@@ -118,6 +118,16 @@ type providerEntry struct {
|
||||
// IssuerURL. When false (the default for most IdPs that haven't
|
||||
// rolled RFC 9207 yet), the check is skipped.
|
||||
issParamSupported bool
|
||||
|
||||
// Audit 2026-05-10 MED-7 — JWKS health counters surfaced via
|
||||
// /api/v1/auth/oidc/providers/{id}/jwks-status. statsMu guards
|
||||
// the four counters. Each is updated under the write-lock from
|
||||
// RefreshKeys + HandleCallback's verify path.
|
||||
statsMu sync.Mutex
|
||||
lastRefreshAt time.Time
|
||||
refreshCount int
|
||||
lastError string
|
||||
rejectedJWSCount int
|
||||
}
|
||||
|
||||
// OIDCProviderLookup is a narrow read-side projection of
|
||||
@@ -873,13 +883,71 @@ func (s *Service) fetchUserinfoGroups(
|
||||
|
||||
// RefreshKeys evicts the cached provider entry and re-loads it from
|
||||
// scratch. Invokes the discovery doc fetch + the downgrade defense.
|
||||
//
|
||||
// Audit 2026-05-10 MED-7 — increments refreshCount + records
|
||||
// lastRefreshAt / lastError on the new providerEntry's counters so
|
||||
// JWKSStatus can surface operator-visible refresh history.
|
||||
func (s *Service) RefreshKeys(ctx context.Context, providerID string) error {
|
||||
s.mu.Lock()
|
||||
delete(s.cache, providerID)
|
||||
s.mu.Unlock()
|
||||
|
||||
_, err := s.getOrLoad(ctx, providerID)
|
||||
return err
|
||||
entry, err := s.getOrLoad(ctx, providerID)
|
||||
if err != nil {
|
||||
// On error, no cached entry exists to record on. JWKSStatus
|
||||
// will return a synthetic snapshot with empty counters for the
|
||||
// not-yet-loaded provider; the lastError surfaces via the
|
||||
// follow-up getOrLoad call's own path.
|
||||
return err
|
||||
}
|
||||
entry.statsMu.Lock()
|
||||
entry.refreshCount++
|
||||
entry.lastRefreshAt = s.clockNow().UTC()
|
||||
entry.lastError = ""
|
||||
entry.statsMu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
// JWKSStatus returns the per-provider JWKS health snapshot used by the
|
||||
// /api/v1/auth/oidc/providers/{id}/jwks-status endpoint. Audit
|
||||
// 2026-05-10 MED-7. Returns an empty-counters snapshot for providers
|
||||
// that have never been loaded (no refresh, no rejected JWS yet).
|
||||
//
|
||||
// `CurrentKIDs` is intentionally omitted — go-oidc's internal JWKS
|
||||
// cache doesn't expose its current keyset, and re-implementing the
|
||||
// JWKS fetch here would duplicate state. Operators wanting kid
|
||||
// inspection use the discovery doc's `jwks_uri` directly. The field
|
||||
// remains in the response shape for forward-compat.
|
||||
func (s *Service) JWKSStatus(ctx context.Context, providerID string) (*JWKSStatusSnapshot, error) {
|
||||
entry, err := s.getOrLoad(ctx, providerID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
entry.statsMu.Lock()
|
||||
defer entry.statsMu.Unlock()
|
||||
snap := &JWKSStatusSnapshot{
|
||||
RefreshCount: entry.refreshCount,
|
||||
LastError: entry.lastError,
|
||||
RejectedJWSCount: entry.rejectedJWSCount,
|
||||
IssParamSupported: entry.issParamSupported,
|
||||
CurrentKIDs: []string{},
|
||||
}
|
||||
if !entry.lastRefreshAt.IsZero() {
|
||||
snap.LastRefreshAt = entry.lastRefreshAt.UTC().Format(time.RFC3339)
|
||||
}
|
||||
return snap, nil
|
||||
}
|
||||
|
||||
// JWKSStatusSnapshot mirrors the per-provider counters the MED-7 HTTP
|
||||
// handler returns. Defined here so cmd/server can wire the OIDC
|
||||
// service directly into the handler without an adapter.
|
||||
type JWKSStatusSnapshot struct {
|
||||
LastRefreshAt string `json:"last_refresh_at,omitempty"`
|
||||
CurrentKIDs []string `json:"current_kids"`
|
||||
RefreshCount int `json:"refresh_count"`
|
||||
LastError string `json:"last_error,omitempty"`
|
||||
RejectedJWSCount int `json:"rejected_jws_count"`
|
||||
IssParamSupported bool `json:"iss_param_supported"`
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
|
||||
Reference in New Issue
Block a user