Skip to content

tables: capability-mask-aware user grant walk - #3486

Closed
bbartman wants to merge 1 commit into
bmb/3376-adding-capability-mask-typesfrom
bmb/3376-masked-user-walk
Closed

bbartman wants to merge 1 commit into
bmb/3376-adding-capability-mask-typesfrom
bmb/3376-masked-user-walk

Conversation

@bbartman

Copy link
Copy Markdown
Contributor

Part of #3376. Stacked on #3484 — review only the top commit here.

What

The tables::UserGrant grant walk takes an explicit authz::CapabilityMask on reachable_nodes, is_authorized, reachable_prefixes, and get_user_capability. The mask is applied at every node emission inside next_neighbors (edge & delegatable & mask), so traversal itself is subject to the mask: a node the mask leaves with neither Delegate nor Assume is terminal, a mask holding neither bit confines the token to direct user grants, and Assume cannot re-widen a child beyond the mask.

  • reachable_prefixes omits prefixes the mask attenuates to an empty bit set. The skip is gated on has_all_capabilities() (a "can this mask hide anything" test, never an "is the bearer masked" test) so a mask which hides nothing keeps the unmasked walk's own empty-bit delegation nodes surfacing.
  • get_user_capability masks reachability but reports a reached node's legacy value un-attenuated: compatibility metadata, not an authorization decision.
  • The RoleGrant walk keeps its signatures and pins ALL_CAPABILITIES internally; no user bearer participates in role-to-role authorization.

Zero behavior change

Only the tables-crate signatures change. Every caller in control-plane-api and agent (24 call sites) passes models::authz::CapabilityMask::ALL_CAPABILITIES explicitly; nothing above the tables crate changes shape. Threading the bearer's real mask from Envelope::capability_mask is the next layer.

Tests

New test_masked_walk_* tests in crates/tables/src/behaviors.rs pin: unmasked parity (full reachable-set literal), empty-mask denial, direct-only traversal without Delegate, multi-hop clamping with Delegate, Assume containment, attenuated-prefix omission with its unmasked contrast, parent-prefix pickup, and get_user_capability legacy pass-through. Red was verified by stubbing out the emission-time mask.apply and the reachable_prefixes gate independently.

Full mise run ci:nextest-run: 1945 passed, 1 skipped.

The `UserGrant` grant walk takes an explicit `authz::CapabilityMask`:
`reachable_nodes`, `is_authorized`, `reachable_prefixes`, and
`get_user_capability`. The mask is applied at every node emission inside
`next_neighbors` (`edge & delegatable & mask`), which makes traversal itself
subject to the mask: a node the mask leaves with neither Delegate nor Assume
is terminal, so a mask holding neither bit confines the token to direct user
grants, and Assume cannot re-widen a child beyond the mask.

`reachable_prefixes` omits prefixes the mask attenuates to an empty bit set,
gated on `has_all_capabilities()` so a mask which hides nothing keeps the
unmasked walk's empty-bit delegation nodes surfacing as they do today.
`get_user_capability` masks reachability but reports a reached node's legacy
value un-attenuated: it is compatibility metadata, not an authorization
decision. The `RoleGrant` walk keeps its signatures and pins
`ALL_CAPABILITIES` internally, since no user bearer participates in
role-to-role authorization.

Zero behavior change: only the tables-crate signatures change, and every
caller (control-plane-api and agent) passes `ALL_CAPABILITIES` explicitly.
Threading the bearer's real mask from `Envelope::capability_mask` is the next
layer.

Tests pin unmasked parity, empty-mask denial, direct-only traversal without
Delegate, multi-hop clamping with Delegate, Assume containment,
attenuated-prefix omission with its unmasked contrast, parent-prefix pickup,
and `get_user_capability` legacy pass-through.
@bbartman
bbartman force-pushed the bmb/3376-masked-user-walk branch from 852e86f to 1386b09 Compare September 15, 2026 11:33
@bbartman

Copy link
Copy Markdown
Contributor Author

Closing along with the rest of this stack (#3482, #3484).

@bbartman bbartman closed this Sep 16, 2026
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