From 67584801cad684f03c0bdd5b1de22f486e697242 Mon Sep 17 00:00:00 2001 From: Duo Developer Date: Thu, 18 Dec 2025 03:01:00 +0000 Subject: [PATCH] feat: Add granular PAT permissions for Approval Rules REST API Implement fine-grained Personal Access Token permissions for all Approval Rules REST API endpoints at both project and group levels. This enables administrators to grant specific permissions (read, create, update, delete) for approval rules, improving security and access control by following the principle of least privilege. Changes include: - Created 5 permission definition files in config/authz/permissions/approval_rule/ - Added route_setting :authorization decorators to project and group API endpoints - Added comprehensive test coverage using shared examples for granular token permissions - Supports both project and group boundaries for all approval rule operations --- .../permissions/approval_rule/_metadata.yml | 1 + .../permissions/approval_rule/create.yml | 7 ++++ .../permissions/approval_rule/delete.yml | 7 ++++ .../authz/permissions/approval_rule/read.yml | 7 ++++ .../permissions/approval_rule/update.yml | 7 ++++ ee/lib/api/group_approval_rules.rb | 4 +- ee/lib/api/project_approval_rules.rb | 5 +++ .../requests/api/group_approval_rules_spec.rb | 24 +++++++++++ .../api/project_approval_rules_spec.rb | 40 +++++++++++++++++++ 9 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 config/authz/permissions/approval_rule/_metadata.yml create mode 100644 config/authz/permissions/approval_rule/create.yml create mode 100644 config/authz/permissions/approval_rule/delete.yml create mode 100644 config/authz/permissions/approval_rule/read.yml create mode 100644 config/authz/permissions/approval_rule/update.yml diff --git a/config/authz/permissions/approval_rule/_metadata.yml b/config/authz/permissions/approval_rule/_metadata.yml new file mode 100644 index 00000000000000..8fe5b8de7a2906 --- /dev/null +++ b/config/authz/permissions/approval_rule/_metadata.yml @@ -0,0 +1 @@ +feature_category: source_code_management diff --git a/config/authz/permissions/approval_rule/create.yml b/config/authz/permissions/approval_rule/create.yml new file mode 100644 index 00000000000000..12390b6d92b3c6 --- /dev/null +++ b/config/authz/permissions/approval_rule/create.yml @@ -0,0 +1,7 @@ +--- +name: create_approval_rule +description: Grants the ability to create approval rules +feature_category: source_code_management +boundaries: + - project + - group diff --git a/config/authz/permissions/approval_rule/delete.yml b/config/authz/permissions/approval_rule/delete.yml new file mode 100644 index 00000000000000..b00a82fa1585cc --- /dev/null +++ b/config/authz/permissions/approval_rule/delete.yml @@ -0,0 +1,7 @@ +--- +name: delete_approval_rule +description: Grants the ability to delete approval rules +feature_category: source_code_management +boundaries: + - project + - group diff --git a/config/authz/permissions/approval_rule/read.yml b/config/authz/permissions/approval_rule/read.yml new file mode 100644 index 00000000000000..38c14c840a8b8f --- /dev/null +++ b/config/authz/permissions/approval_rule/read.yml @@ -0,0 +1,7 @@ +--- +name: read_approval_rule +description: Grants the ability to read approval rules +feature_category: source_code_management +boundaries: + - project + - group diff --git a/config/authz/permissions/approval_rule/update.yml b/config/authz/permissions/approval_rule/update.yml new file mode 100644 index 00000000000000..a9fc1ecd882e8f --- /dev/null +++ b/config/authz/permissions/approval_rule/update.yml @@ -0,0 +1,7 @@ +--- +name: update_approval_rule +description: Grants the ability to update approval rules +feature_category: source_code_management +boundaries: + - project + - group diff --git a/ee/lib/api/group_approval_rules.rb b/ee/lib/api/group_approval_rules.rb index 46ec7f02d0ce49..af9d591bee2169 100644 --- a/ee/lib/api/group_approval_rules.rb +++ b/ee/lib/api/group_approval_rules.rb @@ -23,6 +23,7 @@ class GroupApprovalRules < ::API::Base params do use :pagination end + route_setting :authorization, permissions: :read_approval_rule, boundary_type: :group get do authorize_group_approval_rule! @@ -39,6 +40,7 @@ class GroupApprovalRules < ::API::Base requires :approvals_required, type: Integer, desc: 'The number of required approvals for this rule' use :group_approval_rule end + route_setting :authorization, permissions: :create_approval_rule, boundary_type: :group post do create_group_approval_rule(present_with: ::API::Entities::GroupApprovalRule) end @@ -48,7 +50,7 @@ class GroupApprovalRules < ::API::Base optional :approvals_required, type: Integer, desc: 'The number of required approvals for this rule' use :group_approval_rule end - + route_setting :authorization, permissions: :update_approval_rule, boundary_type: :group put ':approval_rule_id' do update_group_approval_rule(present_with: ::API::Entities::GroupApprovalRule) end diff --git a/ee/lib/api/project_approval_rules.rb b/ee/lib/api/project_approval_rules.rb index 9206c1b334eabd..50c8e5e8cd2dcb 100644 --- a/ee/lib/api/project_approval_rules.rb +++ b/ee/lib/api/project_approval_rules.rb @@ -23,6 +23,7 @@ class ProjectApprovalRules < ::API::Base params do use :pagination end + route_setting :authorization, permissions: :read_approval_rule, boundary_type: :project get do authorize_read_project_approval_rule! @@ -36,6 +37,7 @@ class ProjectApprovalRules < ::API::Base params do use :create_project_approval_rule end + route_setting :authorization, permissions: :create_approval_rule, boundary_type: :project post do create_project_approval_rule(present_with: ::API::Entities::ProjectApprovalRule) end @@ -45,6 +47,7 @@ class ProjectApprovalRules < ::API::Base success ::API::Entities::ProjectApprovalRule tags %w[project_approval_rules] end + route_setting :authorization, permissions: :read_approval_rule, boundary_type: :project get do authorize_read_project_approval_rule! @@ -60,6 +63,7 @@ class ProjectApprovalRules < ::API::Base params do use :update_project_approval_rule end + route_setting :authorization, permissions: :update_approval_rule, boundary_type: :project put do update_project_approval_rule(present_with: ::API::Entities::ProjectApprovalRule) end @@ -71,6 +75,7 @@ class ProjectApprovalRules < ::API::Base params do use :delete_project_approval_rule end + route_setting :authorization, permissions: :delete_approval_rule, boundary_type: :project delete do destroy_project_approval_rule end diff --git a/ee/spec/requests/api/group_approval_rules_spec.rb b/ee/spec/requests/api/group_approval_rules_spec.rb index 0ddd2f8407533a..bac64fbfef5112 100644 --- a/ee/spec/requests/api/group_approval_rules_spec.rb +++ b/ee/spec/requests/api/group_approval_rules_spec.rb @@ -67,6 +67,12 @@ expect(json_response.dig(0, 'id')).to eq(approval_rules.last.id) end end + + it_behaves_like 'authorizing granular token permissions', :read_approval_rule do + let(:boundary_object) { group } + let(:user) { current_user } + let(:request) { get api("/groups/#{group.id}/approval_rules", personal_access_token: pat) } + end end describe 'POST /groups/:id/approval_rules' do @@ -161,6 +167,15 @@ end end end + + it_behaves_like 'authorizing granular token permissions', :create_approval_rule do + let(:boundary_object) { group } + let(:user) { current_user } + let(:request) do + post api("/groups/#{group.id}/approval_rules", personal_access_token: pat), + params: { name: 'security', approvals_required: 10 } + end + end end describe 'PUT /groups/:id/approval_rules/:approval_rule_id' do @@ -243,5 +258,14 @@ end end end + + it_behaves_like 'authorizing granular token permissions', :update_approval_rule do + let(:boundary_object) { group } + let(:user) { current_user } + let(:request) do + put api("/groups/#{group.id}/approval_rules/#{approval_group_rule.id}", personal_access_token: pat), + params: { name: 'updated_name' } + end + end end end diff --git a/ee/spec/requests/api/project_approval_rules_spec.rb b/ee/spec/requests/api/project_approval_rules_spec.rb index 98cd632736741f..62744c25bdcfb1 100644 --- a/ee/spec/requests/api/project_approval_rules_spec.rb +++ b/ee/spec/requests/api/project_approval_rules_spec.rb @@ -56,6 +56,12 @@ expect(response).to have_gitlab_http_status(:forbidden) end end + + it_behaves_like 'authorizing granular token permissions', :read_approval_rule do + let(:boundary_object) { private_project } + let(:user) { user } + let(:request) { get api("/projects/#{private_project.id}/approval_rules/#{approval_rule.id}", personal_access_token: pat) } + end end describe 'GET /projects/:id/approval_rules' do @@ -188,6 +194,13 @@ expect(response).to have_gitlab_http_status(:forbidden) end end + + it_behaves_like 'authorizing granular token permissions', :read_approval_rule do + let(:boundary_object) { project } + let(:user) { developer } + let!(:setup) { project.add_developer(user) } + let(:request) { get api("/projects/#{project.id}/approval_rules", personal_access_token: pat) } + end end describe 'POST /projects/:id/approval_rules' do @@ -195,6 +208,16 @@ let(:url) { "/projects/#{project.id}/approval_rules" } it_behaves_like 'an API endpoint for creating project approval rule' + + it_behaves_like 'authorizing granular token permissions', :create_approval_rule do + let(:boundary_object) { project } + let(:user) { user } + let!(:setup) { project.add_maintainer(user) } + let(:request) do + post api("/projects/#{project.id}/approval_rules", personal_access_token: pat), + params: { name: 'security', approvals_required: 10 } + end + end end describe 'PUT /projects/:id/approval_rules/:approval_rule_id' do @@ -203,6 +226,16 @@ let(:url) { "/projects/#{project.id}/approval_rules/#{approval_rule.id}" } it_behaves_like 'an API endpoint for updating project approval rule' + + it_behaves_like 'authorizing granular token permissions', :update_approval_rule do + let(:boundary_object) { project } + let(:user) { user } + let!(:setup) { project.add_maintainer(user) } + let(:request) do + put api("/projects/#{project.id}/approval_rules/#{approval_rule.id}", personal_access_token: pat), + params: { name: 'updated_name' } + end + end end describe 'DELETE /projects/:id/approval_rules/:approval_rule_id' do @@ -210,5 +243,12 @@ let(:url) { "/projects/#{project.id}/approval_rules/#{approval_rule.id}" } it_behaves_like 'an API endpoint for deleting project approval rule' + + it_behaves_like 'authorizing granular token permissions', :delete_approval_rule do + let(:boundary_object) { project } + let(:user) { user } + let!(:setup) { project.add_maintainer(user) } + let(:request) { delete api("/projects/#{project.id}/approval_rules/#{approval_rule.id}", personal_access_token: pat) } + end end end -- GitLab