From 1ddc21b99d69112fae5f426cb073f44e5bf66920 Mon Sep 17 00:00:00 2001 From: Duo Developer Date: Sun, 14 Dec 2025 20:04:52 +0000 Subject: [PATCH 1/7] feat: Add granular PAT permissions for Repositories REST API Implement granular Personal Access Token permissions for all Repositories REST API endpoints to enable fine-grained access control. - Add 3 permission YAML files (read_repository, read_repository_health, update_repository) with proper naming and feature categorization - Apply authorization decorators to 10 API endpoints in repositories.rb - Add comprehensive test coverage using shared example pattern - Enable token-based authorization for repository operations This allows users to create PATs with specific repository permissions rather than broad access, improving security and access control. --- config/authz/permissions/repository/read.yml | 7 ++ .../authz/permissions/repository/update.yml | 7 ++ .../permissions/repository_health/read.yml | 7 ++ lib/api/repositories.rb | 11 ++- spec/requests/api/repositories_spec.rb | 89 +++++++++++++++++++ 5 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 config/authz/permissions/repository/read.yml create mode 100644 config/authz/permissions/repository/update.yml create mode 100644 config/authz/permissions/repository_health/read.yml diff --git a/config/authz/permissions/repository/read.yml b/config/authz/permissions/repository/read.yml new file mode 100644 index 00000000000000..4939ccb4367f12 --- /dev/null +++ b/config/authz/permissions/repository/read.yml @@ -0,0 +1,7 @@ +--- +name: read_repository +description: Grants the ability to read repositories +feature_category: source_code_management +available_for_tokens: true +boundaries: + - project diff --git a/config/authz/permissions/repository/update.yml b/config/authz/permissions/repository/update.yml new file mode 100644 index 00000000000000..66ba806f1248c9 --- /dev/null +++ b/config/authz/permissions/repository/update.yml @@ -0,0 +1,7 @@ +--- +name: update_repository +description: Grants the ability to update repositories +feature_category: source_code_management +available_for_tokens: true +boundaries: + - project diff --git a/config/authz/permissions/repository_health/read.yml b/config/authz/permissions/repository_health/read.yml new file mode 100644 index 00000000000000..c2a09147102b43 --- /dev/null +++ b/config/authz/permissions/repository_health/read.yml @@ -0,0 +1,7 @@ +--- +name: read_repository_health +description: Grants the ability to read repository health information +feature_category: source_code_management +available_for_tokens: true +boundaries: + - project diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb index 07bd3348fa6670..411dcb1b98883e 100644 --- a/lib/api/repositories.rb +++ b/lib/api/repositories.rb @@ -113,6 +113,7 @@ def compare_cache_key(current_user, user_project, target_project, params) end end + route_setting :authorization, permissions: :read_repository, boundary_type: :project desc 'Get a project repository tree' do success Entities::TreeObject end @@ -151,6 +152,7 @@ def compare_cache_key(current_user, user_project, target_project, params) not_found!(e.message) end + route_setting :authorization, permissions: :read_repository, boundary_type: :project desc 'Get raw blob contents from the repository' params do requires :sha, type: String, @@ -164,6 +166,7 @@ def compare_cache_key(current_user, user_project, target_project, params) send_git_blob @repo, @blob end + route_setting :authorization, permissions: :read_repository, boundary_type: :project desc 'Get a blob from the repository' params do requires :sha, type: String, @@ -188,6 +191,7 @@ def compare_cache_key(current_user, user_project, target_project, params) } end + route_setting :authorization, permissions: :read_repository, boundary_type: :project desc 'Get an archive of the repository' params do optional :sha, type: String, @@ -215,6 +219,7 @@ def compare_cache_key(current_user, user_project, target_project, params) not_found!('File') end + route_setting :authorization, permissions: :read_repository, boundary_type: :project desc 'Compare two branches, tags, or commits' do success Entities::Compare end @@ -253,6 +258,7 @@ def compare_cache_key(current_user, user_project, target_project, params) end end + route_setting :authorization, permissions: :read_repository_health, boundary_type: :project desc 'Get repository health' do success Entities::RepositoryHealth end @@ -278,6 +284,7 @@ def compare_cache_key(current_user, user_project, target_project, params) present health, with: Entities::RepositoryHealth end + route_setting :authorization, permissions: :read_repository, boundary_type: :project desc 'Get repository contributors' do success Entities::Contributor end @@ -296,6 +303,7 @@ def compare_cache_key(current_user, user_project, target_project, params) not_found! end + route_setting :authorization, permissions: :read_repository, boundary_type: :project desc 'Get the common ancestor between commits' do success Entities::Commit end @@ -335,7 +343,7 @@ def compare_cache_key(current_user, user_project, target_project, params) use :release_params end route_setting :authentication, job_token_allowed: true - route_setting :authorization, job_token_policies: :read_releases, + route_setting :authorization, permissions: :read_repository, boundary_type: :project, job_token_policies: :read_releases, allow_public_access_for_enabled_project_features: :repository get ':id/repository/changelog' do check_rate_limit!(:project_repositories_changelog, scope: [current_user, user_project]) do @@ -354,6 +362,7 @@ def compare_cache_key(current_user, user_project, target_project, params) render_api_error!("Failed to generate the changelog: #{ex.message}", 422) end + route_setting :authorization, permissions: :update_repository, boundary_type: :project desc 'Generates a changelog section for a release and commits it in a changelog file' do detail 'This feature was introduced in GitLab 13.9' success code: 200 diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb index 29fcca11377d18..d65d3b21dcc403 100644 --- a/spec/requests/api/repositories_spec.rb +++ b/spec/requests/api/repositories_spec.rb @@ -166,6 +166,14 @@ expect(json_response).to be_an(Array) end end + + it_behaves_like 'authorizing granular token permissions', :read_repository do + let(:boundary_object) { project } + let(:user) { user } + let(:request) do + get api(route, personal_access_token: pat) + end + end end describe "GET /projects/:id/repository/blobs/:sha" do @@ -239,6 +247,14 @@ let(:request) { get api(route, guest) } end end + + it_behaves_like 'authorizing granular token permissions', :read_repository do + let(:boundary_object) { project } + let(:user) { user } + let(:request) do + get api(route, personal_access_token: pat) + end + end end describe "GET /projects/:id/repository/blobs/:sha/raw" do @@ -312,6 +328,14 @@ let(:request) { get api(route, guest) } end end + + it_behaves_like 'authorizing granular token permissions', :read_repository do + let(:boundary_object) { project } + let(:user) { user } + let(:request) do + get api(route, personal_access_token: pat) + end + end end describe "GET /projects/:id/repository/archive(.:format)?:sha" do @@ -501,6 +525,14 @@ def expected_archive_request(repository, metadata, path, include_lfs_blobs, excl let(:request) { get api(route, guest) } end end + + it_behaves_like 'authorizing granular token permissions', :read_repository do + let(:boundary_object) { project } + let(:user) { user } + let(:request) do + get api(route, personal_access_token: pat) + end + end end describe 'GET /projects/:id/repository/compare' do @@ -725,6 +757,14 @@ def commit_messages(response) let(:request) { get api(route, guest) } end end + + it_behaves_like 'authorizing granular token permissions', :read_repository do + let(:boundary_object) { project } + let(:user) { user } + let(:request) do + get api(route, personal_access_token: pat), params: { from: 'master', to: 'feature' } + end + end end describe 'GET /projects/:id/repository/contributors' do @@ -839,6 +879,14 @@ def commit_messages(response) expect(first_link_url).to include('sort=asc') end end + + it_behaves_like 'authorizing granular token permissions', :read_repository do + let(:boundary_object) { project } + let(:user) { user } + let(:request) do + get api(route, personal_access_token: pat) + end + end end describe 'GET :id/repository/health' do @@ -920,6 +968,23 @@ def commit_messages(response) let(:current_user) { guest } end end + + context 'when feature flag is disabled' do + before do + stub_feature_flags(project_repositories_health: false) + end + + it_behaves_like '404 response' do + let(:current_user) { user } + end + end + + it_behaves_like 'authorizing granular token permissions', :read_repository_health do + let(:boundary_object) { project } + let(:request) do + get api("/projects/#{project.id}/repository/health", personal_access_token: pat) + end + end end describe 'GET :id/repository/merge_base' do @@ -993,6 +1058,14 @@ def commit_messages(response) expect(json_response['message']).to eq('Provide at least 2 refs') end end + + it_behaves_like 'authorizing granular token permissions', :read_repository do + let(:boundary_object) { project } + let(:user) { user } + let(:request) do + get api("/projects/#{project.id}/repository/merge_base", personal_access_token: pat), params: { refs: refs } + end + end end describe 'GET /projects/:id/repository/changelog' do @@ -1200,6 +1273,14 @@ def commit_messages(response) let(:message) { 'Failed to generate the changelog: The commit start range is unspecified, and no previous tag could be found to use instead' } end end + + it_behaves_like 'authorizing granular token permissions', :read_repository do + let(:boundary_object) { project } + let(:user) { user } + let(:request) do + get api("/projects/#{project.id}/repository/changelog", personal_access_token: pat), params: { version: '1.0.0' } + end + end end describe 'POST /projects/:id/repository/changelog' do @@ -1400,5 +1481,13 @@ def commit_messages(response) expect(response).to have_gitlab_http_status(:too_many_requests) end + + it_behaves_like 'authorizing granular token permissions', :update_repository do + let(:boundary_object) { project } + let(:user) { user } + let(:request) do + post api("/projects/#{project.id}/repository/changelog", personal_access_token: pat), params: { version: '1.0.0' } + end + end end end -- GitLab From e2972c6687467b96bbff84ce81eb02e20fd02086 Mon Sep 17 00:00:00 2001 From: Matthew MacRae-Bovell Date: Tue, 16 Dec 2025 04:52:11 -0500 Subject: [PATCH 2/7] Remove redefining user --- spec/requests/api/repositories_spec.rb | 8 -------- 1 file changed, 8 deletions(-) diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb index d65d3b21dcc403..a2eee2c2aa35b5 100644 --- a/spec/requests/api/repositories_spec.rb +++ b/spec/requests/api/repositories_spec.rb @@ -169,7 +169,6 @@ it_behaves_like 'authorizing granular token permissions', :read_repository do let(:boundary_object) { project } - let(:user) { user } let(:request) do get api(route, personal_access_token: pat) end @@ -250,7 +249,6 @@ it_behaves_like 'authorizing granular token permissions', :read_repository do let(:boundary_object) { project } - let(:user) { user } let(:request) do get api(route, personal_access_token: pat) end @@ -331,7 +329,6 @@ it_behaves_like 'authorizing granular token permissions', :read_repository do let(:boundary_object) { project } - let(:user) { user } let(:request) do get api(route, personal_access_token: pat) end @@ -528,7 +525,6 @@ def expected_archive_request(repository, metadata, path, include_lfs_blobs, excl it_behaves_like 'authorizing granular token permissions', :read_repository do let(:boundary_object) { project } - let(:user) { user } let(:request) do get api(route, personal_access_token: pat) end @@ -760,7 +756,6 @@ def commit_messages(response) it_behaves_like 'authorizing granular token permissions', :read_repository do let(:boundary_object) { project } - let(:user) { user } let(:request) do get api(route, personal_access_token: pat), params: { from: 'master', to: 'feature' } end @@ -882,7 +877,6 @@ def commit_messages(response) it_behaves_like 'authorizing granular token permissions', :read_repository do let(:boundary_object) { project } - let(:user) { user } let(:request) do get api(route, personal_access_token: pat) end @@ -1061,7 +1055,6 @@ def commit_messages(response) it_behaves_like 'authorizing granular token permissions', :read_repository do let(:boundary_object) { project } - let(:user) { user } let(:request) do get api("/projects/#{project.id}/repository/merge_base", personal_access_token: pat), params: { refs: refs } end @@ -1276,7 +1269,6 @@ def commit_messages(response) it_behaves_like 'authorizing granular token permissions', :read_repository do let(:boundary_object) { project } - let(:user) { user } let(:request) do get api("/projects/#{project.id}/repository/changelog", personal_access_token: pat), params: { version: '1.0.0' } end -- GitLab From 848cd4ecd7ec86209bdd377de16129ce3609e164 Mon Sep 17 00:00:00 2001 From: Matthew MacRae-Bovell Date: Tue, 16 Dec 2025 04:58:29 -0500 Subject: [PATCH 3/7] Add permission groups --- .../repositories/repository/read.yml | 9 +++++++++ .../repositories/repository/update.yml | 8 ++++++++ config/authz/permissions/repository/read.yml | 1 - config/authz/permissions/repository/update.yml | 1 - config/authz/permissions/repository_health/read.yml | 1 - 5 files changed, 17 insertions(+), 3 deletions(-) create mode 100644 config/authz/permission_groups/assignable_permissions/repositories/repository/read.yml create mode 100644 config/authz/permission_groups/assignable_permissions/repositories/repository/update.yml diff --git a/config/authz/permission_groups/assignable_permissions/repositories/repository/read.yml b/config/authz/permission_groups/assignable_permissions/repositories/repository/read.yml new file mode 100644 index 00000000000000..e280171a72fa42 --- /dev/null +++ b/config/authz/permission_groups/assignable_permissions/repositories/repository/read.yml @@ -0,0 +1,9 @@ +--- +name: read_repository +description: Grants the ability to read repositories +feature_category: source_code_management +permissions: + - read_repository + - read_repository_health +boundaries: + - project diff --git a/config/authz/permission_groups/assignable_permissions/repositories/repository/update.yml b/config/authz/permission_groups/assignable_permissions/repositories/repository/update.yml new file mode 100644 index 00000000000000..251c8c60645a4b --- /dev/null +++ b/config/authz/permission_groups/assignable_permissions/repositories/repository/update.yml @@ -0,0 +1,8 @@ +--- +name: update_repository +description: Grants the ability to update repositories +feature_category: source_code_management +permissions: + - update_repository +boundaries: + - project diff --git a/config/authz/permissions/repository/read.yml b/config/authz/permissions/repository/read.yml index 4939ccb4367f12..2245630f102e17 100644 --- a/config/authz/permissions/repository/read.yml +++ b/config/authz/permissions/repository/read.yml @@ -2,6 +2,5 @@ name: read_repository description: Grants the ability to read repositories feature_category: source_code_management -available_for_tokens: true boundaries: - project diff --git a/config/authz/permissions/repository/update.yml b/config/authz/permissions/repository/update.yml index 66ba806f1248c9..c72ee163b2e355 100644 --- a/config/authz/permissions/repository/update.yml +++ b/config/authz/permissions/repository/update.yml @@ -2,6 +2,5 @@ name: update_repository description: Grants the ability to update repositories feature_category: source_code_management -available_for_tokens: true boundaries: - project diff --git a/config/authz/permissions/repository_health/read.yml b/config/authz/permissions/repository_health/read.yml index c2a09147102b43..be50f0f9e4a29a 100644 --- a/config/authz/permissions/repository_health/read.yml +++ b/config/authz/permissions/repository_health/read.yml @@ -2,6 +2,5 @@ name: read_repository_health description: Grants the ability to read repository health information feature_category: source_code_management -available_for_tokens: true boundaries: - project -- GitLab From 5ec49cfe62bd230048a50184af9eecee6a829b50 Mon Sep 17 00:00:00 2001 From: Matthew MacRae-Bovell Date: Tue, 16 Dec 2025 05:36:12 -0500 Subject: [PATCH 4/7] Remove redefining user --- spec/requests/api/repositories_spec.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb index a2eee2c2aa35b5..ab49503987066d 100644 --- a/spec/requests/api/repositories_spec.rb +++ b/spec/requests/api/repositories_spec.rb @@ -1476,7 +1476,6 @@ def commit_messages(response) it_behaves_like 'authorizing granular token permissions', :update_repository do let(:boundary_object) { project } - let(:user) { user } let(:request) do post api("/projects/#{project.id}/repository/changelog", personal_access_token: pat), params: { version: '1.0.0' } end -- GitLab From 8c4d92c71e4b12591a69de0495d8a70c205ebd07 Mon Sep 17 00:00:00 2001 From: Matthew MacRae-Bovell Date: Tue, 16 Dec 2025 06:00:38 -0500 Subject: [PATCH 5/7] Add metadata yml file --- config/authz/permissions/repository/_metadata.yml | 1 + 1 file changed, 1 insertion(+) create mode 100644 config/authz/permissions/repository/_metadata.yml diff --git a/config/authz/permissions/repository/_metadata.yml b/config/authz/permissions/repository/_metadata.yml new file mode 100644 index 00000000000000..8fe5b8de7a2906 --- /dev/null +++ b/config/authz/permissions/repository/_metadata.yml @@ -0,0 +1 @@ +feature_category: source_code_management -- GitLab From a585ad1b160099ebabe5ac7c0eb32c8e4fa12575 Mon Sep 17 00:00:00 2001 From: Matthew MacRae-Bovell Date: Tue, 16 Dec 2025 06:01:57 -0500 Subject: [PATCH 6/7] Add another metadata yml file --- config/authz/permissions/repository_health/_metadata.yml | 1 + 1 file changed, 1 insertion(+) create mode 100644 config/authz/permissions/repository_health/_metadata.yml diff --git a/config/authz/permissions/repository_health/_metadata.yml b/config/authz/permissions/repository_health/_metadata.yml new file mode 100644 index 00000000000000..8fe5b8de7a2906 --- /dev/null +++ b/config/authz/permissions/repository_health/_metadata.yml @@ -0,0 +1 @@ +feature_category: source_code_management -- GitLab From 4b022b1db3483876f7951b55fbcd39b2a100f273 Mon Sep 17 00:00:00 2001 From: Matthew MacRae-Bovell Date: Tue, 16 Dec 2025 14:40:06 -0500 Subject: [PATCH 7/7] Update specs to actually work --- spec/requests/api/repositories_spec.rb | 44 ++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb index ab49503987066d..c2ed70d5c674c5 100644 --- a/spec/requests/api/repositories_spec.rb +++ b/spec/requests/api/repositories_spec.rb @@ -974,9 +974,10 @@ def commit_messages(response) end it_behaves_like 'authorizing granular token permissions', :read_repository_health do + let(:params) { { generate: true } } let(:boundary_object) { project } let(:request) do - get api("/projects/#{project.id}/repository/health", personal_access_token: pat) + get api("/projects/#{project.id}/repository/health", personal_access_token: pat), params: params end end end @@ -1269,8 +1270,24 @@ def commit_messages(response) it_behaves_like 'authorizing granular token permissions', :read_repository do let(:boundary_object) { project } + + before do + spy = instance_spy(::Repositories::ChangelogService, execute: 'Release notes') + + allow(::Repositories::ChangelogService) + .to receive(:new) + .and_return(spy) + end + let(:request) do - get api("/projects/#{project.id}/repository/changelog", personal_access_token: pat), params: { version: '1.0.0' } + get api("/projects/#{project.id}/repository/changelog", personal_access_token: pat), + params: { + version: '1.0.0', + from: 'foo', + to: 'bar', + date: '2020-01-01', + trailer: 'Foo' + } end end end @@ -1476,8 +1493,29 @@ def commit_messages(response) it_behaves_like 'authorizing granular token permissions', :update_repository do let(:boundary_object) { project } + + before do + spy = instance_spy(::Repositories::ChangelogService) + + allow(::Repositories::ChangelogService) + .to receive(:new) + .and_return(spy) + + allow(spy).to receive(:execute).with(commit_to_changelog: true) + end + let(:request) do - post api("/projects/#{project.id}/repository/changelog", personal_access_token: pat), params: { version: '1.0.0' } + post api("/projects/#{project.id}/repository/changelog", personal_access_token: pat), + params: { + version: '1.0.0', + from: 'foo', + to: 'bar', + date: '2020-01-01', + branch: 'kittens', + trailer: 'Foo', + file: 'FOO.md', + message: 'Commit message' + } end end end -- GitLab