Fine-grained PATs get null from the AccessLevel and ProjectStatistics GraphQL types, while REST returns the same data to the same token
Everyone can contribute. Help move this issue forward while earning points, leveling up and collecting rewards.
Summary
Two GraphQL types are unreachable for fine-grained personal access tokens regardless of which
permissions are granted, and both resolve to null with a 200 rather than an error. REST
returns the same underlying data to the same token, so this reads as an oversight rather than a
deliberate restriction.
There is no permission a user can select that fixes either one.
Steps to reproduce
- Create a fine-grained PAT with all memberships access and, among others,
Member: ReadandProject: Read. - Query, as that token:
{ user(username: "<your-username>") {
projectMemberships { nodes {
accessLevel { stringValue }
project { fullPath statistics { commitCount } }
} } } }- Repeat with a legacy PAT scoped to
read_api.
Current behaviour
Measured on gitlab.com (19.4.0-pre) against a user with 26 project memberships:
legacy read_api |
fine-grained | |
|---|---|---|
| nodes returned | 26 | 26 |
with non-null accessLevel |
26 | 0 |
with non-null statistics |
26 | 0 |
Same for project.projectMembers[].accessLevel. Adding Member: Read changes node visibility
(8 → 26, matching legacy) but every accessLevel stays null.
REST returns both facts to the same fine-grained token:
| Call | Result |
|---|---|
GET /projects/:id/members/all/:user_id |
200, access_level=30 |
GET /projects/:id?statistics=true |
200, commit_count=3711 |
Expected behaviour
Member: Read should make accessLevel readable, and reading project statistics should be
possible with some grantable permission. At minimum GraphQL and REST should agree for one token.
Cause
Types::BaseObject.authorized? → lib/gitlab/graphql/authz/granular_scope_authorization.rb
denies a type carrying no authorize_granular_token directive when the token is granular.
app/graphql/types/base_field.rb#L124 does the opposite for a field
(return true if granular_scope_authorization.empty?).
Member: Read grants the ProjectMember type; accessLevel returns AccessLevel, a separate
type, and app/graphql/types/access_level_type.rb has no directive. Same for
app/graphql/types/project_statistics_type.rb. Both are listed in
config/authz/graphql/authorization_todo.txt — type:AccessLevel (line 208) and
type:ProjectStatistics (line 797) — also present on v19.3.0-ee, so not a master regression.
Per the granular-access GraphQL guide, an unauthorized query returns null with a 200, which
is why this is silent.
Sharper for statistics: read_statistic is an assignable fine-grained permission with a
project boundary, and it appears in the REST catalogue — but the string statistic appears zero
times in the GraphQL catalogue. Note GET /projects/:id/statistics (which Statistic: Read
covers) returns fetch counts, not commit counts; the working route is
GET /projects/:id?statistics=true under Project: Read.
Possible fix
Same shape as #606143, #604498 (closed) and #603527 (closed):
add the directive, add the shared-example test, drop the authorization_todo.txt line, and
regenerate with bundle exec rake gitlab:permissions:graphql:compile_docs.
For AccessLevel specifically, note it is a leaf value type reached from ~8 parents
(ProjectMember, GroupMember, MergeAccessLevel, PushAccessLevel, …), so
skip_reason: :parent_authorizes may fit better than a permission of its own — the
Authorization group's call.
Related
- #593878 — the umbrella for exactly this gap ("no 'ghost APIs' that are exempt from AuthZ"). Names no specific types; this issue names two.
- #323733 (closed) — not a duplicate. Same-sounding title, filed 2021, closed in two
days, five years before fine-grained PATs; it concerned the role-based
read_statisticsabilit