Orbit API: POST /api/v4/orbit/query rejects read_api-scoped tokens despite being read-only
### Summary `POST /api/v4/orbit/query` rejects OAuth/PAT tokens scoped with `read_api`, returning `403 insufficient_scope`. The sibling `GET /api/v4/orbit/{status,schema,tools}` endpoints all accept `read_api` correctly. The endpoint is semantically a read operation — it executes graph traversals against the Knowledge Graph service with no data mutation. The HTTP verb is POST only because the query DSL is a JSON body that doesn't fit a querystring. ### Steps to reproduce 1. Create a Personal Access Token with only the `read_api` scope 2. Send a POST request to `/api/v4/orbit/query`: ```bash curl -sS -w "\nHTTP %{http_code}\n" \ -H "PRIVATE-TOKEN: <read_api-scoped PAT>" \ -H "Content-Type: application/json" \ -d '{"query": {"nodes": ["MergeRequest"]}}' \ https://gitlab.com/api/v4/orbit/query ``` ### What is the current *bug* behavior? The request returns `403 insufficient_scope`: ```json {"error":"insufficient_scope","error_description":"The request requires higher privileges than provided by the access token.","scope":"api read_api"} ``` Integrators are forced to mint full `api` (read + write) tokens to run graph queries — a needless privilege escalation. ### What is the expected *correct* behavior? `POST /api/v4/orbit/query` should accept `read_api`-scoped tokens, since the endpoint is a read-only operation (graph traversals, no state mutation). The request should pass authentication and proceed to the Knowledge Graph service. ### Relevant logs and/or screenshots The `"scope":"api read_api"` WWW-Authenticate challenge is the standard Grape/Doorkeeper response indicating that the token's scope doesn't match any of the accepted scopes for this endpoint. ### Output of checks This bug happens on GitLab.com ### Root cause The `API::Orbit::Data` Grape class ([`ee/lib/api/orbit/data.rb`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/api/orbit/data.rb)) declares no class-level `allow_access_with_scope` annotations. Scope enforcement falls through to the global defaults in [`lib/api/api.rb:53-54`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/api/api.rb#L53-54): ```ruby allow_access_with_scope :api allow_access_with_scope :read_api, if: ->(request) { request.get? || request.head? } ``` Those rules allow `read_api` for GET/HEAD only, so: | Endpoint | Method | `read_api` accepted? | |----------|--------|----------------------| | `/orbit/query` | POST | :x: (requires `api`) | | `/orbit/schema` | GET | :white_check_mark: | | `/orbit/status` | GET | :white_check_mark: | | `/orbit/tools` | GET | :white_check_mark: | Git history confirms this is an oversight: the file was introduced with no scope declarations, and no review discussion argued against `read_api` for this POST. ### Possible fixes Add `allow_access_with_scope :read_api` to `ee/lib/api/orbit/data.rb`. This mirrors the exact pattern used by two other POST-based read-only endpoints: - [`lib/api/glql.rb:14-15`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/api/glql.rb#L14-15) (GitLab Query Language) - [`lib/api/markdown.rb:7-8`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/api/markdown.rb#L7-8) (Markdown rendering) Both carry the comment: `# Although this API endpoint responds to POST requests, it is a read-only operation` ```diff diff --git a/ee/lib/api/orbit/data.rb b/ee/lib/api/orbit/data.rb --- a/ee/lib/api/orbit/data.rb +++ b/ee/lib/api/orbit/data.rb @@ -6,6 +6,9 @@ class Data < ::API::Base include ::API::Helpers::HeadersHelpers include APIGuard + # Although this API endpoint responds to POST requests, it is a read-only operation + allow_access_with_scope :read_api + feature_category :knowledge_graph urgency :low ``` `ee/spec/requests/api/orbit/data_spec.rb` currently has no OAuth-scope coverage for `POST /orbit/query`. Adding a parameterized block (modelled on `spec/requests/api/glql_spec.rb`) would lock the fix in. /cc @jgdoyon1
issue