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

  1. Create a fine-grained PAT with all memberships access and, among others, Member: Read and Project: Read.
  2. Query, as that token:
{ user(username: "<your-username>") {
    projectMemberships { nodes {
      accessLevel { stringValue }
      project { fullPath statistics { commitCount } }
} } } }
  1. 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.

  • #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_statistics abilit
Edited by 🤖 GitLab Bot 🤖