fix(api): /api/v1 authz hardening — live owner enforcement, job gate, graceful mint refusal#26
Merged
Merged
Conversation
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Audit of the
/api/v1Bearer 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_allowners, so every existing key is admin-ownedscope_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_activeonly 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_resolvenow re-reads the owner on every call and folds their live standing in through a new pure, unit-tested helperapi_keys::clamp_to_owner:None→ 401, same as revoked);caps &= owner_caps,scope_all &= owner_scope_allon the next call;2.
GET /api/v1/jobs/:idungated (authz)The cookie UI gates every
/jobsroute onis_admin_or_higher(), but the API job-poll took any valid key. Now requires ascope_allkey (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 toAppError::Rpc→ a 502 Bad Gateway page for a user-actionable error. Now pre-checksscope_alland 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 (unfilteredGET /hostingsfor non-scope_allkeys) is real but unreachable while the mint gate holds — it's the core of the p1b work, tracked separately.Test
clippy -D warningsclean;clamp_to_ownerunit test (locked / absent / demoted / admin) +hyperion-web+hyperion-statesuites green.🤖 Generated with Claude Code