Skip to content

control-plane-api: capability-mask Authority extractor - #3404

Open
bbartman wants to merge 6 commits into
bmb/3376-stack-2-masked-user-walkfrom
bmb/3376-stack-3-authority-extractor
Open

control-plane-api: capability-mask Authority extractor#3404
bbartman wants to merge 6 commits into
bmb/3376-stack-2-masked-user-walkfrom
bmb/3376-stack-3-authority-extractor

Conversation

@bbartman

@bbartman bbartman commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

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:

  • Requirement declares a route’s compile-time authorization precondition: REQUIRED, a slice of CapabilityBundles — the same vocabulary the capability_mask claim speaks, converted to capability bits at evaluation via capabilities() (a fast-fail necessary condition on the mask only — never a substitute for walk enforcement) and REQUIRE_UNMASKED, which keys on the capability_mask claim's presence, never the mask's value.
  • A capability shortfall rejects at extraction with a structured 403 (Forbidden) naming the missing PascalCase capabilities — the stable contract a client parses to drive an upgrade_token re-mint. Refusing masked bearers outright yields unmasked_token_required, which no re-mint can remedy.
  • NoRequirement (the default) stays Maybe-shaped: unauthenticated requests extract successfully and Envelope::claims() remains the lazy per-callsite identity gate.
  • Call sites decompose Authority { envelope, mask, .. } structurally, like axum's State. 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).

Zero behavior change: no handler extracts Authority yet. The mechanical EnvelopeAuthority migration and live mask threading through the ALL_CAPABILITIES call sites follow in stack 3b.

Tests cover evaluate_requirement directly and drive Authority<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 inherited Envelope authentication (aud, expiry, malformed bearers).

@bbartman bbartman changed the title control-plane-api: Authority extractor with route Requirements control-plane-api: capability-mask Authority extractor Aug 26, 2026
@bbartman
bbartman force-pushed the bmb/3376-stack-3-authority-extractor branch 2 times, most recently from 6d68fc0 to 9244e38 Compare August 27, 2026 12:12
@bbartman
bbartman force-pushed the bmb/3376-stack-3-authority-extractor branch from 9244e38 to ffd8d0b Compare August 27, 2026 12:22
@bbartman
bbartman force-pushed the bmb/3376-stack-3-authority-extractor branch from ffd8d0b to 74f369d Compare August 28, 2026 12:17
@bbartman
bbartman force-pushed the bmb/3376-stack-3-authority-extractor branch from 74f369d to 3fe3eda Compare August 28, 2026 16:59
@bbartman
bbartman force-pushed the bmb/3376-stack-3-authority-extractor branch from 3fe3eda to 132aae1 Compare August 31, 2026 12:45
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
bbartman force-pushed the bmb/3376-stack-3-authority-extractor branch from 132aae1 to 68c2eb9 Compare September 1, 2026 13:08
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