control-plane-api: capability-mask Authority extractor - #3404
Open
bbartman wants to merge 6 commits into
Open
Conversation
This was referenced Aug 26, 2026
bbartman
force-pushed
the
bmb/3376-stack-3-authority-extractor
branch
2 times, most recently
from
August 27, 2026 12:12
6d68fc0 to
9244e38
Compare
bbartman
force-pushed
the
bmb/3376-stack-3-authority-extractor
branch
from
August 27, 2026 12:22
9244e38 to
ffd8d0b
Compare
bbartman
force-pushed
the
bmb/3376-stack-3-authority-extractor
branch
from
August 28, 2026 12:17
ffd8d0b to
74f369d
Compare
bbartman
force-pushed
the
bmb/3376-stack-3-authority-extractor
branch
from
August 28, 2026 16:59
74f369d to
3fe3eda
Compare
bbartman
force-pushed
the
bmb/3376-stack-3-authority-extractor
branch
from
August 31, 2026 12:45
3fe3eda to
132aae1
Compare
Authority<R: Requirement> composes over Envelope extraction and carries the capability mask computed from the bearer's verified capability_mask claim, with the route's Requirement evaluated at extraction: - Requirement declares REQUIRED capabilities (a fast-fail necessary condition on the mask only — never a substitute for walk enforcement) and REQUIRE_UNMASKED, which keys on the claim's presence, never the mask's value. - A capability shortfall rejects with a structured 403 (Forbidden) naming the missing PascalCase capabilities, the stable contract a client parses to drive an upgrade_token re-mint. - NoRequirement stays Maybe-shaped: unauthenticated requests extract and Envelope::claims() remains the lazy per-callsite identity gate. - The PhantomData<R> field is private, so a requirement-bearing Authority is unforgeable proof that R was evaluated; from_envelope is the assembly path for non-HTTP callers (GraphQL test harnesses). Handlers still extract Envelope; the mechanical migration to Authority follows separately.
The extractor-inheritance snapshots expected bare jsonwebtoken error names, but Envelope's verify path wraps them with context: an expired bearer reports "failed to verify token: ExpiredSignature", and a malformed bearer fails base64 header decoding before any structural InvalidToken check.
Review follow-ups for the Authority extractor: - New test: every bearer fails BOTH Envelope authentication and its route's Requirement, pinning that the Envelope's 401 wins over the Requirement's 403 — expiry and aud against RequireUnmasked, expiry against uncovered REQUIRED capabilities, a wrong-key signature whose mask fully covers the requirement, and a malformed bearer returning the Envelope rejection verbatim on requirement-bearing routes. The existing /none cases can't discriminate this ordering because a vacuous Requirement passes any bearer. - Docs: drop the stale upgrade_token wording (the revised design has clients request a fresh capability token naming what they need), and explain why the aide::OperationInput impl exists ahead of any handler extracting Authority.
The token endpoint (POST /api/v1/auth/token) never extracts Envelope, so it doesn't depend on Maybe-shaped extraction. GraphQL does: its one route serves every operation, so identity errors must surface per-resolver rather than at extraction.
Requirement::REQUIRED is now a slice of CapabilityBundles — the same vocabulary the capability_mask claim names — converted to capability bits at evaluation time via capabilities(), since it is not a const fn. This drops the enum_set! literal spelling and keeps route declarations in the vocabulary a client would echo back in a mask request.
bbartman
force-pushed
the
bmb/3376-stack-3-authority-extractor
branch
from
September 1, 2026 13:08
132aae1 to
68c2eb9
Compare
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.
Part of #3376 (task 3a). Stacked on #3397 (stack 2); review only the last three commits.
Introduces
Authority<R: Requirement>, the extractor which will carry capability-mask context through the request path:Requirementdeclares a route’s compile-time authorization precondition:REQUIRED, a slice ofCapabilityBundles — the same vocabulary thecapability_maskclaim speaks, converted to capability bits at evaluation viacapabilities()(a fast-fail necessary condition on the mask only — never a substitute for walk enforcement) andREQUIRE_UNMASKED, which keys on thecapability_maskclaim's presence, never the mask's value.Forbidden) naming the missing PascalCase capabilities — the stable contract a client parses to drive anupgrade_tokenre-mint. Refusing masked bearers outright yieldsunmasked_token_required, which no re-mint can remedy.NoRequirement(the default) stays Maybe-shaped: unauthenticated requests extract successfully andEnvelope::claims()remains the lazy per-callsite identity gate.Authority { envelope, mask, .. }structurally, like axum'sState. ThePhantomData<R>field is private, so a requirement-bearingAuthorityis unforgeable proof thatRwas evaluated;from_envelopeis the assembly path for non-HTTP callers (GraphQL test harnesses).Zero behavior change: no handler extracts
Authorityyet. The mechanicalEnvelope→Authoritymigration and live mask threading through theALL_CAPABILITIEScall sites follow in stack 3b.Tests cover
evaluate_requirementdirectly and driveAuthority<R>as a real extractor over an axum router, pinning the exact status and body of every refusal, mask-claim carry-through (including unrecognized names and the empty mask), and inheritedEnvelopeauthentication (aud, expiry, malformed bearers).