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