Tell generated GraphQL types apart by class in the permission check
What does this MR do and why?
rake gitlab:permissions:validate, which the permissions-verify job runs, requires every GraphQL object type to declare authorize_granular_token, with permissions or a skip reason, unless the type is listed in config/authz/graphql/authorization_todo.txt. To pass over the types GraphQL Ruby generates, graphql_object_type? in lib/tasks/gitlab/permissions/graphql/schema_directives.rb leaves out every object type whose name ends in Payload, Connection or Edge. A type written by hand can carry one of those names too. Four do, and three of them declare nothing and are not on the todo list, so the validation never asks about them:
| Type | Class | Declares | On the todo list |
|---|---|---|---|
DependencyPathEdge |
Types::Sbom::DependencyPathEdge < Types::BaseObject |
nothing | no |
NamespaceWorkItemChangesPayload |
Types::WorkItems::NamespaceWorkItemChangesPayloadType < Types::BaseObject |
nothing | no |
PushEventPayload |
Types::Users::PushEventPayloadType < Types::BaseObject |
nothing | no |
FindingDueDatesSetPayload |
Types::Vulnerabilities::FindingDueDates::FindingDueDatesSetPayloadType < Types::BaseObject |
skip_reason: :parent_authorizes |
no |
All four inherit from Types::BaseObject, whose authorized? runs the granular token check, and with no directive that check denies a fine-grained token, as it does for every type on the todo list except PageInfo, the only one that does not inherit from Types::BaseObject. The first three are in that state while appearing on neither side of the validation.
This MR tells the generated types apart by class instead of by name: a type that includes GraphQL Ruby's ConnectionBehaviors or EdgeBehaviors, or a payload type GraphQL Ruby generated for a mutation, which is the only kind of type whose mutation is set. None of these inherits from Types::BaseObject, so none runs the granular token check, and leaving them out stays correct. A connection or edge class written by hand on top of GraphQL Ruby's, such as DiffConnection, is left out for the same reason. On the schema at 6b73f177, the class rule leaves out the same 1587 generated types the name rule did (437 connections, 437 edges and 713 mutation payloads) and checks the four types above.
The three undeclared types go on the todo list in the same change, because the rule and the entries only pass together: without the entries this branch reports the three types, and with them the rule on master reads the entries as stale, rewrites the file and stops. I know the file, the GraphQL implementation guide and the review guidelines all ask for no new entries. Two of the three predate the requirement !238363 (merged) added: DependencyPathEdge came with !195455 (merged), and PushEventPayload with !236728 (merged), merged three days before it. NamespaceWorkItemChangesPayload does not: !244735 (merged) added it on 2026-07-28, and it passed the validation only because the name rule skipped it. I listed all three rather than declare them, because a directive or a skip reason changes what a fine-grained token gets from them at runtime, which is a denial today, and that is for the groups that own them to decide. DependencyPathEdge and PushEventPayload say in their RuboCop comments that the parent type authorizes, and NamespaceWorkItemChangesPayload, the payload type of the namespaceWorkItemChanges subscription, says the subscription's authorized? method does. If you prefer, I can declare them here instead, starting with NamespaceWorkItemChangesPayload. FindingDueDatesSetPayload already declares a skip reason, which the validation now checks too.
The change is to development tooling only, so it needs no feature flag and no changelog entry.
I found this while maintaining gitlab-mcp-server, an MCP server for GitLab. It records from a running GitLab which object types enforce the granular token check and which declare a permission, and compares the enforced types that declare nothing with authorization_todo.txt. These three were the ones the file did not list.
References
- !238363 (merged) added the requirement that every type declares a directive or sits on the todo list.
- #631631 notes that connections and edges are not
Types::BaseObjectand never run the check. - gitlab-mcp-server keeps its record of this finding in upstream-bugs.md.
Screenshots or screen recordings
bundle exec rake gitlab:permissions:validate, on this branch with the three entries removed from the todo list:
#######################################################################
#
# The following GraphQL mutations and/or types are missing granular token authorization.
# Add `authorize_granular_token` with permissions and boundary_type to the mutation or type.
# Learn more: https://docs.gitlab.com/development/permissions/granular_access/graphql_implementation_guide
#
# - [type] DependencyPathEdge (ee/app/graphql/types/sbom/dependency_path_edge.rb)
# - [type] PushEventPayload (app/graphql/types/users/push_event_payload_type.rb)
# - [type] NamespaceWorkItemChangesPayload (app/graphql/types/work_items/namespace_work_item_changes_payload_type.rb)
#
#######################################################################With the entries, as committed, it prints GraphQL permissions are valid.
Evidence, file and line on master at 6b73f177
lib/tasks/gitlab/permissions/graphql/schema_directives.rb:69-74is the name rule.each_type_with_directives(line 31) uses it, and so doescurrent_entry_sourcesinvalidate_task.rb(line 103), which builds the setvalidate!compares with the todo list (line 37).BaseValidateTask#sync_todo(base_validate_task.rb:34-41) rewrites the file and aborts when the list holds an entry the task does not check.app/graphql/types/base_object.rb:45-46:Types::BaseObject.authorized?returns false unlessgranular_scope_authorization.ok?.lib/gitlab/graphql/authz/granular_scope_authorization.rb:51-52: with no directive, a granular token gets the default error.- GraphQL Ruby 2.6.10:
Types::Relay::BaseConnectionincludesConnectionBehaviorsandTypes::Relay::BaseEdgeincludesEdgeBehaviors;Types::BaseEdgeinherits from the latter.Schema::Mutation.generate_payload_typesetsmutationon the payload class it builds (lib/graphql/schema/mutation.rb:84-88), whose superclass is the mutation'sobject_class,GraphQL::Schema::Objectunless the mutation sets one (lib/graphql/schema/resolver/has_payload_type.rb:56). A payload type a subscription names withpayload_typeis written by hand, andmutationis not set on it.Subscriptions::BaseSubscriptionsetsobject_class Types::BaseObject(app/graphql/subscriptions/base_subscription.rb:7), so a payload type GraphQL Ruby generated for a subscription would run the check, and the class rule checks it where the name rule skipped it. Every subscription registered today names itspayload_type, so the schema holds none. - The figures come from walking
GitlabSchema.typeswithrails runnerand classifying each object type by both rules; the script is not part of this MR. Of 2763 object types, the name rule leaves out 1591 and the class rule 1587, and the four above are the whole difference. No type the class rule leaves out is checked by the name rule, and none of the 1587 inherits fromTypes::BaseObject. DocsTaskand the assignable permissions check (assignable/validate_task.rb:8,collect_graphql_assignable_when_consumersat line 213) read types through the same module. The first builds its page only from directives that name a permission (docs_task.rb:68-70), and the second skips a directive with a skip reason or no boundary type (line 216). The four types name no permission, so neither changes: on this branchrake gitlab:permissions:validatestill reportsAssignable permission definitions are validandGraphQL field documentation is up-to-date.
What I left out
- Declaring the three types, for the reason above.
PageInfo. It is GraphQL Ruby's own class and does not inherit fromTypes::BaseObjecteither, so it never runs the check, and #631631 calls its todo entry noise. Neither the name rule nor the class rule leaves it out, so it stays on the todo list as it is today. Leaving out every type that does not inherit fromTypes::BaseObjectwould cover it as well, but that changes the rule further than this defect needs, and the existing specs build their types as plain objects rather than classes. On the current schema it is the only such type the class rule checks.- !251840 and !254455 also edit
authorization_todo.txt, on other lines.
How to set up and validate locally
- Run
bundle exec rake gitlab:permissions:validate. It printsGraphQL permissions are valid. - Remove the three
type:lines this MR adds toconfig/authz/graphql/authorization_todo.txtand run it again. It reportsDependencyPathEdge,PushEventPayloadandNamespaceWorkItemChangesPayloadas missing granular token authorization. - Run
bundle exec rspec spec/tasks/gitlab/permissions/graphql/validate_task_spec.rb. It passes on this branch. Withschema_directives.rbfrommaster, the three examples for hand-written types fail, and removing any one of the three branches of the new check fails the examples for generated types.
RuboCop and both commit message linters report nothing on the change.
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.