From c6ec1663228c594412d7c20f58fa2eea44a6278c Mon Sep 17 00:00:00 2001 From: Duo Developer Date: Sun, 14 Dec 2025 20:19:12 +0000 Subject: [PATCH 1/4] feat: Add granular PAT permissions for Snippets REST API Implement granular Personal Access Token permissions for all 10 Snippets REST API endpoints to enable fine-grained access control. - Create 6 permission YAML files in config/authz/permissions/snippet/ - Add authorization decorators to all endpoints in lib/api/snippets.rb - Add comprehensive test coverage in spec/requests/api/snippets_spec.rb This enables users to create PATs with specific snippet permissions (read, create, update, delete, read_all, read_user_agent_detail) instead of broad access tokens. --- config/authz/permissions/snippet/create.yml | 6 ++ config/authz/permissions/snippet/delete.yml | 6 ++ config/authz/permissions/snippet/read.yml | 6 ++ config/authz/permissions/snippet/read_all.yml | 6 ++ .../snippet/read_user_agent_detail.yml | 6 ++ config/authz/permissions/snippet/update.yml | 6 ++ lib/api/snippets.rb | 10 +++ spec/requests/api/snippets_spec.rb | 72 +++++++++++++++++++ 8 files changed, 118 insertions(+) create mode 100644 config/authz/permissions/snippet/create.yml create mode 100644 config/authz/permissions/snippet/delete.yml create mode 100644 config/authz/permissions/snippet/read.yml create mode 100644 config/authz/permissions/snippet/read_all.yml create mode 100644 config/authz/permissions/snippet/read_user_agent_detail.yml create mode 100644 config/authz/permissions/snippet/update.yml diff --git a/config/authz/permissions/snippet/create.yml b/config/authz/permissions/snippet/create.yml new file mode 100644 index 00000000000000..1e86d0803de8d4 --- /dev/null +++ b/config/authz/permissions/snippet/create.yml @@ -0,0 +1,6 @@ +--- +name: create_snippet +description: Grants the ability to create snippets +feature_category: source_code_management +boundaries: + - instance diff --git a/config/authz/permissions/snippet/delete.yml b/config/authz/permissions/snippet/delete.yml new file mode 100644 index 00000000000000..a411a2a60c203d --- /dev/null +++ b/config/authz/permissions/snippet/delete.yml @@ -0,0 +1,6 @@ +--- +name: delete_snippet +description: Grants the ability to delete snippets +feature_category: source_code_management +boundaries: + - instance diff --git a/config/authz/permissions/snippet/read.yml b/config/authz/permissions/snippet/read.yml new file mode 100644 index 00000000000000..dd7955fb8f6a3f --- /dev/null +++ b/config/authz/permissions/snippet/read.yml @@ -0,0 +1,6 @@ +--- +name: read_snippet +description: Grants the ability to read snippets +feature_category: source_code_management +boundaries: + - instance diff --git a/config/authz/permissions/snippet/read_all.yml b/config/authz/permissions/snippet/read_all.yml new file mode 100644 index 00000000000000..6e01bc765e3467 --- /dev/null +++ b/config/authz/permissions/snippet/read_all.yml @@ -0,0 +1,6 @@ +--- +name: read_all_snippets +description: Grants the ability to read all snippets the user has access to +feature_category: source_code_management +boundaries: + - instance diff --git a/config/authz/permissions/snippet/read_user_agent_detail.yml b/config/authz/permissions/snippet/read_user_agent_detail.yml new file mode 100644 index 00000000000000..1f4cc5ed74aa71 --- /dev/null +++ b/config/authz/permissions/snippet/read_user_agent_detail.yml @@ -0,0 +1,6 @@ +--- +name: read_snippet_user_agent_detail +description: Grants the ability to read snippet user agent details +feature_category: source_code_management +boundaries: + - instance diff --git a/config/authz/permissions/snippet/update.yml b/config/authz/permissions/snippet/update.yml new file mode 100644 index 00000000000000..ceb6100cbe25ab --- /dev/null +++ b/config/authz/permissions/snippet/update.yml @@ -0,0 +1,6 @@ +--- +name: update_snippet +description: Grants the ability to update snippets +feature_category: source_code_management +boundaries: + - instance diff --git a/lib/api/snippets.rb b/lib/api/snippets.rb index 0dc7542f889bf3..8df45f04389c89 100644 --- a/lib/api/snippets.rb +++ b/lib/api/snippets.rb @@ -45,6 +45,7 @@ def find_snippet(id) use :pagination end + route_setting :authorization, permissions: :read_snippet, boundary_type: :instance get do authenticate! @@ -68,6 +69,7 @@ def find_snippet(id) use :pagination end + route_setting :authorization, permissions: :read_snippet, boundary_type: :instance get 'public' do authenticate! @@ -96,6 +98,7 @@ def find_snippet(id) use :pagination use :optional_list_params_ee end + route_setting :authorization, permissions: :read_all_snippets, boundary_type: :instance get 'all' do authenticate! @@ -115,6 +118,7 @@ def find_snippet(id) params do requires :id, type: Integer, desc: 'The ID of a snippet' end + route_setting :authorization, permissions: :read_snippet, boundary_type: :instance get ':id' do snippet = find_snippet(params[:id]) @@ -143,6 +147,7 @@ def find_snippet(id) use :create_file_params end + route_setting :authorization, permissions: :create_snippet, boundary_type: :instance post do authenticate! @@ -189,6 +194,7 @@ def find_snippet(id) use :update_file_params use :minimum_update_params end + route_setting :authorization, permissions: :update_snippet, boundary_type: :instance put ':id' do authenticate! @@ -231,6 +237,7 @@ def find_snippet(id) params do requires :id, type: Integer, desc: 'The ID of a snippet' end + route_setting :authorization, permissions: :delete_snippet, boundary_type: :instance delete ':id' do authenticate! @@ -260,6 +267,7 @@ def find_snippet(id) params do requires :id, type: Integer, desc: 'The ID of a snippet' end + route_setting :authorization, permissions: :read_snippet, boundary_type: :instance get ":id/raw" do snippet = find_snippet(params.delete(:id)) not_found!('Snippet') unless snippet @@ -276,6 +284,7 @@ def find_snippet(id) params do use :raw_file_params end + route_setting :authorization, permissions: :read_snippet, boundary_type: :instance get ":id/files/:ref/:file_path/raw", requirements: { file_path: API::NO_SLASH_URL_PART_REGEX } do snippet = find_snippet(params.delete(:id)) not_found!('Snippet') unless snippet&.repo_exists? @@ -293,6 +302,7 @@ def find_snippet(id) params do requires :id, type: Integer, desc: 'The ID of a snippet' end + route_setting :authorization, permissions: :read_snippet_user_agent_detail, boundary_type: :instance get ":id/user_agent_detail" do authenticated_as_admin! diff --git a/spec/requests/api/snippets_spec.rb b/spec/requests/api/snippets_spec.rb index ecd5310e361d16..174243612369aa 100644 --- a/spec/requests/api/snippets_spec.rb +++ b/spec/requests/api/snippets_spec.rb @@ -90,6 +90,13 @@ expect(snippet["id"]).not_to eq(public_snippet.id) end end + + it_behaves_like 'authorizing granular token permissions', :read_snippet do + let(:boundary_object) { nil } + let(:request) do + get api("/snippets", personal_access_token: pat) + end + end end describe 'GET /snippets/public' do @@ -136,6 +143,13 @@ public_snippet_in_time_range.id) end end + + it_behaves_like 'authorizing granular token permissions', :read_snippet do + let(:boundary_object) { nil } + let(:request) do + get api("/snippets/public", personal_access_token: pat) + end + end end describe 'GET /snippets/all' do @@ -179,6 +193,13 @@ end end end + + it_behaves_like 'authorizing granular token permissions', :read_all_snippets do + let(:boundary_object) { nil } + let(:request) do + get api("/snippets/all", personal_access_token: pat) + end + end end describe 'GET /snippets/:id/raw' do @@ -211,6 +232,13 @@ subject { get api("/snippets/#{snippet.id}/raw", snippet.author, personal_access_token: user_token) } end + + it_behaves_like 'authorizing granular token permissions', :read_snippet do + let(:boundary_object) { nil } + let(:request) do + get api("/snippets/#{private_snippet.id}/raw", personal_access_token: pat) + end + end end describe 'GET /snippets/:id/files/:ref/:file_path/raw' do @@ -223,6 +251,13 @@ it_behaves_like 'snippet access with different users' do let(:path) { "/snippets/#{snippet.id}/files/master/%2Egitattributes/raw" } end + + it_behaves_like 'authorizing granular token permissions', :read_snippet do + let(:boundary_object) { nil } + let(:request) do + get api("/snippets/#{private_snippet.id}/files/master/%2Egitattributes/raw", personal_access_token: pat) + end + end end describe 'GET /snippets/:id' do @@ -260,6 +295,13 @@ it_behaves_like 'snippet access with different users' do let(:path) { "/snippets/#{snippet.id}" } end + + it_behaves_like 'authorizing granular token permissions', :read_snippet do + let(:boundary_object) { nil } + let(:request) do + get api("/snippets/#{private_snippet.id}", personal_access_token: pat) + end + end end describe 'POST /snippets/', :with_current_organization do @@ -426,6 +468,13 @@ end end end + + it_behaves_like 'authorizing granular token permissions', :create_snippet do + let(:boundary_object) { nil } + let(:request) do + post api("/snippets/", personal_access_token: pat), params: { title: 'Test Title', description: 'test description', visibility: 'public', files: [{ file_path: 'file_1.rb', content: 'puts "hello world"' }] } + end + end end describe 'PUT /snippets/:id' do @@ -533,6 +582,14 @@ def update_snippet(snippet_id: snippet.id, params: {}, requester: user) put api("/snippets/#{snippet_id}", requester), params: params end + + it_behaves_like 'authorizing granular token permissions', :update_snippet do + let(:snippet) { create(:personal_snippet, :repository, author: user, visibility_level: Snippet::PUBLIC) } + let(:boundary_object) { nil } + let(:request) do + put api("/snippets/#{snippet.id}", personal_access_token: pat), params: { title: 'Updated Title' } + end + end end describe 'DELETE /snippets/:id' do @@ -578,6 +635,14 @@ def update_snippet(snippet_id: snippet.id, params: {}, requester: user) it_behaves_like '412 response' do let(:request) { api("/snippets/#{public_snippet.id}", personal_access_token: user_token) } end + + it_behaves_like 'authorizing granular token permissions', :delete_snippet do + let(:snippet_to_delete) { create(:personal_snippet, :repository, author: user) } + let(:boundary_object) { nil } + let(:request) do + delete api("/snippets/#{snippet_to_delete.id}", personal_access_token: pat) + end + end end describe "GET /snippets/:id/user_agent_detail" do @@ -595,5 +660,12 @@ def update_snippet(snippet_id: snippet.id, params: {}, requester: user) expect(json_response['ip_address']).to eq(user_agent_detail.ip_address) expect(json_response['akismet_submitted']).to eq(user_agent_detail.submitted) end + + it_behaves_like 'authorizing granular token permissions', :read_snippet_user_agent_detail do + let(:boundary_object) { nil } + let(:request) do + get api("/snippets/#{public_snippet.id}/user_agent_detail", personal_access_token: pat) + end + end end end -- GitLab From e4002e6c92e21f219afa22121feef09ba8812c90 Mon Sep 17 00:00:00 2001 From: Matthew MacRae-Bovell Date: Wed, 17 Dec 2025 21:16:45 -0500 Subject: [PATCH 2/4] Add groups --- .../project_features/snippet/create.yml | 8 ++++++++ .../project_features/snippet/delete.yml | 8 ++++++++ .../project_features/snippet/read.yml | 8 ++++++++ .../project_features/snippet/read_all.yml | 8 ++++++++ .../project_features/snippet/read_user_agent_detail.yml | 8 ++++++++ config/authz/permissions/snippet/_metadata.yml | 1 + config/authz/permissions/snippet/read_all.yml | 2 +- 7 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 config/authz/permission_groups/assignable_permissions/project_features/snippet/create.yml create mode 100644 config/authz/permission_groups/assignable_permissions/project_features/snippet/delete.yml create mode 100644 config/authz/permission_groups/assignable_permissions/project_features/snippet/read.yml create mode 100644 config/authz/permission_groups/assignable_permissions/project_features/snippet/read_all.yml create mode 100644 config/authz/permission_groups/assignable_permissions/project_features/snippet/read_user_agent_detail.yml create mode 100644 config/authz/permissions/snippet/_metadata.yml diff --git a/config/authz/permission_groups/assignable_permissions/project_features/snippet/create.yml b/config/authz/permission_groups/assignable_permissions/project_features/snippet/create.yml new file mode 100644 index 00000000000000..c81a1cd3373633 --- /dev/null +++ b/config/authz/permission_groups/assignable_permissions/project_features/snippet/create.yml @@ -0,0 +1,8 @@ +--- +name: create_snippet +description: Grants the ability to create snippets +feature_category: source_code_management +boundaries: + - instance +permissions: + - create_snippet diff --git a/config/authz/permission_groups/assignable_permissions/project_features/snippet/delete.yml b/config/authz/permission_groups/assignable_permissions/project_features/snippet/delete.yml new file mode 100644 index 00000000000000..0f86bd56eee55f --- /dev/null +++ b/config/authz/permission_groups/assignable_permissions/project_features/snippet/delete.yml @@ -0,0 +1,8 @@ +--- +name: delete_snippet +description: Grants the ability to delete snippets +feature_category: source_code_management +boundaries: + - instance +permissions: + - delete_snippet diff --git a/config/authz/permission_groups/assignable_permissions/project_features/snippet/read.yml b/config/authz/permission_groups/assignable_permissions/project_features/snippet/read.yml new file mode 100644 index 00000000000000..8ac17600adb217 --- /dev/null +++ b/config/authz/permission_groups/assignable_permissions/project_features/snippet/read.yml @@ -0,0 +1,8 @@ +--- +name: read_snippet +description: Grants the ability to read snippets +feature_category: source_code_management +boundaries: + - instance +permissions: + - read_snippet diff --git a/config/authz/permission_groups/assignable_permissions/project_features/snippet/read_all.yml b/config/authz/permission_groups/assignable_permissions/project_features/snippet/read_all.yml new file mode 100644 index 00000000000000..52b7c4bc5abc93 --- /dev/null +++ b/config/authz/permission_groups/assignable_permissions/project_features/snippet/read_all.yml @@ -0,0 +1,8 @@ +--- +name: read_all_snippet +description: Grants the ability to read all snippets +feature_category: source_code_management +boundaries: + - instance +permissions: + - read_all_snippet diff --git a/config/authz/permission_groups/assignable_permissions/project_features/snippet/read_user_agent_detail.yml b/config/authz/permission_groups/assignable_permissions/project_features/snippet/read_user_agent_detail.yml new file mode 100644 index 00000000000000..7fbea838efd27d --- /dev/null +++ b/config/authz/permission_groups/assignable_permissions/project_features/snippet/read_user_agent_detail.yml @@ -0,0 +1,8 @@ +--- +name: read_snippet_user_agent_detail +description: Grants the ability to read snippet user agent details +feature_category: source_code_management +boundaries: + - instance +permissions: + - read_snippet_user_agent_detail diff --git a/config/authz/permissions/snippet/_metadata.yml b/config/authz/permissions/snippet/_metadata.yml new file mode 100644 index 00000000000000..8fe5b8de7a2906 --- /dev/null +++ b/config/authz/permissions/snippet/_metadata.yml @@ -0,0 +1 @@ +feature_category: source_code_management diff --git a/config/authz/permissions/snippet/read_all.yml b/config/authz/permissions/snippet/read_all.yml index 6e01bc765e3467..764fe8fd394597 100644 --- a/config/authz/permissions/snippet/read_all.yml +++ b/config/authz/permissions/snippet/read_all.yml @@ -1,5 +1,5 @@ --- -name: read_all_snippets +name: read_all_snippet description: Grants the ability to read all snippets the user has access to feature_category: source_code_management boundaries: -- GitLab From 7d3b3cc0597079f03e4f50d5d81f489dcccde92e Mon Sep 17 00:00:00 2001 From: Matthew MacRae-Bovell Date: Wed, 17 Dec 2025 21:30:31 -0500 Subject: [PATCH 3/4] Fix permission linting --- .../read.yml} | 0 config/authz/permissions/definitions_todo.txt | 3 --- .../read.yml} | 0 3 files changed, 3 deletions(-) rename config/authz/permission_groups/assignable_permissions/project_features/{snippet/read_user_agent_detail.yml => snippet_user_agent_detail/read.yml} (100%) rename config/authz/permissions/{snippet/read_user_agent_detail.yml => snippet_user_agent_detail/read.yml} (100%) diff --git a/config/authz/permission_groups/assignable_permissions/project_features/snippet/read_user_agent_detail.yml b/config/authz/permission_groups/assignable_permissions/project_features/snippet_user_agent_detail/read.yml similarity index 100% rename from config/authz/permission_groups/assignable_permissions/project_features/snippet/read_user_agent_detail.yml rename to config/authz/permission_groups/assignable_permissions/project_features/snippet_user_agent_detail/read.yml diff --git a/config/authz/permissions/definitions_todo.txt b/config/authz/permissions/definitions_todo.txt index 1814b72928b550..cdf3723322e6d3 100644 --- a/config/authz/permissions/definitions_todo.txt +++ b/config/authz/permissions/definitions_todo.txt @@ -248,7 +248,6 @@ create_runners create_saved_replies create_sentry_issue create_service_account -create_snippet create_squash_option create_subgroup create_task @@ -668,7 +667,6 @@ read_security_settings read_self_hosted_models_settings read_sentry_issue read_shared_with_group -read_snippet read_software_license_policy read_squash_option read_statistics @@ -831,7 +829,6 @@ update_saved_replies update_secret_detection_validity_checks_status update_security_orchestration_policy_project update_sentry_issue -update_snippet update_squash_option update_subscription update_subscription_limit diff --git a/config/authz/permissions/snippet/read_user_agent_detail.yml b/config/authz/permissions/snippet_user_agent_detail/read.yml similarity index 100% rename from config/authz/permissions/snippet/read_user_agent_detail.yml rename to config/authz/permissions/snippet_user_agent_detail/read.yml -- GitLab From 178a4c574ad51f4b8482cf99ac6100cc4dff4d53 Mon Sep 17 00:00:00 2001 From: Matthew MacRae-Bovell Date: Wed, 17 Dec 2025 21:33:01 -0500 Subject: [PATCH 4/4] Add metadata yml --- config/authz/permissions/snippet_user_agent_detail/_metadata.yml | 1 + 1 file changed, 1 insertion(+) create mode 100644 config/authz/permissions/snippet_user_agent_detail/_metadata.yml diff --git a/config/authz/permissions/snippet_user_agent_detail/_metadata.yml b/config/authz/permissions/snippet_user_agent_detail/_metadata.yml new file mode 100644 index 00000000000000..8fe5b8de7a2906 --- /dev/null +++ b/config/authz/permissions/snippet_user_agent_detail/_metadata.yml @@ -0,0 +1 @@ +feature_category: source_code_management -- GitLab