Support fine-grained access tokens in Orbit

Problem to Solve

Fine-grained personal access tokens have no clear, working story for GitLab Orbit.

  • Orbit's docs say fine-grained tokens are not supported. The code accepts them anyway.
  • Orbit has two token permissions: Knowledge Graph: Read for REST and MCP tool: Execute for MCP. A token with only the MCP permission can query Orbit without any read permission.
  • The names say "Knowledge Graph". The product is Orbit.
  • The upstream thread (gitlab-org/gitlab#572694 (closed)) raised a boundary question. A token scoped to Project A can still read Project B through Orbit. Authz called the permission an unbounded gateway. AppSec asked whether that defeats granularity.

How the answer evolved:

When Where Decision
2026-07 #572694 thread Orbit should respect the user's permissions all the way down. Option (b), enforce token boundaries, drafted as gitlab!247199 and later gitlab!253835.
2026-09-02 Orbit offsite Support fine-grained tokens. Mirror GitLab read access. No Orbit-specific section in the token UI. False negatives are fine, false positives are not.
2026-09-18 DSL migration sync Per-entity token settings are too costly to honor. "It needs to be global." Global Search is the prior art.
2026-09-20 Build on gitlab-work-vm-3 Group routes with JWT narrowing built and tested. Then simplified: global only, one permission, no alias, no migration.
2026-09-22 Review of the #572694 blast-radius note Group scope returns without group routes. The token's group selection trims the JWT traversal paths.
2026-09-24 Authz review on gitlab!256556 Narrowing dropped: the token check decides which endpoints a token can call, not what Orbit returns. Old names kept as deprecated so tokens can still rotate. Group access moves to /groups/:id/orbit/* routes.

Decision

We follow prior art: the Global Search pattern. One gateway check on the token, then the user's own permissions govern results.

Requirements:

  • One permission, read_orbit, shown as Orbit: Read under the User tab.
  • It guards every Orbit REST route and POST /orbit/mcp. A token without it gets 403 on both.
  • Results follow the token owner's access in GitLab through per-user redaction.
  • read_knowledge_graph and execute_orbit_mcp_tool stay defined as deprecated. No Orbit route checks them, so old tokens get 403 on Orbit but stay valid and rotatable.
  • Orbit does not read the token's other permissions. Orbit does not parse queries for namespaces.
  • No Orbit section or toggle in the token UI. Nothing new for users to configure.
  • Docs state the gateway model, the Reporter floor, the SAML caveat, the missing group and project scopes, and the old-name denial.

Tradeoffs we accept:

  • Tokens made with the old names lose Orbit access and must be recreated. There is no data migration; the deprecated definitions keep the tokens valid.
  • Group and project scopes are not offered. A token made only under Group and project access gets 403, same as Global Search's /search route.
  • No renames yet for the enabled-namespace and setting permissions. Stacked MR later.

Limitations today:

  • A token cannot limit Orbit to one group. It reads everything the owner can read.
  • Orbit ignores every other permission on the token. A token without Work item: Read still gets work items from Orbit when the owner can read them.

Acceptance:

  • A fine-grained token with Orbit: Read under User calls every /orbit/* REST route and /orbit/mcp.
  • A fine-grained token without Orbit: Read gets 403 on both.
  • A token made only under Group and project access gets 403.
  • A token with an old permission name gets 403 on Orbit and can still rotate.
  • The Orbit docs no longer say fine-grained tokens are not supported.

Merge requests:

Follow-up: group-limited access through /groups/:id/orbit/* routes, tracked in #1324.

Agent context: extended analysis, alternatives considered, related-code dumps

Prior art. use_global_search lists project, group, and user scopes. /search declares the user boundary; /groups/:id/search and /projects/:id/search declare group and project. After authorize_granular_token_scopes! in lib/api/helpers.rb, SearchService filters only with Ability.allowed?(current_user, ...). The token is never read again. The GitLab MCP server keeps execute_mcp_tool user-only and re-dispatches the REST route per tool call, so each tool inherits its route's boundary.

Why global only. Honoring every token setting means parsing queries for namespaces and mapping each entity to a token permission. The 2026-09-18 sync judged that unmaintainable. Granular scopes are stored as JSONB and read on every request, about 5,000 reads per second on gitlab.com. Per-entity checks add cost as well.

Group narrowing, built then dropped. Commits on gitlab!256556 derived the route boundary from the token and trimmed the JWT traversal paths to the selected groups. Authz review rejected it on 2026-09-24. Scopes gate endpoints and don't filter results. Silent filtering misleads agents. Global entities such as runners and users skip the traversal-path filter.

Group scoping prototype. Built and tested on gitlab-work-vm-3 against gitlab master f37b46faaa7c. Query, named query, agent commands, and graph status mounted under groups/:id/orbit with the group boundary. AuthorizationContext narrowed traversal paths to the group before signing the JWT. MCP invoke_command took namespace_id and ran the read check per call. 17 end-to-end checks and 328 specs passed. It needed a paired gitlab-org/cells/http-router snapshot MR. Archived on branch fgat-group-scoping-archive in the local gitlab worktree and on the VM.

Alternatives dropped.

  • Enforce token boundaries inside Orbit by propagating them into the JWT and the redaction path (gitlab!247199, gitlab!253835, !2409 (closed)). Closed 2026-09-19. Too much machinery for a beta feature.
  • Query-parameter container (?namespace_id=) on existing routes. Replaced by path routes, then deferred with the rest of group scoping.
  • Deprecated alias plus batched background migration for the rename. The migration is skipped. The deprecated definitions alone keep old tokens valid and rotatable.
  • Sorting deprecated definitions last in Authz::PermissionGroups::Assignable. Reverted; shared authz code stays untouched.

Sources. Zoom: Orbit Offsite GA Scope Security AX Orbit Next 2026-09-02, Orbit Offsite GA Planning 2026-09-02, Orbit Monetization Engineering 2026-07-02, Security Meeting 2026-04-20. Wispr Flow: Orbit Fine-Grained Token Support 2026-09-03, Orbit DSL Migration with Open Cipher 2026-09-18.

Edited by Michael Angelo Rivera