Allow read_api scope on POST /internal/orbit/redaction

What does this MR do and why?

Follow-up to !233255 (merged). That MR allowed :read_api PATs to call POST /api/v4/orbit/query, but a second-order bug remained: valid queries that return rows still 502 with :read_api PATs. Empty-result queries succeed; only queries that produce data fail. This MR closes that gap.

Root cause

POST /api/v4/orbit/query is Workhorse-accelerated (Send-Data: orbit-query:…). When GKG returns rows mid-stream, it issues a RedactionExchange request and Workhorse calls back into Rails via POST /api/v4/internal/orbit/redaction (see workhorse/internal/orbit/redaction.go), forwarding the original caller's PAT verbatim:

// workhorse/internal/orbit/redaction.go
const redactionEndpointPath = "/api/v4/internal/orbit/redaction"
var authHeaders = []string{"Authorization", "Private-Token", "Cookie", "X-Csrf-Token"}

API::Internal::Orbit (ee/lib/api/internal/orbit.rb) only declared :mcp_orbit. The aggregated scope list for the redaction POST was therefore:

Scope Predicate Matches :read_api PAT on POST?
:mcp_orbit none (token doesn't have it)
:ai_workflows (added by allow_ai_workflows_access) (token doesn't have it)
:api none (from lib/api/api.rb global) (token doesn't have it)
:read_api request.get? || request.head? (global) POST → predicate false

Gitlab::Auth::InsufficientScopeError403. Workhorse converts the non-2xx redaction response to 502 Bad Gateway (workhorse/internal/orbit/sendquery.go:155–157).

This explains every observation in the bug report:

  • :api-scoped PAT works (matches the global :api scope).
  • All GET orbit endpoints work with :read_api (no redaction callback needed).
  • POST with empty/invalid query returns 400 with both scopes (never reaches GKG → no redaction).
  • POST with valid query that returns rows → 502 with :read_api, 200 with :api.

Fix

Add allow_access_with_scope :read_api to API::Internal::Orbit. This mirrors the precedent established by ee/lib/api/orbit/data.rb (post-!233255), lib/api/glql.rb, and lib/api/markdown.rb: the redaction endpoint is read-only with respect to GitLab state — it only queries Authz::RedactionService policies, never mutates anything — so allowing :read_api is semantically correct.

Risk assessment

  • The endpoint already requires verify_workhorse_api! (Workhorse-signed header), so external callers cannot reach it directly. Real callers are always Workhorse forwarding a token the user already used to authenticate the upstream /orbit/query call. The change cannot expose redaction to a token type that wasn't already authorized for the upstream call.
  • Authz::RedactionService only reads policies; :read_api semantics are preserved.
  • Granular-permission gating is unaffected (route already declares route_setting :authorization, skip_granular_token_authorization: :orbit_internal_auth).
  • Stateless code change, no schema/migration. Hot-deploy safe.

Why the existing tests didn't catch this

  • ee/spec/requests/api/orbit/data_spec.rb (the public-route spec) mocks the gRPC layer, so the redaction-callback path is invisible to it.
  • ee/spec/requests/api/internal/orbit_redaction_spec.rb exists and is comprehensive, but its default personal_access_token factory creates an :api-scoped PAT, so the :read_api regression wasn't covered.

This MR adds the missing :read_api PAT spec to close that hole.

How to set up and validate locally

  1. Ensure GDK is running and GKG has indexed data (graph_status reports non-zero counts).

  2. Mint two PATs on the same user: one with :api, one with :read_api only.

  3. Run a query that returns rows (e.g. Pipeline filtered by status is_not_null):

    PAYLOAD='{"query":{"query_type":"traversal","node":{"id":"p","entity":"Pipeline","filters":{"status":{"op":"is_not_null"}},"columns":["id","status"]},"limit":5}}'
    curl -i -X POST -H "PRIVATE-TOKEN: $READ_API_PAT" -H "Content-Type: application/json" \
      -d "$PAYLOAD" http://127.0.0.1:3000/api/v4/orbit/query
    • Before this MR: HTTP/1.1 502 Bad Gateway. Rails log/api_json.log shows the redaction callback returning 403 with meta.auth_fail_reason: insufficient_scope and meta.auth_fail_requested_scopes: 'mcp_orbit ai_workflows api read_api'.
    • After this MR: HTTP/1.1 200 OK with the redacted query result. Both /api/v4/orbit/query and /api/v4/internal/orbit/redaction return 200.

    Repeat with the :api PAT to verify no regression — both before and after, returns 200.

Screenshots or screen recordings

N/A (API-only change).

MR acceptance checklist

This checklist encourages us to confirm any changes have been analyzed to reduce risks in quality, performance, reliability, security, and maintainability.

Refs https://gitlab.com/dgruzd/tasks/-/work_items/2550, follow-up to !233255 (merged) / gitlab-org/orbit/knowledge-graph#514 (closed).

Edited by Dmitry Gruzd

Merge request reports

Loading