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 | |
:ai_workflows |
(added by allow_ai_workflows_access) |
|
:api |
none (from lib/api/api.rb global) |
|
:read_api |
request.get? || request.head? (global) |
→ Gitlab::Auth::InsufficientScopeError → 403. 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:apiscope).✅ AllGETorbit endpoints work with:read_api(no redaction callback needed).✅ POSTwith empty/invalid query returns 400 with both scopes (never reaches GKG → no redaction).✅ POSTwith 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/querycall. The change cannot expose redaction to a token type that wasn't already authorized for the upstream call. Authz::RedactionServiceonly reads policies;:read_apisemantics 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.rbexists and is comprehensive, but its defaultpersonal_access_tokenfactory creates an:api-scoped PAT, so the:read_apiregression wasn't covered.
This MR adds the missing :read_api PAT spec to close that hole.
How to set up and validate locally
-
Ensure GDK is running and GKG has indexed data (graph_status reports non-zero counts).
-
Mint two PATs on the same user: one with
:api, one with:read_apionly. -
Run a query that returns rows (e.g.
Pipelinefiltered bystatusis_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. Railslog/api_json.logshows the redaction callback returning 403 withmeta.auth_fail_reason: insufficient_scopeandmeta.auth_fail_requested_scopes: 'mcp_orbit ai_workflows api read_api'. - After this MR:
HTTP/1.1 200 OKwith the redacted query result. Both/api/v4/orbit/queryand/api/v4/internal/orbit/redactionreturn 200.
Repeat with the
:apiPAT to verify no regression — both before and after, returns 200. - Before this MR:
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.
- I have evaluated the MR acceptance checklist for this MR.
Refs https://gitlab.com/dgruzd/tasks/-/work_items/2550, follow-up to !233255 (merged) / gitlab-org/orbit/knowledge-graph#514 (closed).