● Increment 3 complete. 147 tests pass (+13), clippy clean. Marking it done: #39

Merged
navicore merged 2 commits from increment-3 into main 2026-05-29 20:26:18 +00:00
Owner

● What landed:

┌────────────────┬───────────────────────────────┬──────────────────────────────────────────────────────────┐
│ Piece │ File │ Notes │
├────────────────┼───────────────────────────────┼──────────────────────────────────────────────────────────┤
│ │ │ RealmCreated/Deleted/MfaRequirementChanged, │
│ 12 new audit │ src/audit.rs │ KeyRotated/Deactivated/Deleted, │
│ actions │ │ UserCreated/Deleted/GroupsUpdated, │
│ │ │ ClientCreated/Deleted, SessionsRevoked │
├────────────────┼───────────────────────────────┼──────────────────────────────────────────────────────────┤
│ │ │ Request DTOs (CreateRealm, SetMfaRequired, RotateKey, │
│ 9 new contract │ src/admin_api.rs │ CreateUser, UpdateGroups, CreateClient) + secret-once │
│ types │ │ response DTOs (ClientCreatedResponse, │
│ │ │ MfaEnrollmentResponse, RevokeSessionsResponse) │
├────────────────┼───────────────────────────────┼──────────────────────────────────────────────────────────┤
│ audit_detail() │ │ AdminContext::audit_detail() returns "source=admin-api │
│ helper │ src/server/admin/auth.rs │ admin_sub="; every write handler attributes the │
│ │ │ operator consistently │
├────────────────┼───────────────────────────────┼──────────────────────────────────────────────────────────┤
│ │ src/server/admin/{lists.rs → │ Deleted lists.rs; each new file holds both reads + │
│ Reorganization │ realms.rs, users.rs, │ writes for its resource. Mirrors │
│ │ clients.rs} │ cli/{realm,user,client}.rs — sets up Increment 4 where │
│ │ │ each cli/*.rs backs one resource module. │
├────────────────┼───────────────────────────────┼──────────────────────────────────────────────────────────┤
│ │ │ All 22 admin routes wired (2 probes + 20 resource). │
│ Lean router │ src/server/admin/mod.rs │ Module declarations + router + probes + AdminContext │
│ │ │ re-export, ~190 lines including the route-coverage test. │
└────────────────┴───────────────────────────────┴──────────────────────────────────────────────────────────┘

Routes added beyond Increment 2 (13 writes):

┌────────┬─────────────────────────────────────────────────┬─────────────────────────────────────────────────┐
│ Verb │ Path │ What │
├────────┼─────────────────────────────────────────────────┼─────────────────────────────────────────────────┤
│ POST │ /admin/realms │ create realm │
├────────┼─────────────────────────────────────────────────┼─────────────────────────────────────────────────┤
│ DELETE │ /admin/realms/{realm} │ delete realm (cascades) │
├────────┼─────────────────────────────────────────────────┼─────────────────────────────────────────────────┤
│ PUT │ /admin/realms/{realm}/mfa-required │ toggle MFA requirement │
├────────┼─────────────────────────────────────────────────┼─────────────────────────────────────────────────┤
│ POST │ /admin/realms/{realm}/keys │ rotate (add active key) │
├────────┼─────────────────────────────────────────────────┼─────────────────────────────────────────────────┤
│ POST │ /admin/realms/{realm}/keys/{kid}/deactivate │ deactivate (stays in JWKS) │
├────────┼─────────────────────────────────────────────────┼─────────────────────────────────────────────────┤
│ DELETE │ /admin/realms/{realm}/keys/{kid} │ hard-delete (verification fails immediately) │
├────────┼─────────────────────────────────────────────────┼─────────────────────────────────────────────────┤
│ POST │ /admin/realms/{realm}/users │ create user (server-side Argon2id) │
├────────┼─────────────────────────────────────────────────┼─────────────────────────────────────────────────┤
│ DELETE │ /admin/realms/{realm}/users/{username} │ remove user │
├────────┼─────────────────────────────────────────────────┼─────────────────────────────────────────────────┤
│ PUT │ /admin/realms/{realm}/users/{username}/groups │ replace group set │
├────────┼─────────────────────────────────────────────────┼─────────────────────────────────────────────────┤
│ POST │ /admin/realms/{realm}/users/{username}/disable │ disable + revoke sessions + revoke refresh │
│ │ │ tokens │
├────────┼─────────────────────────────────────────────────┼─────────────────────────────────────────────────┤
│ POST │ /admin/realms/{realm}/users/{username}/enable │ re-enable │
├────────┼─────────────────────────────────────────────────┼─────────────────────────────────────────────────┤
│ POST │ /admin/realms/{realm}/users/{username}/mfa │ enroll TOTP, returns secret+URI+recovery codes │
│ │ │ once │
├────────┼─────────────────────────────────────────────────┼─────────────────────────────────────────────────┤
│ DELETE │ /admin/realms/{realm}/users/{username}/mfa │ disable MFA (recovery path) │
├────────┼─────────────────────────────────────────────────┼─────────────────────────────────────────────────┤
│ DELETE │ /admin/realms/{realm}/users/{username}/sessions │ revoke all sessions, returns count │
├────────┼─────────────────────────────────────────────────┼─────────────────────────────────────────────────┤
│ POST │ /admin/realms/{realm}/clients │ create regular or service-account client; │
│ │ │ returns generated secret once if minted │
├────────┼─────────────────────────────────────────────────┼─────────────────────────────────────────────────┤
│ DELETE │ /admin/realms/{realm}/clients/{client_id} │ remove client │
└────────┴─────────────────────────────────────────────────┴─────────────────────────────────────────────────┘

Security posture, verified by tests:

  • password_hash never appears even on user-create response; literal password doesn't echo back
  • client_secret raw value appears only in POST /admin/realms/.../clients response when generated, never
    afterward; subsequent fetches report has_secret: true only
  • MFA enrollment returns plaintext secret + URI + recovery codes once; storage uses encrypted secret + hashed
    recovery codes (existing crypto path reused)
  • Service-account validation: requires audience AND non-empty scopes (400 otherwise), mirrors local CLI
  • Regular client validation: rejects empty / duplicate redirect_uris (400)
  • All 22 admin routes (2 probes + 20 resource) go through AdminContext — every_admin_route_requires_auth
    iterates all of them and asserts 401 without bearer. Failing assertion names the offending METHOD path, so a
    future copy-paste that forgets the extractor breaks loudly.

Audit attribution: every successful write emits an audit event with detail containing source=admin-api
admin_sub=<operator_uid> plus operation-specific fields (e.g. algorithm=EdDSA kid= for key rotation,
sessions=N refresh_tokens=N for user disable).

● What landed: ┌────────────────┬───────────────────────────────┬──────────────────────────────────────────────────────────┐ │ Piece │ File │ Notes │ ├────────────────┼───────────────────────────────┼──────────────────────────────────────────────────────────┤ │ │ │ RealmCreated/Deleted/MfaRequirementChanged, │ │ 12 new audit │ src/audit.rs │ KeyRotated/Deactivated/Deleted, │ │ actions │ │ UserCreated/Deleted/GroupsUpdated, │ │ │ │ ClientCreated/Deleted, SessionsRevoked │ ├────────────────┼───────────────────────────────┼──────────────────────────────────────────────────────────┤ │ │ │ Request DTOs (CreateRealm, SetMfaRequired, RotateKey, │ │ 9 new contract │ src/admin_api.rs │ CreateUser, UpdateGroups, CreateClient) + secret-once │ │ types │ │ response DTOs (ClientCreatedResponse, │ │ │ │ MfaEnrollmentResponse, RevokeSessionsResponse) │ ├────────────────┼───────────────────────────────┼──────────────────────────────────────────────────────────┤ │ audit_detail() │ │ AdminContext::audit_detail() returns "source=admin-api │ │ helper │ src/server/admin/auth.rs │ admin_sub=<id>"; every write handler attributes the │ │ │ │ operator consistently │ ├────────────────┼───────────────────────────────┼──────────────────────────────────────────────────────────┤ │ │ src/server/admin/{lists.rs → │ Deleted lists.rs; each new file holds both reads + │ │ Reorganization │ realms.rs, users.rs, │ writes for its resource. Mirrors │ │ │ clients.rs} │ cli/{realm,user,client}.rs — sets up Increment 4 where │ │ │ │ each cli/*.rs backs one resource module. │ ├────────────────┼───────────────────────────────┼──────────────────────────────────────────────────────────┤ │ │ │ All 22 admin routes wired (2 probes + 20 resource). │ │ Lean router │ src/server/admin/mod.rs │ Module declarations + router + probes + AdminContext │ │ │ │ re-export, ~190 lines including the route-coverage test. │ └────────────────┴───────────────────────────────┴──────────────────────────────────────────────────────────┘ Routes added beyond Increment 2 (13 writes): ┌────────┬─────────────────────────────────────────────────┬─────────────────────────────────────────────────┐ │ Verb │ Path │ What │ ├────────┼─────────────────────────────────────────────────┼─────────────────────────────────────────────────┤ │ POST │ /admin/realms │ create realm │ ├────────┼─────────────────────────────────────────────────┼─────────────────────────────────────────────────┤ │ DELETE │ /admin/realms/{realm} │ delete realm (cascades) │ ├────────┼─────────────────────────────────────────────────┼─────────────────────────────────────────────────┤ │ PUT │ /admin/realms/{realm}/mfa-required │ toggle MFA requirement │ ├────────┼─────────────────────────────────────────────────┼─────────────────────────────────────────────────┤ │ POST │ /admin/realms/{realm}/keys │ rotate (add active key) │ ├────────┼─────────────────────────────────────────────────┼─────────────────────────────────────────────────┤ │ POST │ /admin/realms/{realm}/keys/{kid}/deactivate │ deactivate (stays in JWKS) │ ├────────┼─────────────────────────────────────────────────┼─────────────────────────────────────────────────┤ │ DELETE │ /admin/realms/{realm}/keys/{kid} │ hard-delete (verification fails immediately) │ ├────────┼─────────────────────────────────────────────────┼─────────────────────────────────────────────────┤ │ POST │ /admin/realms/{realm}/users │ create user (server-side Argon2id) │ ├────────┼─────────────────────────────────────────────────┼─────────────────────────────────────────────────┤ │ DELETE │ /admin/realms/{realm}/users/{username} │ remove user │ ├────────┼─────────────────────────────────────────────────┼─────────────────────────────────────────────────┤ │ PUT │ /admin/realms/{realm}/users/{username}/groups │ replace group set │ ├────────┼─────────────────────────────────────────────────┼─────────────────────────────────────────────────┤ │ POST │ /admin/realms/{realm}/users/{username}/disable │ disable + revoke sessions + revoke refresh │ │ │ │ tokens │ ├────────┼─────────────────────────────────────────────────┼─────────────────────────────────────────────────┤ │ POST │ /admin/realms/{realm}/users/{username}/enable │ re-enable │ ├────────┼─────────────────────────────────────────────────┼─────────────────────────────────────────────────┤ │ POST │ /admin/realms/{realm}/users/{username}/mfa │ enroll TOTP, returns secret+URI+recovery codes │ │ │ │ once │ ├────────┼─────────────────────────────────────────────────┼─────────────────────────────────────────────────┤ │ DELETE │ /admin/realms/{realm}/users/{username}/mfa │ disable MFA (recovery path) │ ├────────┼─────────────────────────────────────────────────┼─────────────────────────────────────────────────┤ │ DELETE │ /admin/realms/{realm}/users/{username}/sessions │ revoke all sessions, returns count │ ├────────┼─────────────────────────────────────────────────┼─────────────────────────────────────────────────┤ │ POST │ /admin/realms/{realm}/clients │ create regular or service-account client; │ │ │ │ returns generated secret once if minted │ ├────────┼─────────────────────────────────────────────────┼─────────────────────────────────────────────────┤ │ DELETE │ /admin/realms/{realm}/clients/{client_id} │ remove client │ └────────┴─────────────────────────────────────────────────┴─────────────────────────────────────────────────┘ Security posture, verified by tests: - password_hash never appears even on user-create response; literal password doesn't echo back - client_secret raw value appears only in POST /admin/realms/.../clients response when generated, never afterward; subsequent fetches report has_secret: true only - MFA enrollment returns plaintext secret + URI + recovery codes once; storage uses encrypted secret + hashed recovery codes (existing crypto path reused) - Service-account validation: requires audience AND non-empty scopes (400 otherwise), mirrors local CLI - Regular client validation: rejects empty / duplicate redirect_uris (400) - All 22 admin routes (2 probes + 20 resource) go through AdminContext — every_admin_route_requires_auth iterates all of them and asserts 401 without bearer. Failing assertion names the offending METHOD path, so a future copy-paste that forgets the extractor breaks loudly. Audit attribution: every successful write emits an audit event with detail containing source=admin-api admin_sub=<operator_uid> plus operation-specific fields (e.g. algorithm=EdDSA kid=<kid> for key rotation, sessions=N refresh_tokens=N for user disable).
● Increment 3 complete. 147 tests pass (+13), clippy clean. Marking it done:
All checks were successful
CI / ci (pull_request) Successful in 3m41s
c1bf050fa0
● What landed:

  ┌────────────────┬───────────────────────────────┬──────────────────────────────────────────────────────────┐
  │     Piece      │             File              │                          Notes                           │
  ├────────────────┼───────────────────────────────┼──────────────────────────────────────────────────────────┤
  │                │                               │ RealmCreated/Deleted/MfaRequirementChanged,              │
  │ 12 new audit   │ src/audit.rs                  │ KeyRotated/Deactivated/Deleted,                          │
  │ actions        │                               │ UserCreated/Deleted/GroupsUpdated,                       │
  │                │                               │ ClientCreated/Deleted, SessionsRevoked                   │
  ├────────────────┼───────────────────────────────┼──────────────────────────────────────────────────────────┤
  │                │                               │ Request DTOs (CreateRealm, SetMfaRequired, RotateKey,    │
  │ 9 new contract │ src/admin_api.rs              │ CreateUser, UpdateGroups, CreateClient) + secret-once    │
  │  types         │                               │ response DTOs (ClientCreatedResponse,                    │
  │                │                               │ MfaEnrollmentResponse, RevokeSessionsResponse)           │
  ├────────────────┼───────────────────────────────┼──────────────────────────────────────────────────────────┤
  │ audit_detail() │                               │ AdminContext::audit_detail() returns "source=admin-api   │
  │  helper        │ src/server/admin/auth.rs      │ admin_sub=<id>"; every write handler attributes the      │
  │                │                               │ operator consistently                                    │
  ├────────────────┼───────────────────────────────┼──────────────────────────────────────────────────────────┤
  │                │ src/server/admin/{lists.rs →  │ Deleted lists.rs; each new file holds both reads +       │
  │ Reorganization │ realms.rs, users.rs,          │ writes for its resource. Mirrors                         │
  │                │ clients.rs}                   │ cli/{realm,user,client}.rs — sets up Increment 4 where   │
  │                │                               │ each cli/*.rs backs one resource module.                 │
  ├────────────────┼───────────────────────────────┼──────────────────────────────────────────────────────────┤
  │                │                               │ All 22 admin routes wired (2 probes + 20 resource).      │
  │ Lean router    │ src/server/admin/mod.rs       │ Module declarations + router + probes + AdminContext     │
  │                │                               │ re-export, ~190 lines including the route-coverage test. │
  └────────────────┴───────────────────────────────┴──────────────────────────────────────────────────────────┘

  Routes added beyond Increment 2 (13 writes):

  ┌────────┬─────────────────────────────────────────────────┬─────────────────────────────────────────────────┐
  │  Verb  │                      Path                       │                      What                       │
  ├────────┼─────────────────────────────────────────────────┼─────────────────────────────────────────────────┤
  │ POST   │ /admin/realms                                   │ create realm                                    │
  ├────────┼─────────────────────────────────────────────────┼─────────────────────────────────────────────────┤
  │ DELETE │ /admin/realms/{realm}                           │ delete realm (cascades)                         │
  ├────────┼─────────────────────────────────────────────────┼─────────────────────────────────────────────────┤
  │ PUT    │ /admin/realms/{realm}/mfa-required              │ toggle MFA requirement                          │
  ├────────┼─────────────────────────────────────────────────┼─────────────────────────────────────────────────┤
  │ POST   │ /admin/realms/{realm}/keys                      │ rotate (add active key)                         │
  ├────────┼─────────────────────────────────────────────────┼─────────────────────────────────────────────────┤
  │ POST   │ /admin/realms/{realm}/keys/{kid}/deactivate     │ deactivate (stays in JWKS)                      │
  ├────────┼─────────────────────────────────────────────────┼─────────────────────────────────────────────────┤
  │ DELETE │ /admin/realms/{realm}/keys/{kid}                │ hard-delete (verification fails immediately)    │
  ├────────┼─────────────────────────────────────────────────┼─────────────────────────────────────────────────┤
  │ POST   │ /admin/realms/{realm}/users                     │ create user (server-side Argon2id)              │
  ├────────┼─────────────────────────────────────────────────┼─────────────────────────────────────────────────┤
  │ DELETE │ /admin/realms/{realm}/users/{username}          │ remove user                                     │
  ├────────┼─────────────────────────────────────────────────┼─────────────────────────────────────────────────┤
  │ PUT    │ /admin/realms/{realm}/users/{username}/groups   │ replace group set                               │
  ├────────┼─────────────────────────────────────────────────┼─────────────────────────────────────────────────┤
  │ POST   │ /admin/realms/{realm}/users/{username}/disable  │ disable + revoke sessions + revoke refresh      │
  │        │                                                 │ tokens                                          │
  ├────────┼─────────────────────────────────────────────────┼─────────────────────────────────────────────────┤
  │ POST   │ /admin/realms/{realm}/users/{username}/enable   │ re-enable                                       │
  ├────────┼─────────────────────────────────────────────────┼─────────────────────────────────────────────────┤
  │ POST   │ /admin/realms/{realm}/users/{username}/mfa      │ enroll TOTP, returns secret+URI+recovery codes  │
  │        │                                                 │ once                                            │
  ├────────┼─────────────────────────────────────────────────┼─────────────────────────────────────────────────┤
  │ DELETE │ /admin/realms/{realm}/users/{username}/mfa      │ disable MFA (recovery path)                     │
  ├────────┼─────────────────────────────────────────────────┼─────────────────────────────────────────────────┤
  │ DELETE │ /admin/realms/{realm}/users/{username}/sessions │ revoke all sessions, returns count              │
  ├────────┼─────────────────────────────────────────────────┼─────────────────────────────────────────────────┤
  │ POST   │ /admin/realms/{realm}/clients                   │ create regular or service-account client;       │
  │        │                                                 │ returns generated secret once if minted         │
  ├────────┼─────────────────────────────────────────────────┼─────────────────────────────────────────────────┤
  │ DELETE │ /admin/realms/{realm}/clients/{client_id}       │ remove client                                   │
  └────────┴─────────────────────────────────────────────────┴─────────────────────────────────────────────────┘

  Security posture, verified by tests:
  - password_hash never appears even on user-create response; literal password doesn't echo back
  - client_secret raw value appears only in POST /admin/realms/.../clients response when generated, never
  afterward; subsequent fetches report has_secret: true only
  - MFA enrollment returns plaintext secret + URI + recovery codes once; storage uses encrypted secret + hashed
  recovery codes (existing crypto path reused)
  - Service-account validation: requires audience AND non-empty scopes (400 otherwise), mirrors local CLI
  - Regular client validation: rejects empty / duplicate redirect_uris (400)
  - All 22 admin routes (2 probes + 20 resource) go through AdminContext — every_admin_route_requires_auth
  iterates all of them and asserts 401 without bearer. Failing assertion names the offending METHOD path, so a
  future copy-paste that forgets the extractor breaks loudly.

  Audit attribution: every successful write emits an audit event with detail containing source=admin-api
  admin_sub=<operator_uid> plus operation-specific fields (e.g. algorithm=EdDSA kid=<kid> for key rotation,
  sessions=N refresh_tokens=N for user disable).
Author
Owner

Review: Increment 3 — admin write endpoints

Verdict: solid, ready to merge. The 13 write routes mirror the local CLI faithfully, every write is audited with operator attribution, and the secret-handling contract holds. Verified locally: clippy clean with -D warnings, 147 tests pass. My Increment 2 note is resolved (every_admin_route_requires_auth iterates all 22 routes and asserts 401 without a bearer, naming the offending METHOD+path on failure).

Verified against the code

  • Client-secret parity is exact — the one cross-check that matters here. The API mints secrets identically to the local CLI (cli/client.rs:88-90): 32 random bytes → URL_SAFE_NO_PADhex(SHA256(raw)) stored as the hash. verify_client_secret (token.rs:496) hashes the presented secret the same way and compares constant-time. So an API-created client (regular or service account) authenticates correctly at /token. Raw secret appears once in the create response; has_secret thereafter.
  • No secret material on the wirepassword_hash absent even on user-create (test also asserts the literal password doesn't echo); MFA secret/URI/recovery codes returned once while storage uses the encrypted-secret + hashed-recovery-code path; signing-key responses stay metadata-only.
  • Validation mirrors the CLI — service account requires audience + non-empty scopes (400 otherwise); regular client rejects empty/duplicate redirect URIs (400); empty password rejected (400).
  • disable_user does the full job — sets the flag, revokes sessions, revokes refresh tokens, and audits sessions=N refresh_tokens=N. Idempotent when already disabled.
  • Audit attribution is consistentaudit_detail() stamps source=admin-api admin_sub=<uid> on every write, plus operation-specific fields (kid=, algorithm=, count=, groups=, …).
  • MFA re-enroll is safeuser_mfa::enroll is transactional (delete-then-insert), so re-enrolling an already-enrolled user replaces cleanly rather than erroring.

Worth addressing

  1. Conflict creates return 500, not 4xx. AppError's From<rusqlite::Error>/From<anyhow::Error> both map to Internal → 500 (and a tracing::error!). So creating a realm/user/client with a name that already exists hits the UNIQUE constraint and surfaces as 500 internal server error — a client-caused error reported as a server fault, and it pollutes the error log. Recommend mapping the constraint violation (or a pre-existence check) to 409 Conflict before Increment 4's CLI lands, so the CLI can render "already exists" instead of "internal server error". This is the one I'd actually want fixed. (Note: MFA enroll is exempt — it's an idempotent upsert.)
  2. No self-lockout guard. Nothing stops an admin from deleting the admin realm itself (cascades away every admin + the admin signing keys), deleting/disabling their own admin user, or removing anz-admin from themselves. It's recoverable via the local break-glass CLI — which is the design's intent — but a guard (refuse to delete config.admin_realm, or refuse to remove the last anz-admin member) would turn a one-command outage into a clear 4xx. At minimum worth a deliberate "we accept this" decision.

Minor / cosmetic

  • create_client + configure_service_account are two non-transactional statements; a failure on the second leaves a created-but-unconfigured client. Matches the CLI pattern, but a partial create is more surprising over a remote API — a transaction would make it atomic.
  • rotate_key builds the response created_at from Utc::now() rather than the stored row, so the DTO timestamp can drift from storage by microseconds. Purely cosmetic.
## Review: Increment 3 — admin write endpoints **Verdict: solid, ready to merge.** The 13 write routes mirror the local CLI faithfully, every write is audited with operator attribution, and the secret-handling contract holds. Verified locally: clippy clean with `-D warnings`, 147 tests pass. My Increment 2 note is resolved (`every_admin_route_requires_auth` iterates all 22 routes and asserts 401 without a bearer, naming the offending METHOD+path on failure). ### Verified against the code - **Client-secret parity is exact** — the one cross-check that matters here. The API mints secrets identically to the local CLI (`cli/client.rs:88-90`): 32 random bytes → `URL_SAFE_NO_PAD` → `hex(SHA256(raw))` stored as the hash. `verify_client_secret` (`token.rs:496`) hashes the presented secret the same way and compares constant-time. So an API-created client (regular or service account) authenticates correctly at `/token`. Raw secret appears once in the create response; `has_secret` thereafter. - **No secret material on the wire** — `password_hash` absent even on user-create (test also asserts the literal password doesn't echo); MFA secret/URI/recovery codes returned once while storage uses the encrypted-secret + hashed-recovery-code path; signing-key responses stay metadata-only. - **Validation mirrors the CLI** — service account requires audience + non-empty scopes (400 otherwise); regular client rejects empty/duplicate redirect URIs (400); empty password rejected (400). - **`disable_user` does the full job** — sets the flag, revokes sessions, revokes refresh tokens, and audits `sessions=N refresh_tokens=N`. Idempotent when already disabled. - **Audit attribution is consistent** — `audit_detail()` stamps `source=admin-api admin_sub=<uid>` on every write, plus operation-specific fields (`kid=`, `algorithm=`, `count=`, `groups=`, …). - **MFA re-enroll is safe** — `user_mfa::enroll` is transactional (delete-then-insert), so re-enrolling an already-enrolled user replaces cleanly rather than erroring. ### Worth addressing 1. **Conflict creates return 500, not 4xx.** `AppError`'s `From<rusqlite::Error>`/`From<anyhow::Error>` both map to `Internal` → 500 (and a `tracing::error!`). So creating a realm/user/client with a name that already exists hits the UNIQUE constraint and surfaces as `500 internal server error` — a client-caused error reported as a server fault, and it pollutes the error log. Recommend mapping the constraint violation (or a pre-existence check) to **409 Conflict** before Increment 4's CLI lands, so the CLI can render "already exists" instead of "internal server error". This is the one I'd actually want fixed. (Note: MFA enroll is exempt — it's an idempotent upsert.) 2. **No self-lockout guard.** Nothing stops an admin from deleting the admin realm itself (cascades away every admin + the admin signing keys), deleting/disabling their own admin user, or removing `anz-admin` from themselves. It's recoverable via the local break-glass CLI — which is the design's intent — but a guard (refuse to delete `config.admin_realm`, or refuse to remove the last `anz-admin` member) would turn a one-command outage into a clear 4xx. At minimum worth a deliberate "we accept this" decision. ### Minor / cosmetic - `create_client` + `configure_service_account` are two non-transactional statements; a failure on the second leaves a created-but-unconfigured client. Matches the CLI pattern, but a partial create is more surprising over a remote API — a transaction would make it atomic. - `rotate_key` builds the response `created_at` from `Utc::now()` rather than the stored row, so the DTO timestamp can drift from storage by microseconds. Purely cosmetic.
● All green: 151 tests pass (+4 over the merged Increment 3 baseline of 147), clippy clean.
All checks were successful
CI / ci (pull_request) Successful in 1m45s
b3c0ce6f85
Summary of the response to the review:

  Addressed:
  - #1 — Conflict on duplicate creates → New AppError::Conflict (409). Made the conversions smart:
  From<rusqlite::Error> and From<anyhow::Error> both detect SQLITE_CONSTRAINT_UNIQUE and route to Conflict (single
   is_unique_violation helper avoids duplication). Critically — the db:: layer returns anyhow::Result, so the
  anyhow path was the one that mattered; downcast to rusqlite::Error catches it. New tests:
  create_realm_duplicate_returns_409, create_user_duplicate_returns_409, create_client_duplicate_returns_409.
  Spurious tracing::error! on duplicate names is also gone — that fires only on Internal.
  - #2 — Self-lockout, catastrophic case → delete_realm refuses 403 when the target is config.admin_realm, with a
  message pointing to the local CLI. The doc comment on the handler explicitly accepts the lesser self-lockout
  paths (self-delete user, self-disable, self-degroup) as "the design doc's break-glass framing — one-command
  recoverable on the box." Test: delete_admin_realm_is_refused.

  Deferred (explicit):
  - #3 — Non-transactional create_client + configure_service_account → matches the existing local CLI pattern;
  making this atomic is a small but separate refactor that touches the DB layer.
  - #4 — rotate_key created_at μs drift → cosmetic; CLI rendering won't notice. Trivial to query-back later if it
  surfaces as a real issue.
navicore deleted branch increment-3 2026-05-29 20:26:18 +00:00
Sign in to join this conversation.
No description provided.