From 9135c4490891c9051822584a532b872e64c290c7 Mon Sep 17 00:00:00 2001 From: shankar0123 Date: Thu, 14 May 2026 12:28:29 +0000 Subject: [PATCH] =?UTF-8?q?docs(arch-h1):=20Phase=2013=20Sprint=2013.5=20?= =?UTF-8?q?=E2=80=94=20OpenAPI=20breakglass=20+=20users=20+=20runtime-conf?= =?UTF-8?q?ig=20ops=20(batch=202,=208=20ops)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- api/openapi-handler-exceptions-baseline.txt | 2 +- api/openapi-handler-exceptions.yaml | 35 +- api/openapi.yaml | 350 ++++++++++++++++++++ 3 files changed, 357 insertions(+), 30 deletions(-) diff --git a/api/openapi-handler-exceptions-baseline.txt b/api/openapi-handler-exceptions-baseline.txt index 60d3b2f..7f8f011 100644 --- a/api/openapi-handler-exceptions-baseline.txt +++ b/api/openapi-handler-exceptions-baseline.txt @@ -1 +1 @@ -15 +7 diff --git a/api/openapi-handler-exceptions.yaml b/api/openapi-handler-exceptions.yaml index 21100e9..8253a4e 100644 --- a/api/openapi-handler-exceptions.yaml +++ b/api/openapi-handler-exceptions.yaml @@ -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 diff --git a/api/openapi.yaml b/api/openapi.yaml index 5841340..815f61c 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -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.