Skip to content

Authz refactor w/ orthogonal capabilities#2936

Open
GregorShear wants to merge 3 commits into
masterfrom
greg/authz/1
Open

Authz refactor w/ orthogonal capabilities#2936
GregorShear wants to merge 3 commits into
masterfrom
greg/authz/1

Conversation

@GregorShear
Copy link
Copy Markdown
Contributor

@GregorShear GregorShear commented May 8, 2026

Summary

Adds an orthogonal capability model that coexists with the legacy read/write/admin hierarchy. Existing authorization paths (PostgREST, transitive-roles BFS) are untouched. New GraphQL authorization checks can opt into orthogonal capabilities, and we'll migrate existing GraphQL checks over one at a time. Once the GraphQL API covers everything PostgREST does, PostgREST and the legacy capability path can be retired together.

What changes

A new OrthogonalCapability enum and role_grants.capabilities / user_grants.capabilities columns let a grant carry an independent set of capabilities, rather than a single level in a hierarchy. This is finer-grained than the legacy roles: instead of admin implying everything, a grant lists exactly which capabilities it confers.

Special capabilities:

  • delegate — a grant carrying delegate can propagate its own capabilities to the next hop. The next hop's effective set is node.capabilities ∩ edge.capabilities — you can only pass on capabilities you actually hold. Without delegate, the capabilities apply at the object, but cannot chain further.
  • assume — a grant carrying assume is a trust root: the next hop inherits the full capability set declared on the edge, with no intersection against the parent's caps. Used when delegating complete authority (e.g. a user grant that says "this user fully impersonates tenantA/groups/editors/"), and as the BFS seed marker so user_grants get their declared capabilities through unfiltered.

In short: delegate carries your own permissions forward; assume carries the edge's permissions forward.

Coexistence with legacy

AnyCapability wraps either a single legacy Capability or a Vec<OrthogonalCapability>. RoleGrant::is_authorized and UserGrant::is_authorized dispatch on the variant: the legacy arm runs the existing transitive_roles BFS unchanged; the orthogonal arm runs a new reachable_nodes BFS that respects the delegate / assume rules above. Call sites pick which model they want.

Migration path

The two systems live side-by-side indefinitely. GraphQL authorization checks get migrated to orthogonal capabilities one at a time as we gain confidence. When the GraphQL API has full coverage of what PostgREST does today, PostgREST is retired and the legacy capability column / BFS can be dropped.

Test plan

  • supabase db reset applies cleanly
  • cargo sqlx prepare --workspace is up to date
  • cargo check -p control-plane-api and cargo test -p tables pass
  • New unit tests cover delegate propagation, assume trust-root semantics, terminal nodes, multi-path capability union, and RoleGrant reachability
  • Existing legacy authorization tests still pass

@GregorShear GregorShear requested a review from jshearer May 8, 2026 21:11
Comment thread crates/tables/src/behaviors.rs Dismissed
Comment thread crates/tables/src/behaviors.rs Dismissed
@GregorShear GregorShear removed the request for review from jshearer May 11, 2026 02:27
@jshearer jshearer added control-plane change:significant This is a significant change labels May 11, 2026
Comment on lines +103 to +109
if required.is_empty() {
debug_assert!(
false,
"is_authorized called with empty orthogonal capabilities"
);
return false;
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

asking is_authorized()? with empty capabilities returns false

@GregorShear GregorShear force-pushed the greg/authz/1 branch 7 times, most recently from 9c4d16d to 4ebba92 Compare May 11, 2026 19:55
Comment on lines +1015 to +1020
let (rg, ug, uid) = build_orthogonal_scenario(
vec![("acmeCo/", vec![Write, Assume])],
vec![("acmeCo/", "bobCo/shared/", vec![Read, Billing, TeamAdmin])],
);
assert_authorized(&rg, &ug, uid, "acmeCo/", vec![Write]);
assert_not_authorized(&rg, &ug, uid, "bobCo/shared/", vec![Write]);
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note potentially non-obvious behavior

@GregorShear GregorShear marked this pull request as ready for review May 11, 2026 19:57
  Adds an orthogonal capability system alongside the existing hierarchical
  (read/write/admin) authorization model. Both RoleGrant::is_authorized and
  UserGrant::is_authorized now accept `impl Into<AnyCapability>`, dispatching
  to either the legacy BFS (transitive_roles/GrantRef) or the new orthogonal
  BFS (reachable_nodes/NodeRef).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

change:significant This is a significant change control-plane

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants