Draft: Enforce fine-grained token boundaries on Orbit graph queries

What does this MR do and why?

A fine-grained personal access token scoped to a group or project could read any Orbit data its owner can see: read_knowledge_graph resolves to the user boundary, and the GKG JWT is built from the user's memberships alone. This MR enforces the token's own boundary, resolving option (b) of gitlab-org/orbit/knowledge-graph#992 (closed).

Primary guarantee: the JWT's traversal paths are narrowed to the intersection of the user's Reporter+ paths and the token's authorized namespace prefixes, so out-of-boundary rows are never produced. This is fail-closed and also bounds aggregation counts. A bounded token drops the admin bypass, and graph_status rejects out-of-boundary targets.

Backstop: the internal redaction callback authenticates with the caller's own token (Workhorse already forwards auth headers), computes the same prefixes, and drops out-of-boundary group and project rows per object. No new wire format or header is involved.

Classic PATs, OAuth tokens, and session requests are unchanged and take zero additional queries (measured); a scoped token adds one namespace batch load per request.

Verification (GDK e2e, 2026-07-27)

Two groups A/B with distinct indexed symbols, non-admin owner of both:

Token Symbol in A Symbol in B graph_status B Project count (agg)
classic api visible visible 200 2
granular, user boundary visible visible 200 2
granular, group A boundary visible hidden (0 rows) 404 1
granular, project A boundary visible hidden (0 rows) 404 1
granular without read_knowledge_graph 403 403 403 403

Same narrowing verified on the agent-commands path. A request spec decodes the real emitted JWT for a group-scoped token and asserts it contains only the scoped prefix.

Design notes from adversarial review (5 reviewers: comments, reuse, minimal diff, performance, test matrix)
  • The gRPC metadata surfaces (schema, DSL, templates, status, tools) mint user-scoped JWTs without the token. This is safe: no namespace row data flows there (GKG rejects non-immediate agent commands outside the Rails interceptor), graph_status is boundary-checked in Rails before the call, and named-query listing binds only the user id.
  • The route gate is an any-of entry gate by design; it cannot deny a scoped token whose boundary comes from its own scope. Enforcement lives in the JWT narrowing and the redaction backstop.
  • boundary_type: :user stays on all routes alongside the boundary: lambda because the permissions route validator and generated docs read only the static key; the lambda wins at runtime.
  • No framework token-to-namespace-set enumerator exists (verified against master and merged authz MRs), so token_authorized_prefixes is genuinely new; it reuses Namespace#traversal_path, Namespaces::Traversal::TrieNode compaction, and one batched namespace load.
  • Prefix truncation past the 500-path cap logs and increments the existing threshold metric; truncation only narrows further (fail-closed).

References

Edited by Michael Angelo Rivera

Merge request reports

Loading
Loading