Loading
Add granular token authorization to work item mutations
What does this MR do and why?
Adds granular (fine-grained) personal access token authorization to 34 GraphQL mutations in the work items area (work items, saved views, work item types/settings, custom fields, lifecycles, requirements, test cases, and iterations), as part of removing mutation: entries from config/authz/graphql/authorization_todo.txt.
For each mutation this MR:
- adds an
authorize_granular_tokendirective with the appropriate permission and boundary, - creates or reuses raw permission definitions and assignable permissions,
- adds the shared-example authorization request spec per declared boundary type,
- removes the mutation's line from the authorization todo file,
- regenerates the fine-grained token reference docs (GraphQL and REST).
Part of the granular PAT GraphQL coverage effort. Implementation guide: GraphQL implementation guide.
Mutation → permission → boundary
| Mutation | Permission | Boundary |
|---|---|---|
| WorkItemAddLinkedItems | update_work_item (reused) |
project + group (id → resource_parent) |
| WorkItemRemoveLinkedItems | update_work_item (reused) |
project + group (id → resource_parent) |
| workItemsHierarchyReorder | update_work_item (reused) |
project + group (id → resource_parent) |
| WorkItemBulkMove | update_work_item (reused) + create_work_item on the target (additional scope) |
project + group (source_full_path, target_full_path) |
| WorkItemBulkUpdate | update_work_item (reused) |
project + group (full_path) |
| WorkItemsCsvExport / WorkItemExport (alias) | read_work_item (reused) |
project (project_path) |
| WorkItemsCsvImport | create_work_item (reused) |
project (project_path) |
| WorkItemSubscribe | subscribe_work_item (new) |
project + group (id → resource_parent) |
| WorkItemUserPreferenceUpdate | update_work_item_user_preference (raw existed; folded into update_work_item) |
project + group (namespace_path) |
| WorkItemSavedViewCreate | create_work_item_saved_view (new raw; own assignable; aliases create_saved_view) |
project + group (namespace_path) |
| WorkItemSavedViewUpdate | update_work_item_saved_view (new raw; own assignable; aliases update_saved_view) |
project + group (id → resource_parent) |
| WorkItemSavedViewDelete | delete_work_item_saved_view (new raw; own assignable; aliases delete_saved_view) |
project + group (id → resource_parent) |
| WorkItemSavedViewReorder / Subscribe / Unsubscribe | reorder_work_item_saved_view / subscribe_work_item_saved_view / unsubscribe_work_item_saved_view (new raws; bundled in the subscribe_work_item_saved_view assignable; alias the generic saved view abilities) |
project + group (id → resource_parent) |
| WorkItemTypeCreate | create_work_item_type (raw existed; folded into create_work_item) |
group (full_path; omitted full_path falls back to the organization and is rejected in ready? for fine-grained tokens) |
| WorkItemTypeUpdate | update_work_item_type (raw existed; folded into update_work_item); passing archive also requires archive_work_item_type (new raw; standalone assignable) via an explicit Authz::Tokens::AuthorizeGranularScopesService check |
group (full_path; omitted full_path falls back to the organization and is rejected in ready? for fine-grained tokens) |
| WorkItemAvailabilityToggle | update_work_item_type_visibility (raw existed; folded into update_work_item) |
project + group (full_path) |
| WorkItemSettingsUpdate | update_work_item_setting (raw existed; folded into update_work_item) |
group (full_path; omitted full_path falls back to the organization and is rejected in ready? for fine-grained tokens) |
| CustomFieldCreate | create_custom_field (new raw; folded into create_work_item) |
group (group_path) |
| CustomFieldUpdate | update_custom_field (new raw; folded into update_work_item) |
group (id → namespace) |
| CustomFieldArchive / CustomFieldUnarchive | archive_custom_field / unarchive_custom_field (new raws; standalone archive_custom_field assignable) |
group (id → namespace) |
| LifecycleCreate | create_work_item_lifecycle (new raw; folded into create_work_item) |
group (namespace_path) |
| LifecycleUpdate / LifecycleAttachWorkItemType | update_work_item_lifecycle (new raw; folded into update_work_item) |
group (namespace_path) |
| LifecycleDelete | delete_work_item_lifecycle (new raw; folded into delete_work_item) |
group (namespace_path) |
| CreateRequirement | create_requirement (new raw; folded into create_work_item) |
project (project_path) |
| UpdateRequirement | update_requirement (new raw; folded into update_work_item) |
project (project_path) |
| ExportRequirements | read_requirement (new raw; folded into read_work_item) |
project (project_path) |
| CreateTestCase | create_test_case (new raw; folded into create_work_item) |
project (project_path) |
| iterationCreate / CreateIteration (alias) | create_iteration (new raw; folded into create_work_item) |
group only (group_path) |
Skipped
- WorkItemHierarchyAddChildrenItems stays in
authorization_todo.txt: its requiredchildren_idsargument usesloads:, and loading the children triggersWorkItemType's type-level granular directive. A token holding only the mutation's permission is denied while loading arguments, before the mutation-level check runs, so the mutation cannot currently be authorized (or spec-tested) with the granular framework. The merged precedent for this situation is removingloads:(seeIssueSetEpic), which is a behavioral change beyond this MR's scope.
Judgment calls
- Iterations are group-only:
Iterationbelongs to a group and the create service does not support projects, soiterationCreatedeclares only a group boundary even though it accepts aproject_pathargument (a code comment records this). - Consolidation into the work_item bundles: nested work item resources (work item types and settings, custom fields, lifecycles, requirements, test cases, and iterations) are folded into the
create/update/delete/read_work_itemassignables by verb, matching the pattern used for issue boards, epics, milestones, and iteration cadences. The exceptions are saved views (own assignables, see above), the archive verbs (own assignables), andsubscribe_work_item(standalone, matchingsubscribe_merge_request). WorkItemSavedViews::SavedView#resource_parentreturns the Project for a project namespace, the Group for a group, andnilotherwise.SavedViewPolicydelegates to it, so the RBAC subject and the granular boundary come from one method.update_work_item_type_visibilitydoes not get its own user-facing permission; it is folded into theupdate_work_itemassignable alongsideupdate_work_item_type.- Omitted
fullPathfalls back to the organization:WorkItemTypeCreate,WorkItemTypeUpdate, andWorkItemSettingsUpdateaccept an omittedfullPaththat falls back to the current organization. That fallback has no project or group boundary, so requests authenticated with a fine-grained token and nofullPathare now rejected inready?with an explicit error instead of an opaque denial.
How was this validated?
bundle exec rake gitlab:permissions:validatepasses (permissions, boundaries, test coverage, docs freshness).bundle exec rake gitlab:permissions:graphql:compile_docsandgitlab:permissions:routes:compile_docsregenerated docs committed.- Authorization request specs run in CI (shared example
authorizing granular token permissions for GraphQL, one per declared boundary type).
Edited by Alex Buijs