tables: capability-mask-aware user grant walk - #3397
Open
bbartman wants to merge 3 commits into
Open
Conversation
bbartman
force-pushed
the
bmb/3376-stack-2-masked-user-walk
branch
from
August 25, 2026 18:18
04e67d7 to
d3bfb92
Compare
bbartman
force-pushed
the
bmb/3376-stack-2-masked-user-walk
branch
2 times, most recently
from
August 26, 2026 10:52
c5a4f51 to
dddf5c9
Compare
This was referenced Aug 26, 2026
bbartman
requested review from
GregorShear and
jgraettinger
and removed request for
jgraettinger
August 26, 2026 18:06
bbartman
force-pushed
the
bmb/3376-stack-2-masked-user-walk
branch
3 times, most recently
from
August 27, 2026 12:22
7084155 to
0d8b4fb
Compare
bbartman
force-pushed
the
bmb/3376-stack-2-masked-user-walk
branch
from
August 28, 2026 12:17
0d8b4fb to
53eaacf
Compare
bbartman
force-pushed
the
bmb/3376-stack-2-masked-user-walk
branch
from
August 28, 2026 16:59
53eaacf to
3fbb0b7
Compare
bbartman
force-pushed
the
bmb/3376-stack-2-masked-user-walk
branch
from
August 31, 2026 12:45
3fbb0b7 to
fa5ed25
Compare
Task 2 of #3376. The user grant walk now takes an explicit `authz::CapabilityMask`, applied at every node emission: each emitted node is `edge_bits & delegatable & mask`. Emission-time application is what makes traversal itself subject to the mask -- a node stripped of Delegate/Assume is terminal, so a mask without Delegate confines the token to direct user grants, and Assume (which makes all of an edge's bits delegatable) cannot re-widen a child beyond the mask. Because each parent is itself masked at emission, the clamp composes identically across any number of hops. `RoleGrant::reachable_nodes` stays unmasked internally: role-to-role authorization answers questions where no user bearer participates. `get_user_capability` masks reachability but passes a reached node's legacy value through un-attenuated -- it's compatibility metadata, never an authorization decision, and may legitimately read broader than the token's effective bits. `reachable_prefixes` omits fully attenuated nodes only when the mask actually hides something (`!mask.is_all()`): a masked token doesn't get to learn the shape of grants it cannot exercise. The omission is deliberately not unconditional, because the walk can already emit empty-bit nodes via delegation intersections and output for a mask that hides nothing must not change. Every existing caller (19 non-test sites: 17 in control-plane-api including the data-plane user token exchanges, 2 in agent discovers) explicitly passes `UNMASKED`, so this commit changes no behavior; the signature change exists to force each future caller to choose a mask. Threading real masks from bearer tokens lands with the Authority extractor (task 3). New tests cover unmasked parity, empty-mask denial, direct-only behavior without Delegate, multi-hop clamping with Delegate, Assume containment, additive multi-path composition within the mask, and fully-attenuated prefix omission with its unmasked contrast.
Comment-only review pass over the stack-2 changes:
- Replace the nonexistent "public role reachability" example on the
role-to-role walk with the real caller (prefix-to-data-plane checks).
- Fix the multi-hop test comment: SpecEdit itself is masked away, not
just its siblings.
- Fix the omit-fully-attenuated test comment: the unmasked examples
have non-empty bits; what's pinned is that mask-hidden prefixes
still surface.
- CapabilityMask::apply doc: traversal terminates only when both
Delegate and Assume are stripped, per the Assume-containment test.
- Drop forward references to artifacts of later stack PRs ("the
mint", "upgrade token") that don't exist in the codebase yet.
Rename for intent, per review: - CapabilityMask::UNMASKED -> ALL_CAPABILITIES, pairing with the predicate it feeds. - CapabilityMask::is_all -> has_all_capabilities, so the name itself says it answers "does this mask attenuate nothing?" and can't be misread as an "is this bearer unmasked?" check (that's capability_mask.is_some(), a property of the claim). Call sites in control-plane-api and agent update mechanically. Fill two masked-walk coverage gaps: - test_masked_walk_parent_prefix_pickup: the upward traversal mode (parent-prefix role-grant subjects) is clamped and Delegate-gated like the downward one. - test_masked_walk_get_user_capability: a reached node's legacy value passes through un-attenuated (even under an empty mask, for direct grants) while the mask still gates which nodes are reached at all.
bbartman
force-pushed
the
bmb/3376-stack-2-masked-user-walk
branch
from
September 1, 2026 13:08
fa5ed25 to
982a843
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 2 of the implementation plan in #3376 (comment). Stacked on #3396 (task 1) — review only the top commit here.
The user grant walk takes an explicit
authz::CapabilityMask, applied at every node emission (edge_bits & delegatable & mask). Emission-time application makes traversal itself subject to the mask: a mask withoutDelegateconfines the token to direct user grants, andAssumecannot re-widen a child beyond the mask. Role-to-role authorization stays unbounded (no user bearer participates), andget_user_capabilitymasks reachability while passing legacy metadata through un-attenuated.Zero behavior change: all 19 non-test callers (17 control-plane-api including the data-plane user token exchanges, 2 agent discovers) explicitly pass
Unbounded; the signature change exists to force every future caller to choose. Real masks thread in with theAuthorityextractor (task 3, next PR).One deliberate deviation from the plan text, called out for review:
reachable_prefixesskips fully attenuated nodes only when the mask actually hides something (!mask.is_all()). The walk can already emit empty-bit nodes today via delegation intersections, so an unconditional skip would change unmaskedauthorized_prefixesoutput. A mask that hides nothing keeps today's output bit-for-bit; a narrower mask omits attenuated nodes so a masked token can't learn the shape of grants it can't exercise. (is_all()is a leak-prevention test only — "is this bearer masked" keys on the claim's presence, per the type's docs.)Tests: unmasked parity (
bounded(all)≡UNMASKEDby construction), empty-mask denial, direct-only withoutDelegate, multi-hop clamping withDelegate,Assumecontainment (nb:AdminbundlesDelegatebut notAssume), additive multi-path composition within the mask, and attenuated-prefix omission with itsUnboundedcontrast.