mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 16:01:30 +00:00
docs(arch-h1): Phase 13 Sprint 13.5 — OpenAPI breakglass + users + runtime-config ops (batch 2, 8 ops)
Phase 13 Sprint 13.5 closure (architecture diligence audit ARCH-H1):
authors OpenAPI operations for the auth/breakglass admin cluster
(4) + auth/users cluster (3) + auth/runtime-config (1), drives the
`rest-deferred` exception bucket from 15 → 7.
OpenAPI-only sprint: zero Go changes. Every schema field-by-field
mirrors the projection types in
internal/api/handler/auth_breakglass.go +
internal/api/handler/auth_users.go.
8 new operations
================
Break-glass admin cluster (4 ops, all gated `auth.breakglass.admin`):
GET /api/v1/auth/breakglass/credentials listBreakglassCredentials
POST /api/v1/auth/breakglass/credentials setBreakglassPassword
DELETE /api/v1/auth/breakglass/credentials/{actor_id} removeBreakglassCredential
POST /api/v1/auth/breakglass/credentials/{actor_id}/unlock unlockBreakglassCredential
Users cluster (3 ops):
GET /api/v1/auth/users listAuthUsers (auth.user.read)
DELETE /api/v1/auth/users/{id} deactivateAuthUser (auth.user.deactivate)
POST /api/v1/auth/users/{id}/reactivate reactivateAuthUser (auth.user.deactivate)
Runtime-config read (1 op):
GET /api/v1/auth/runtime-config getAuthRuntimeConfig (auth.role.assign)
5 new schemas (components/schemas)
==================================
BreakglassCredentialResponse — mirrors breakglassCredentialResponse
(6 fields). Password hash NEVER
serialized.
BreakglassCredentialListResponse — mirrors listBreakglassCredentialsResponse
({"credentials": [...]}).
BreakglassSetPasswordRequest — mirrors breakglassSetPasswordRequest
(actor_id + password; password marked
`format: password`).
BreakglassSetPasswordResponse — mirrors the inline response shape
returned by SetPassword (actor_id +
created_at).
AuthUser — mirrors userResponse (9 fields,
including pointer-based
deactivated_at marked nullable).
Every schema field's JSON tag, type, required-ness, and (where
applicable) nullability grounded against the live Go source. The
`tenant_id` field surfaces on AuthUser (the handler emits it) but
does NOT appear on the breakglass schemas (the breakglass surface
is tenant-implicit — derived from caller context, not request body).
Surface-invisibility property
=============================
Each break-glass admin endpoint returns 404 when
`CERTCTL_BREAKGLASS_ENABLED=false` so an attacker probing the admin
surface gets the same signal as probing the login endpoint
(consistent with Audit 2026-05-10 CRIT-4 closure). Documented in the
per-op description so client implementations don't surprise on the
404 path.
Self-deactivate guard
=====================
`DELETE /api/v1/auth/users/{id}` returns 409 (not 403) when the
caller is deactivating their own account — Audit 2026-05-11 A-2
foot-gun closure. Break-glass remains the documented recovery path.
The 409 is documented in the per-op responses block.
Exception YAML + baseline
=========================
8 entries removed from api/openapi-handler-exceptions.yaml. Post-cut
shape:
total entries: 43 (was 51)
wire-protocol: 36 (unchanged)
rest-deferred: 7 (was 15)
Baseline file bumped 15 → 7. The Sprint 13.1 monotonic-decrease
guard now pins `rest-deferred ≤ 7`. Sprint 13.6 walks it to zero
(7 → 0).
YAML header narrative updated: "Sprint 13.5 SHIPPED — 15 - 8 = 7".
Receipts (all from the live tree)
=================================
$ grep -cE '^\s+operationId:' api/openapi.yaml
179 (was 171 + 8)
$ bash scripts/ci-guards/openapi-handler-parity.sh
Router routes: 220
OpenAPI operations: 179
Documented exceptions: 43
wire-protocol: 36
rest-deferred: 7
openapi-handler-parity: clean.
$ bash scripts/ci-guards/openapi-rest-deferred-monotonic.sh
openapi-rest-deferred-monotonic: clean — rest-deferred = 7,
baseline = 7.
$ cat api/openapi-handler-exceptions-baseline.txt
7
$ python3 -c "import yaml; ..."
paths: 133, operations: 179, schemas: 72
sprint-13.5 schemas missing: (none)
OpenAPI lint: clean.
$ gofmt -l . → clean
$ go vet ./internal/api/handler/... ./cmd/server/... → clean
Sprint 13.6 next (audit/export + demo-residual + 3 OIDC browser
flows + auth/logout + auth/breakglass/login = 7 ops; rest-deferred
7 → 0 — the zero-floor commit that completes ARCH-H1's substantive
burn-down). Same OpenAPI-only pattern; the OIDC browser-flow
endpoints in 13.6 model redirect-only operations (302 + Location
header, empty body) per OAS 3.1 conventions.
Refs: ARCH-H1 batch 2 closure.
This commit is contained in:
@@ -1 +1 @@
|
||||
15
|
||||
7
|
||||
|
||||
@@ -41,19 +41,20 @@
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
#
|
||||
# Current split, re-derived by the parity script's bucket-reporting
|
||||
# subcommand (post-Sprint-13.4 / 2026-05-14):
|
||||
# subcommand (post-Sprint-13.5 / 2026-05-14):
|
||||
#
|
||||
# total entries: 51
|
||||
# total entries: 43
|
||||
# wire-protocol: 36
|
||||
# rest-deferred: 15
|
||||
# rest-deferred: 7
|
||||
#
|
||||
# Burn-down progress + remaining plan for the rest-deferred bucket:
|
||||
#
|
||||
# Sprint 13.4 SHIPPED — 28 - 13 = 15 (auth/sessions cluster 3 ops +
|
||||
# auth/oidc CRUD + JWKS + test + refresh
|
||||
# + group-mappings cluster, 10 ops)
|
||||
# Sprint 13.5 → 15 - 8 = 7 (auth/breakglass + auth/users +
|
||||
# auth/runtime-config, 8 ops)
|
||||
# Sprint 13.5 SHIPPED — 15 - 8 = 7 (auth/breakglass admin 4 ops +
|
||||
# auth/users 3 ops + auth/runtime-config
|
||||
# 1 op, 8 ops total)
|
||||
# Sprint 13.6 → 7 - 7 = 0 (audit/export + demo-residual + 3
|
||||
# OIDC browser flows + auth/logout +
|
||||
# auth/breakglass/login, 7 ops)
|
||||
@@ -211,30 +212,6 @@ documented_exceptions:
|
||||
- route: "POST /auth/oidc/back-channel-logout"
|
||||
why: "Bundle 2 Phase 5 RFC OIDC Back-Channel Logout 1.0 endpoint. OpenAPI rep deferred to pre-2.2.0."
|
||||
category: rest-deferred
|
||||
- route: "GET /api/v1/auth/breakglass/credentials"
|
||||
why: "Bundle 2 Phase 7.5 admin break-glass list (404 when disabled; password hash never on wire)."
|
||||
category: rest-deferred
|
||||
- route: "POST /api/v1/auth/breakglass/credentials"
|
||||
why: "Bundle 2 Phase 7.5 admin break-glass set/rotate password."
|
||||
category: rest-deferred
|
||||
- route: "POST /api/v1/auth/breakglass/credentials/{actor_id}/unlock"
|
||||
why: "Bundle 2 Phase 7.5 admin break-glass unlock after lockout."
|
||||
category: rest-deferred
|
||||
- route: "DELETE /api/v1/auth/breakglass/credentials/{actor_id}"
|
||||
why: "Bundle 2 Phase 7.5 admin break-glass credential delete."
|
||||
category: rest-deferred
|
||||
- route: "GET /api/v1/auth/users"
|
||||
why: "Bundle 2 audit-2026-05-10 MED-11 users page."
|
||||
category: rest-deferred
|
||||
- route: "DELETE /api/v1/auth/users/{id}"
|
||||
why: "Bundle 2 audit-2026-05-10 MED-11 user deactivate."
|
||||
category: rest-deferred
|
||||
- route: "POST /api/v1/auth/users/{id}/reactivate"
|
||||
why: "Bundle 2 audit-2026-05-10 MED-11 user reactivate."
|
||||
category: rest-deferred
|
||||
- route: "GET /api/v1/auth/runtime-config"
|
||||
why: "Bundle 2 audit-2026-05-10 MED-12 effective auth-runtime-config (read-only)."
|
||||
category: rest-deferred
|
||||
- route: "POST /api/v1/auth/demo-residual/cleanup"
|
||||
why: "Audit 2026-05-11 A-8 demo-mode residual-grants cleanup endpoint."
|
||||
category: rest-deferred
|
||||
|
||||
@@ -1024,6 +1024,234 @@ paths:
|
||||
"404": { description: Mapping not found }
|
||||
"500": { description: Internal error }
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════
|
||||
# Phase 13 Sprint 13.5 (ARCH-H1 batch 2) — break-glass admin, users
|
||||
# admin, runtime-config read. Authored 2026-05-14 against the
|
||||
# internal/api/handler/auth_breakglass.go + auth_users.go handlers.
|
||||
# Surface-invisibility: every break-glass admin endpoint returns 404
|
||||
# when CERTCTL_BREAKGLASS_ENABLED=false; documented per-op.
|
||||
# ════════════════════════════════════════════════════════════════════
|
||||
|
||||
/api/v1/auth/breakglass/credentials:
|
||||
get:
|
||||
tags: [Auth]
|
||||
summary: List break-glass credentials (metadata only — never the password hash)
|
||||
description: |
|
||||
Permission `auth.breakglass.admin`. Audit 2026-05-10 CRIT-4
|
||||
closure — backs the GUI Break-glass admin page. The password
|
||||
hash is NEVER serialized to the wire; only the credential
|
||||
metadata.
|
||||
|
||||
Returns 404 when `CERTCTL_BREAKGLASS_ENABLED=false` (surface-
|
||||
invisibility: an attacker probing the admin surface gets the
|
||||
same signal as probing the login endpoint).
|
||||
operationId: listBreakglassCredentials
|
||||
responses:
|
||||
"200":
|
||||
description: Credential list
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/BreakglassCredentialListResponse"
|
||||
"401": { description: Unauthorized }
|
||||
"403": { description: Forbidden }
|
||||
"404": { description: Break-glass disabled }
|
||||
"500": { description: Internal error }
|
||||
post:
|
||||
tags: [Auth]
|
||||
summary: Set a break-glass password for an actor
|
||||
description: |
|
||||
Permission `auth.breakglass.admin`. Creates or rotates a
|
||||
break-glass credential for the named `actor_id`. Password
|
||||
strength is validated by the service (min 12 bytes, max 256
|
||||
bytes); weak passwords return 400.
|
||||
|
||||
Returns 404 when `CERTCTL_BREAKGLASS_ENABLED=false`.
|
||||
operationId: setBreakglassPassword
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/BreakglassSetPasswordRequest"
|
||||
responses:
|
||||
"201":
|
||||
description: Credential created or rotated
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/BreakglassSetPasswordResponse"
|
||||
"400": { description: Password fails strength requirements or invalid JSON body }
|
||||
"401": { description: Unauthorized }
|
||||
"403": { description: Forbidden }
|
||||
"404": { description: Break-glass disabled }
|
||||
"500": { description: Internal error }
|
||||
|
||||
/api/v1/auth/breakglass/credentials/{actor_id}:
|
||||
delete:
|
||||
tags: [Auth]
|
||||
summary: Remove a break-glass credential
|
||||
description: |
|
||||
Permission `auth.breakglass.admin`. Returns 404 when
|
||||
`CERTCTL_BREAKGLASS_ENABLED=false` OR when the credential
|
||||
doesn't exist.
|
||||
operationId: removeBreakglassCredential
|
||||
parameters:
|
||||
- in: path
|
||||
name: actor_id
|
||||
required: true
|
||||
schema: { type: string }
|
||||
responses:
|
||||
"204": { description: Credential removed }
|
||||
"400": { description: Missing actor_id path param }
|
||||
"401": { description: Unauthorized }
|
||||
"403": { description: Forbidden }
|
||||
"404": { description: Break-glass disabled OR credential not found }
|
||||
"500": { description: Internal error }
|
||||
|
||||
/api/v1/auth/breakglass/credentials/{actor_id}/unlock:
|
||||
post:
|
||||
tags: [Auth]
|
||||
summary: Clear the lockout on a break-glass credential
|
||||
description: |
|
||||
Permission `auth.breakglass.admin`. Resets the failure counter
|
||||
and clears any active lockout on the named credential so the
|
||||
actor can attempt to log in again after the configured
|
||||
lockout window has elapsed organically OR an admin unblocks
|
||||
them early.
|
||||
|
||||
Returns 404 when `CERTCTL_BREAKGLASS_ENABLED=false` OR when
|
||||
the credential doesn't exist.
|
||||
operationId: unlockBreakglassCredential
|
||||
parameters:
|
||||
- in: path
|
||||
name: actor_id
|
||||
required: true
|
||||
schema: { type: string }
|
||||
responses:
|
||||
"204": { description: Lockout cleared }
|
||||
"400": { description: Missing actor_id path param }
|
||||
"401": { description: Unauthorized }
|
||||
"403": { description: Forbidden }
|
||||
"404": { description: Break-glass disabled OR credential not found }
|
||||
"500": { description: Internal error }
|
||||
|
||||
/api/v1/auth/users:
|
||||
get:
|
||||
tags: [Auth]
|
||||
summary: List federated users for the active tenant
|
||||
description: |
|
||||
Permission `auth.user.read`. Audit 2026-05-10 MED-11 +
|
||||
2026-05-11 A-2 — backs the admin GUI Users page. Pagination is
|
||||
not server-side; the repository's `ListAll` returns every row
|
||||
and the handler filters client-side. Optional
|
||||
`oidc_provider_id` query parameter scopes the list to users
|
||||
federated from one provider.
|
||||
operationId: listAuthUsers
|
||||
parameters:
|
||||
- in: query
|
||||
name: oidc_provider_id
|
||||
required: false
|
||||
schema: { type: string }
|
||||
description: When set, only return users whose `oidc_provider_id` matches exactly.
|
||||
responses:
|
||||
"200":
|
||||
description: User list
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [users]
|
||||
properties:
|
||||
users:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/AuthUser"
|
||||
"401": { description: Unauthorized }
|
||||
"403": { description: Forbidden }
|
||||
"500": { description: Internal error }
|
||||
|
||||
/api/v1/auth/users/{id}:
|
||||
delete:
|
||||
tags: [Auth]
|
||||
summary: Deactivate a user (sets deactivated_at + cascade-revokes active sessions)
|
||||
description: |
|
||||
Permission `auth.user.deactivate`. Audit 2026-05-11 A-2 — the
|
||||
handler rejects self-deactivation with 409 to prevent the
|
||||
"admin deactivates self, can't reactivate themselves" foot-gun
|
||||
(break-glass remains the recovery path).
|
||||
operationId: deactivateAuthUser
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema: { type: string }
|
||||
responses:
|
||||
"204": { description: User deactivated + active sessions revoked }
|
||||
"400": { description: Missing user id }
|
||||
"401": { description: Unauthorized }
|
||||
"403": { description: Forbidden }
|
||||
"404": { description: User not found }
|
||||
"409": { description: Refused — caller is deactivating their own account (use break-glass recovery or have another admin act) }
|
||||
"500": { description: Internal error }
|
||||
|
||||
/api/v1/auth/users/{id}/reactivate:
|
||||
post:
|
||||
tags: [Auth]
|
||||
summary: Reactivate a previously-deactivated user
|
||||
description: |
|
||||
Permission `auth.user.deactivate` (same gate as the inverse
|
||||
op — reactivation is not a separate privilege). Idempotent:
|
||||
reactivating an already-active user is a no-op (204).
|
||||
operationId: reactivateAuthUser
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema: { type: string }
|
||||
responses:
|
||||
"204": { description: User reactivated (or was already active — idempotent) }
|
||||
"400": { description: Missing user id }
|
||||
"401": { description: Unauthorized }
|
||||
"403": { description: Forbidden }
|
||||
"404": { description: User not found }
|
||||
"500": { description: Internal error }
|
||||
|
||||
/api/v1/auth/runtime-config:
|
||||
get:
|
||||
tags: [Auth]
|
||||
summary: Read the deployed auth-related runtime configuration
|
||||
description: |
|
||||
Permission `auth.role.assign` (admin-class — gated tighter
|
||||
than `auth.user.read` so non-admins can't enumerate the
|
||||
deployment's auth knobs). Audit 2026-05-10 MED-12 — backs the
|
||||
GUI AuthSettings page so operators can verify the deployed
|
||||
configuration matches their intent from the browser without
|
||||
SSH access to the host.
|
||||
|
||||
Read-only — no mutation surface. Config changes require a
|
||||
restart + env-var edit by design.
|
||||
operationId: getAuthRuntimeConfig
|
||||
responses:
|
||||
"200":
|
||||
description: Flat-map view of the relevant CERTCTL_* env vars
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [runtime_config]
|
||||
properties:
|
||||
runtime_config:
|
||||
type: object
|
||||
additionalProperties: { type: string }
|
||||
description: |
|
||||
Map of CERTCTL_* env var name → resolved value.
|
||||
The exact key set depends on the deployment's
|
||||
configured auth surface (OIDC, break-glass, etc.).
|
||||
"401": { description: Unauthorized }
|
||||
"403": { description: Forbidden }
|
||||
"500": { description: Internal error }
|
||||
|
||||
/api/v1/version:
|
||||
get:
|
||||
tags: [Health]
|
||||
@@ -7061,3 +7289,125 @@ components:
|
||||
type: string
|
||||
role_id:
|
||||
type: string
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════
|
||||
# Phase 13 Sprint 13.5 (ARCH-H1 batch 2) schemas. Field-by-field
|
||||
# mirror of the projection types in
|
||||
# internal/api/handler/auth_breakglass.go + auth_users.go.
|
||||
# ════════════════════════════════════════════════════════════════════
|
||||
|
||||
BreakglassCredentialResponse:
|
||||
type: object
|
||||
description: |
|
||||
Mirrors internal/api/handler/auth_breakglass.go::
|
||||
breakglassCredentialResponse. Password hash is NEVER serialized
|
||||
to the wire — only metadata.
|
||||
required: [actor_id, created_at, last_password_change_at, failure_count]
|
||||
properties:
|
||||
actor_id:
|
||||
type: string
|
||||
description: Actor the credential belongs to.
|
||||
created_at:
|
||||
type: string
|
||||
format: date-time
|
||||
description: RFC 3339 UTC timestamp the credential was first set.
|
||||
last_password_change_at:
|
||||
type: string
|
||||
format: date-time
|
||||
description: RFC 3339 UTC timestamp the password was most-recently rotated.
|
||||
failure_count:
|
||||
type: integer
|
||||
description: Current consecutive-failure counter (Argon2id lockout state-machine input).
|
||||
locked_until:
|
||||
type: string
|
||||
format: date-time
|
||||
nullable: true
|
||||
description: RFC 3339 UTC timestamp past which the lockout clears organically. Omitted when no active lockout.
|
||||
last_failure_at:
|
||||
type: string
|
||||
format: date-time
|
||||
nullable: true
|
||||
description: RFC 3339 UTC timestamp of the most recent failed-attempt. Omitted when failure_count == 0.
|
||||
|
||||
BreakglassCredentialListResponse:
|
||||
type: object
|
||||
description: |
|
||||
Mirrors internal/api/handler/auth_breakglass.go::
|
||||
listBreakglassCredentialsResponse.
|
||||
required: [credentials]
|
||||
properties:
|
||||
credentials:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/BreakglassCredentialResponse"
|
||||
|
||||
BreakglassSetPasswordRequest:
|
||||
type: object
|
||||
description: |
|
||||
Mirrors internal/api/handler/auth_breakglass.go::
|
||||
breakglassSetPasswordRequest. Password is plaintext on the wire
|
||||
ONLY at set-time; stored at rest as an Argon2id hash with
|
||||
per-record salt.
|
||||
required: [actor_id, password]
|
||||
properties:
|
||||
actor_id:
|
||||
type: string
|
||||
description: Actor the password is being set for.
|
||||
password:
|
||||
type: string
|
||||
format: password
|
||||
description: New break-glass password. Validated server-side against the strength policy (min 12 bytes, max 256 bytes).
|
||||
|
||||
BreakglassSetPasswordResponse:
|
||||
type: object
|
||||
description: |
|
||||
Mirrors the inline response body returned by
|
||||
AuthBreakglassHandler.SetPassword: actor_id + the credential's
|
||||
created_at timestamp (RFC 3339, UTC).
|
||||
required: [actor_id, created_at]
|
||||
properties:
|
||||
actor_id:
|
||||
type: string
|
||||
created_at:
|
||||
type: string
|
||||
format: date-time
|
||||
description: RFC 3339 UTC timestamp the credential row was created (or re-created on rotation).
|
||||
|
||||
AuthUser:
|
||||
type: object
|
||||
description: |
|
||||
Mirrors internal/api/handler/auth_users.go::userResponse. Federated
|
||||
user shape (OIDC subject + provider). `deactivated_at` is the soft-
|
||||
delete marker; nil/absent means the user is active.
|
||||
required: [id, tenant_id, email, display_name, oidc_subject, oidc_provider_id, last_login_at, created_at]
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
description: User identifier (UUID-shaped).
|
||||
tenant_id:
|
||||
type: string
|
||||
email:
|
||||
type: string
|
||||
description: Federated email claim from the IdP.
|
||||
display_name:
|
||||
type: string
|
||||
description: Federated display name (preferred_username or name claim from the IdP).
|
||||
oidc_subject:
|
||||
type: string
|
||||
description: The IdP's `sub` claim for this user (stable identifier across email changes).
|
||||
oidc_provider_id:
|
||||
type: string
|
||||
description: ID of the OIDC provider that minted this user record.
|
||||
last_login_at:
|
||||
type: string
|
||||
format: date-time
|
||||
description: RFC 3339 UTC timestamp of the user's most-recent successful login.
|
||||
created_at:
|
||||
type: string
|
||||
format: date-time
|
||||
description: RFC 3339 UTC timestamp the user row was first created (upserted from an OIDC callback).
|
||||
deactivated_at:
|
||||
type: string
|
||||
format: date-time
|
||||
nullable: true
|
||||
description: RFC 3339 UTC timestamp the user was deactivated. Omitted when the user is active.
|
||||
|
||||
Reference in New Issue
Block a user