.ai/principles/distilled/permissions-graphql-gpat.md
Prerequisite: If you haven't already, also read .ai/principles/distilled/permissions-fundamentals.md - it contains foundational rules that apply to all permissions work.
bin/permission <permission_name> to generate raw permission definition files; pass -a (action) and -r (resource) flags to override the default name-splitting behaviour when the action is more than one word.config/authz/permissions/<resource>/<action>.yml; DO NOT add extra directories between the base path and the final filename.name and description field; the description must follow the pattern "Grants the ability to <action> <resource>"..metadata.yml with a valid feature_category entry from config/feature_categories.yml; look at existing API endpoints for that resource to find the correct category.bundle exec rake gitlab:permissions:validate (or rely on the Lefthook pre-push hook) to validate all permission definition files before pushing.IssueType → read_issue, Mutations::Issues::Create → create_issue).read_<resource> for object types, create_<resource> for create mutations, update_<resource> for update mutations, and delete_<resource> for delete mutations; use a specific permission name for special-action mutations (move, archive, transfer, etc.)..ai/principles/distilled/permissions-fundamentals.md § Permission Naming Conventions) enforced by the validation Rake task.config/authz/permission_groups/assignable_permissions/<category>/<resource>/<action>.yml; DO NOT place them at any other path.permissions array already exists as a raw permission definition file before referencing it.boundaries field to only the organizational levels (project, group, user, instance) where the bundled raw permissions actually apply; use the principle of least privilege and DO NOT include boundaries that the endpoints do not support.instance boundary sparingly — typically only for admin-facing permissions..metadata.yml only when titleization produces an incorrect display name (e.g., ci_cd → "CI/CD"); DO NOT create one when the folder name titleizes correctly..metadata.yml only when the resource name contains an acronym, brand name, or unconventional action that titleizes or pluralizes incorrectly, or when the generated description needs a custom noun.<actions> interpolation in any custom description field in a resource .metadata.yml so the action list stays in sync automatically.boundaries field on an assignable permission covers the union of all boundary_type values declared by its raw permissions' endpoints and directives; the Lefthook pre-push validation catches mismatches.rename_granular_scope_permission batched background migration, and mark the old permission as deprecated: true; (2) finalize the batched background migration in a later milestone; (3) remove the deprecated file using bundle exec rake gitlab:permissions:assignable:cleanup_deprecated.boundary_type between project and group (safe, because projects belong to groups); treat any change to or from user or instance as a breaking change requiring token holders to recreate their scopes.authorize_granular_token to every GraphQL object type and mutation that exposes protected resources; all fields accessed with granular PATs must have a directive or the authorization service returns "Unable to determine boundaries and permissions for authorization".boundary: :project (or :group, :user, :instance) on object types where the resolved object has a method to reach the boundary (e.g., issue.project); use boundary_argument: :project_path on mutations and root query fields where the boundary is passed as an argument.boundary: :itself when the type itself is the boundary object (e.g., ProjectType or GroupType).boundary: :user or boundary: :instance for standalone resources that do not belong to a specific project or group.boundary (without :id argument) to root query fields that lack an :id argument — the object is not yet resolved and an ArgumentError will be raised; use boundary_argument instead.permissions references only valid permission symbols from Authz::PermissionGroups::Assignable.all_permissions; the gitlab:permissions:validate Rake task enforces this.boundary_type matches at least one boundary declared in the corresponding assignable permission's boundaries field; the Lefthook pre-push validation catches mismatches.DirectiveFinder stops at the first match in that priority order.permissions: [] on a directive; the authorization service returns "Unable to determine permissions for authorization" when the permissions array is empty.traversal: true on entry-point fields (e.g., Query.group(fullPath:), Query.project(fullPath:)) that resolve a boundary from a path argument but do not expose data themselves; this causes the authorization service to verify only that the token is scoped to the boundary, not the listed permission. Note: traversal: true only applies to project and group boundary types.DirectiveFinder checks directives in this priority order and returns the first match: field-level → owner type → implementing type → return type (unwrapping List, NonNull, and Connection wrappers).BoundaryExtractor uses Strategy A (boundary_argument) for mutations and query fields with path arguments, Strategy B (boundary method on resolved object) for type fields, Strategy C (ID fallback via GlobalID) for query fields with an :id argument when the object is nil, and Strategy D (standalone NilBoundary) for user or instance boundaries.boundary method values are one of the valid accessor methods: project, group, or itself; any other value raises ArgumentError: "Invalid boundary method: '<value>'".authorize_granular_token, the owner type's directive is automatically skipped; the child type's directive enforces authorization when fields on the child object are resolved.RepositoryLanguageType, PushRulesType); for leaf types the collection-level check always fires and must not be bypassed.directives: granular_scope_directive(...) to any field where the automatic traversal skip should not apply; an explicit field-level directive always wins.context[:authz_cache] Set to avoid redundant authorization checks; the cache key is [permissions.sort, boundary.class, boundary.namespace.id], so multiple fields on the same type and boundary incur only one authorization service call.GranularTokenAuthorization extension with zero overhead; granular authorization only runs when token.granular? is true.createIssue.issue) and permission metadata fields (e.g., issue.userPermissions) are automatically skipped by SkipRules; DO NOT add directives to these fields.granular_personal_access_tokens feature flag is enabled for the token's user during development and testing; when the flag is disabled, granular PATs do not work for GraphQL requests.'authorizing granular token permissions for GraphQL' shared example for both query and mutation specs; provide user, boundary_object, and request let-bindings.boundary_object to match the boundary_type: project for :project, group for :group, :user for :user, :instance for :instance.user is a member of the boundary_object namespace (project or group) when the boundary type is :project or :group; authorization is denied otherwise.granular_personal_access_tokens feature flag is enforced.For the full picture, see: