Add granular token directives to MCP-reached GraphQL types
What does this MR do and why?
With a fine-grained PAT, any GraphQL object type without an authorize_granular_token directive resolves to null, because Gitlab::Graphql::Authz::GranularScopeAuthorization denies when no directive is declared. GitLab's MCP server runs GraphQL queries on behalf of the token. Once the token is passed into the MCP GraphQL tools (#631630 (closed)), tools return no data.
For example, the root namespace(fullPath:) field returns Namespace, which has no directive. That breaks get_work_item_types, get_saved_view, list_work_items and get_saved_view_work_items.
I found the affected types by loading every MCP query against the schema and listing each object type it reaches, instead of relying on config/authz/graphql/authorization_todo.txt.
Changes:
-
Real directives (see the implementation guide):
Namespace:read_namespaceat the project (via the project namespace's project), group (the group itself) and user boundaries. Theread_namespaceassignable permission gains thegroupboundary. It previously allowed only project and user.WorkItemSavedViewType: a newread_work_item_saved_viewpermission at the project and group boundaries, via the saved view's project or group. It is its own assignable permission (Work item saved view: Read), next to the existing create, update, delete and subscribe permissions. This coversNamespace.savedViews, the saved view mutation payloads and the subscription, so a mutation token also needs this permission to getsavedViewback in the payload.read_saved_viewcan't be used because merge request dashboard saved views share it, and it is only assignable at the user boundary.
-
skip_reason: :parent_authorizeson child and value types that are only reachable through an authorized parent: the nineWorkItemWidgetDefinition*types,CustomField,CustomFieldSelectOptionand the fourWorkItem*FieldValuetypes. Their lines are removed fromauthorization_todo.txt. -
Field-level directives on fields that return skipped types directly from a container or the root query, so a token holding only
read_project,read_group,read_namespaceorread_organizationcannot read them. Each directive names the ability the returned type authorizes with:read_work_item_typeonProject.workItemTypes,Group.workItemTypes,Namespace.workItemTypesandOrganization.workItemTypes. TheProject,GroupandOrganizationdirectives came from !251840 (merged) withread_work_item.read_custom_fieldonNamespace.customFields,Group.customFields,Group.customFieldandQuery.customField. The last two take the boundary from the custom field's namespace, so a token scoped to one group cannot read another group's field by ID.read_work_itemonNamespace.statusesat the project and group boundaries. !251840 (merged) added the group boundary only.read_work_item_saved_viewonSubscription.workItemSavedViewUpdated, with the boundary from the saved view's project or group.
read_work_item_typeandread_custom_fieldare new raw permissions in the existing Work item: Read assignable permission, so tokens with that permission keep access. -
Specs: request specs for every directive and boundary, using the shared examples 'authorizing granular token permissions for GraphQL' and '... with a skipped child type'. A new shared example, 'denying a granular token that only authorizes the parent of a field', checks that a token with only the parent permission gets the parent but not the gated field. The existing shared example's negative case removes every scope, so it cannot show that.
Organization.workItemTypes uses the user boundary, because the instance boundary for Work item: Read is admin only. A token with user-level Work item: Read can list the organization's work item types. That is configuration metadata the user can already read, and there is no single group or project to scope to.
Behavior for legacy PATs is unchanged. Granular checks only run for fine-grained tokens (or namespaces that enforce them).
Overlap with other MRs
!251840 (merged) merged first. It added the WorkItemType and WorkItemStatus skips and the directives on Project.workItemTypes, Group.workItemTypes, Organization.workItemTypes, Namespace.statuses and Query.workItemAllowedStatuses. This MR switches the workItemTypes directives to read_work_item_type, adds the project boundary to Namespace.statuses, and updates its spec that asserted Namespace has no directive.
The note, diff, repository tree, wiki and AccessLevel changes were split out into !265425 (merged) to make review easier. The two MRs are independent.
Out of scope
The EE Duo types are covered by !261877 (merged), which has merged. The SecurityScanProfileAttach and vulnerabilitiesSeverityOverride mutations take arrays of IDs, which the boundary extractor can't resolve yet, so they will be handled separately. PendingProjectMember and PendingGroupMember are not reachable from MCP queries, because MemberInterface only resolves to ProjectMember and GroupMember.
References
- Part of #631631
- Parent: #630327
- Token passthrough: #631630 (closed)
- Related MRs: !251840 (merged) (merged), !261877 (merged) (merged) and !265425 (merged) (split from this MR)
- Directive docs: https://docs.gitlab.com/development/permissions/granular_access/graphql_implementation_guide/
Screenshots or screen recordings
Not applicable, this MR only changes the backend.
How to set up and validate locally
-
Create a fine-grained personal access token in the UI (User settings > Personal access tokens > Fine-grained) for a project, with the Namespace: Read and Work item: Read permissions.
-
Run this query against
/api/graphqlwith the token, for example with curl:query { namespace(fullPath: "flightjs/Flight") { id workItemTypes { nodes { name widgetDefinitions { type } } } } }On master the response has
"namespace": null. With this MR it returns the work item types. -
Remove the Work item permission from the token and run the query again.
namespaceis returned, butworkItemTypesis null.
MR acceptance checklist
Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.