Draft: fix(auth): enforce token permissions before graph reads

What Orbit receives

The token's grant list stays in Rails. For a granular request, Orbit receives the following claim in its signed JWT.

{"token_authorization_required": true}

Rails then returns decisions for the requested permission and namespace IDs.

{
  "authorizations": [{
    "resource_type": "namespace",
    "ability": "read_project",
    "authorized": {"211": true, "212": false}
  }]
}

This example shows the Rails callback response. Workhorse forwards these decisions to Orbit over the gRPC stream. The raw token is not sent to Orbit.

What does this MR do and why?

Fine-grained tokens now restrict Orbit reads to the token's existing permissions and the user's access. Orbit filters inputs before queries run and checks returned resources through Rails.

Draft while we compare a Rails-owned scope response with the current candidate-check callback. The token authorization module remains complex. The compiler split and 65-line cleanup do not settle that design question.

Request flow

  1. Rails authenticates the token and sends Orbit a signed JWT with token_authorization_required: true. The JWT contains no raw token or grants.
  2. Orbit identifies the required permissions and asks Rails to check them through Workhorse.
  3. Workhorse forwards the original credentials to the internal Rails callback.
  4. Rails checks the token’s saved grants and returns allow/deny decisions for each permission and resource.
  5. Orbit uses those decisions to filter query inputs. Final resource checks also enforce the user’s existing abilities.

Testing

The cleanup passes 314 unit tests and five integration tests, including redaction, token permissions, and scan cost. Two independent reviews found no cleanup defect. The rebuilt server passes all 20 live Robot tests. Pipeline 2825064334 passes. Workspace Clippy passes with all features and targets.

Performance Analysis

Token checks add work. The pinned File-neighbors base scan reads 88,064 rows; the classic path reads 7,168 rows. Unbounded queries keep full membership checks. The cost test confirms that extra pinned-neighbor reads stay below 8,192 rows as unrelated data grows.

  • This merge request does not introduce any performance regression. Granular requests have a measured cost above classic requests.
Agent context

A local Rails scope-export probe passed three examples. Exporting all 17 namespace permissions required 187 policy calls, versus 11 for one permission, on ten projects and their group. A query-specific export needs further validation.

Each child namespace needs a Rails decision. Missing catalog rows fail closed. User checks use the global user boundary; Runner checks use the actual owner. Responses match both resource type and ability. Unclear legacy responses fail closed.

Project counts require read_project. Code coverage requires read_code. Group status omits stored progress that cannot be filtered by project. Broader classic user-policy gaps in issue 347 remain outside this fix.

Schema 94 keeps the same DDL and needs no backfill. The internal compiler flag does not change the query DSL.

Release the protocol first. Deploy the complete Orbit stack next, then Rails and Workhorse. The compiler prerequisite alone does not enable server token checks.

See the security design for the full contract.

Edited by Michael Angelo Rivera

Merge request reports

Loading
Loading