Add fine-grained token support for project labels API endpoints

What does this MR do and why?

Add fine-grained token support for project labels API endpoints following the implementation guide

  • Add authorization decorators
  • Create assignable permissions for labels
  • Add authorization tests for granular tokens
  • Supports GET, POST, PUT, DELETE operations

This change will allow me fix the feedback I have raised regarding fine-grained tokens not supporting /projects/:id/labels endpoints, which will help me unblock token migration tasks such as https://gitlab.com/gitlab-com/gl-security/product-security/product-security-engineering/product-security-engineering-team/-/work_items/401

Project Label Permissions (NEW)

  • Created project_label permissions to support granular token access without conflicting with other resources that use generic label permissions
  • Implemented 4 permission types: read_project_label, create_project_label, update_project_label, delete_project_label
  • Added corresponding assignable permissions for UI dropdown and token creation

Group Label Support

  • authorization decorators already exist in /api/group_labels.rb from before so this was partially supported already.
  • Created group_label assignable permissions to support granular token access for group label endpoints
  • Implemented 3 assignable permissions: read_group_label, write_group_label, delete_group_label

Work Item Cleanup

  • Removed unused create_label, update_label, and delete_label permissions from work_item assignable permissions
  • Work items only read and associate labels; actual label CRUD operations are exclusive to label API endpoints

API Authorization

  • Added authorization decorators with appropriate project_label permissions to all 9 project label endpoints (GET, POST, PUT, DELETE operations)
  • Added comprehensive authorization tests validating both legacy PATs and granular PATs work correctly

All 9 Label Endpoints Now Support Fine-Grained Tokens

READ Operations (read_project_label)

  • GET /projects/:id/labels - List project labels
  • GET /projects/:id/labels/:name - Get a single label by name

CREATE Operations (create_project_label)

  • POST /projects/:id/labels - Create a new label

UPDATE Operations (update_project_label)

  • PUT /projects/:id/labels/:name - Update an existing label
  • PUT /projects/:id/labels/:name/promote - Promote a label to group level
  • PUT /projects/:id/labels (deprecated) - Update label via deprecated endpoint

DELETE Operations (delete_project_label)

  • DELETE /projects/:id/labels/:name - Delete a label
  • DELETE /projects/:id/labels (deprecated) - Delete label via deprecated endpoint

References

Re: https://gitlab.com/gitlab-com/gl-security/product-security/product-security-engineering/product-security-engineering-team/-/work_items/382+

Screenshots or screen recordings

Before After
Screenshot_2026-03-31_at_7.44.10_PM Screenshot_2026-03-31_at_7.36.49_PM

How to set up and validate locally

  1. In rails console enable the feature flag

    Feature.enable(:granular_personal_access_tokens)
  2. Create a gPAT via UI or rails console and select READ, UPDATE, CREATE, DELETE for label. I created the token via UI:

Screenshot_2026-03-31_at_7.44.10_PM

  1. Run the following commands
# verify the token does not have access to perform any operation against other resources

curl -H "PRIVATE-TOKEN: <my local gPAT>"  https://gdk.test:3443/api/v4/projects/2

# I got
{"error":"insufficient_granular_scope","error_description":"Access denied: This operation requires a fine-grained personal access token with the following project permissions: [Project: Read]."}%                                                          
# verify the token can read project tokens

curl -H "PRIVATE-TOKEN: <gPAT>"  https://gdk.test:3443/api/v4/projects/2/labels

# I got
[{"id":79,"name":"Civic","description":null,"text_color":"#1F1E24","description_html":"","color":"#a6e753","archived":false,"subscribed":false,"priority":null,"is_project_label":true},{"id":77,"name":"Elantra","description":null,"text_color":"#FFFFFF","description_html":"","color":"#597253","archived":false,"subscribed":false,"priority":null,"is_project_label":true},{"id":75,"name":"Equinox","description":null,"text_color":"#FFFFFF","description_html":"","color":"#b400a8","archived":false,"subscribed":false,"priority":null,"is_project_label":true},
...]
# verify the token can UPDATE project label

curl -X PUT -H "PRIVATE-TOKEN: <gPAT>" \
  https://gdk.test:3443/api/v4/projects/2/labels \
  -d "name=Civic&new_name=Updated-Civic&color=%23FF0000"

# I got
{"id":79,"name":"Updated-Civic","description":null,"text_color":"#FFFFFF","description_html":"","color":"#FF0000","archived":false,"subscribed":false,"priority":null,"is_project_label":true}%                                                              
# verify the token can DELETE project label

curl -X DELETE -H "PRIVATE-TOKEN: <gPAT>" \
  https://gdk.test:3443/api/v4/projects/2/labels/110

# I was able to see the label disappear from the UI
# verify projects/:id/labels/:name is supported

curl -X GET -H "PRIVATE-TOKEN: <gpat>" \
  https://gdk.test:3443/api/v4/projects/2/labels/Equinox

# I got
{"id":75,"name":"Equinox","description":null,"text_color":"#FFFFFF","description_html":"","color":"#b400a8","archived":false,"subscribed":false,"priority":null,"is_project_label":true}% 
# verify the /projects/:id/labels/:name/prmote endpoint is supported

curl -X PUT -H "PRIVATE-TOKEN: <gPAT>" \
  https://gdk.test:3443/api/v4/projects/2/labels/Rio/promote

# I was able to see the label being promoted from project label to a group label. See screenshots below
  • before promote:

Screenshot_2026-03-30_at_6.39.29_PM

  • after promote:

Screenshot_2026-03-30_at_6.39.43_PM

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.

Edited by Jennifer Li

Merge request reports

Loading
Loading