Skip to content

fix(api): /api/v1 authz hardening — live owner enforcement, job gate, graceful mint refusal#26

Merged
nechodom merged 1 commit into
mainfrom
fix/api-v1-authz-hardening
Jul 2, 2026
Merged

fix(api): /api/v1 authz hardening — live owner enforcement, job gate, graceful mint refusal#26
nechodom merged 1 commit into
mainfrom
fix/api-v1-authz-hardening

Conversation

@nechodom

@nechodom nechodom commented Jul 2, 2026

Copy link
Copy Markdown
Owner

Audit of the /api/v1 Bearer edge (Fable-5 multi-agent pass, verified against the code by hand after the workflow's verify stage hit a session limit) surfaced three authz/UX defects. All three are latent today — the mint gate refuses non-scope_all owners, so every existing key is admin-owned scope_all — but they bite the moment per-tenant keys ship in p1b. Closing them now, before building write endpoints on top.

1. Owner standing not re-checked after mint (authz)

resolve_active only vetted the key row (not revoked, not expired). A locked or down-scoped owner's keys kept their full mint-time caps — directly contradicting the module doc: "Revoking/down-scoping the owner can never be exceeded by a key it minted."

Core api_key_resolve now re-reads the owner on every call and folds their live standing in through a new pure, unit-tested helper api_keys::clamp_to_owner:

  • locked or deleted owner → key inert (None → 401, same as revoked);
  • demoted ownercaps &= owner_caps, scope_all &= owner_scope_all on the next call;
  • healthy admin owner → no-op.

2. GET /api/v1/jobs/:id ungated (authz)

The cookie UI gates every /jobs route on is_admin_or_higher(), but the API job-poll took any valid key. Now requires a scope_all key (403 otherwise). Job records describe cluster-wide operations. Becomes "poll your own jobs" when p1b write endpoints land.

3. Mint refusal → bare 502 (ux/wiring)

Minting from a tenant-scoped account returned RpcError::Validation, which the handler mapped to AppError::Rpc → a 502 Bad Gateway page for a user-actionable error. Now pre-checks scope_all and flash-redirects to Settings; the server-side RPC refusal stays as the authoritative backstop.

Not in scope

The TODO(api-p1b) tenant read-scoping gap (unfiltered GET /hostings for non-scope_all keys) is real but unreachable while the mint gate holds — it's the core of the p1b work, tracked separately.

Test

clippy -D warnings clean; clamp_to_owner unit test (locked / absent / demoted / admin) + hyperion-web + hyperion-state suites green.

🤖 Generated with Claude Code

…efusal

Audit (Fable-5 multi-agent pass) of the /api/v1 Bearer edge surfaced three
authz/UX defects. All three are latent today (every existing key is
scope_all, admin-owned) but bite the moment per-tenant keys ship in p1b, so
close them now — before building write endpoints on top.

1. Owner standing wasn't re-checked after mint. `resolve_active` vetted only
   the key row (revoked/expired); a LOCKED or DOWN-SCOPED owner's keys kept
   their full mint-time power — contradicting the module's own contract
   ("a key can never be exceeded by revoking/down-scoping the owner"). Core
   `api_key_resolve` now re-reads the owner every call and folds their live
   standing in via the new pure `api_keys::clamp_to_owner`: locked/gone owner
   -> key inert (401), demoted owner -> caps/scope_all re-clamped. No-op for
   the usual admin-owned scope_all key.

2. `GET /api/v1/jobs/:id` took ANY valid key, while the cookie UI gates every
   /jobs route on admin. Now requires a scope_all key (403 otherwise).

3. Minting a key from a tenant-scoped account dead-ended on a bare 502 (the
   server-side Validation refusal rendered as AppError::Rpc). Pre-check
   scope_all in the handler and flash-redirect to Settings; the RPC refusal
   stays as the authoritative backstop.

clippy -D warnings clean; clamp_to_owner unit test (locked/absent/demoted/
admin) + hyperion-web + hyperion-state suites green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@nechodom nechodom merged commit afb0b44 into main Jul 2, 2026
1 check passed
@nechodom nechodom deleted the fix/api-v1-authz-hardening branch July 2, 2026 13:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant