From 783d15c33c35b0d8d977433cb71a1dd1c40be2b5 Mon Sep 17 00:00:00 2001 From: Sam Joan Roque-Worcel Date: Tue, 11 Nov 2025 15:10:46 +1300 Subject: [PATCH 01/13] Add new endpoint for downloading attestations In this commit, I've added a new endpoint to download the bundles associated with the attestations. --- lib/api/supply_chain/attestations.rb | 29 +++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/lib/api/supply_chain/attestations.rb b/lib/api/supply_chain/attestations.rb index 5194f69b5d014b..06bf0dc19ff31f 100644 --- a/lib/api/supply_chain/attestations.rb +++ b/lib/api/supply_chain/attestations.rb @@ -20,11 +20,9 @@ class Attestations < ::API::Base end resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do desc 'Fetch the list of all attestations for a specific project and artifact hash' do - detail 'This feature was introduced in GitLab 18.5' # TODO: update when FF is removed + detail 'This feature was introduced in GitLab 18.7' # TODO: update when FF is removed failure [ - { code: 401, message: 'Unauthorized' }, - { code: 403, message: 'Forbidden' }, - { code: 404, message: 'Artifact not found' } + { code: 404, message: 'Artifact SHA-256 not found' } ] end params do @@ -33,13 +31,26 @@ class Attestations < ::API::Base end get ':id/attestations/:subject_digest', urgency: :low, format: false, requirements: { subject_digest: /[A-Fa-f0-9]{64}/ } do - subject_digest = params[:subject_digest] - project = find_project!(params[:id]) + subject_digest = params[:subject_digest] + project = find_project!(params[:id]) - attestations = ::SupplyChain::Attestation.for_project(project.id).with_digest(subject_digest) + attestations = ::SupplyChain::Attestation.for_project(project.id).with_digest(subject_digest) - present paginate(attestations), with: ::API::Entities::SupplyChain::Attestation - end + present paginate(attestations), with: ::API::Entities::SupplyChain::Attestation + end + + desc 'Fetch a specific bundle by iid' do + detail 'This feature was introduced in GitLab 18.7' # TODO: update when FF is removed + failure [ + { code: 404, message: 'Attestation iid not found' } + ] + end + params do + requires :attestation_iid, types: [String, Integer], desc: 'The iid of the attestation' + end + get ':id/attestations/:attestation_iid/download', urgency: :low, format: false do + present "hello, world" + end end end end -- GitLab From 47f1be7569fb84e13ee8f30ebfd9a5e5c92b2433 Mon Sep 17 00:00:00 2001 From: Sam Joan Roque-Worcel Date: Tue, 11 Nov 2025 15:15:40 +1300 Subject: [PATCH 02/13] Add openapi_v2.yml changes --- doc/api/openapi/openapi_v2.yaml | 33 +++++++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/doc/api/openapi/openapi_v2.yaml b/doc/api/openapi/openapi_v2.yaml index 809ba50946c2ea..fa6e1114a1dfc7 100644 --- a/doc/api/openapi/openapi_v2.yaml +++ b/doc/api/openapi/openapi_v2.yaml @@ -38326,7 +38326,7 @@ paths: get: summary: Fetch the list of all attestations for a specific project and artifact hash - description: This feature was introduced in GitLab 18.5 + description: This feature was introduced in GitLab 18.7 produces: - application/json parameters: @@ -38344,15 +38344,36 @@ paths: '200': description: Fetch the list of all attestations for a specific project and artifact hash - '401': - description: Unauthorized - '403': - description: Forbidden '404': - description: Artifact not found + description: Artifact SHA-256 not found tags: - projects operationId: getApiV4ProjectsIdAttestationsSubjectDigest + "/api/v4/projects/{id}/attestations/{attestation_iid}/download": + get: + summary: Fetch a specific bundle by iid + description: This feature was introduced in GitLab 18.7 + produces: + - application/json + parameters: + - in: path + name: id + description: The ID or URL-encoded path of the project + type: string + required: true + - in: path + name: attestation_iid + description: The iid of the attestation + type: string + required: true + responses: + '200': + description: Fetch a specific bundle by iid + '404': + description: Attestation iid not found + tags: + - projects + operationId: getApiV4ProjectsIdAttestationsAttestationIidDownload "/api/v4/projects/{id}/repository/tags": get: description: Get a project repository tags -- GitLab From ecc55fd7ddf1cdb19e7379c1aca3c85f007c8b44 Mon Sep 17 00:00:00 2001 From: Sam Joan Roque-Worcel Date: Wed, 12 Nov 2025 14:34:12 +1300 Subject: [PATCH 03/13] Add iid, download_url to attestation entity. --- lib/api/entities/supply_chain/attestation.rb | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/api/entities/supply_chain/attestation.rb b/lib/api/entities/supply_chain/attestation.rb index c415ac479a6c6e..a7cb00ee9b73db 100644 --- a/lib/api/entities/supply_chain/attestation.rb +++ b/lib/api/entities/supply_chain/attestation.rb @@ -4,8 +4,10 @@ module API module Entities module SupplyChain class Attestation < Grape::Entity - expose :id, documentation: { type: 'integer', example: 1 } + include ::API::Helpers::RelatedResourcesHelpers + expose :id, documentation: { type: 'integer', example: 1 } + expose :iid, documentation: { type: 'integer', example: 14 } expose :created_at, documentation: { type: 'dateTime', example: '2025-09-17T02:26:10.898Z' } expose :updated_at, documentation: { type: 'dateTime', example: '2025-09-17T02:26:10.898Z' } expose :expire_at, documentation: { type: 'dateTime', example: '2025-09-17T02:26:10.898Z' } @@ -17,6 +19,11 @@ class Attestation < Grape::Entity expose :predicate_type, documentation: { type: 'string', example: 'https://slsa.dev/provenance/v1' } expose :subject_digest, documentation: { type: 'string', example: '5db1fee4b5703808c48078a76768b155b421b210c0761cd6a5d223f4d99f1eaa' } + + expose :download_url do |attestation| + expose_url(api_v4_projects_attestations_download_path(id: attestation.project_id, attestation_iid: + attestation.iid)) + end end end end -- GitLab From ceda75c931f7a71689f63c0e7f0199d9615a7d92 Mon Sep 17 00:00:00 2001 From: Sam Joan Roque-Worcel Date: Thu, 13 Nov 2025 11:37:27 +1300 Subject: [PATCH 04/13] Implement download endpoint In this commit, I've implemented the attestation download endpoint. I've also improved the 'desc' block of this API to be more like what https://docs.gitlab.com/development/api_styleguide/#defining-endpoint-details describes. --- app/models/supply_chain/attestation.rb | 1 + lib/api/supply_chain/attestations.rb | 27 +++++++++++-------- .../attestations/6/20251113-67866-q7dg7k | 1 + 3 files changed, 18 insertions(+), 11 deletions(-) create mode 100644 public/19/58/19581e27de7ced00ff1ce50b2047e7a567c76b1cbaebabe5ef03f7c3017bb5b7/attestations/6/20251113-67866-q7dg7k diff --git a/app/models/supply_chain/attestation.rb b/app/models/supply_chain/attestation.rb index e5d8c0ed2100e6..f8189a13db759b 100644 --- a/app/models/supply_chain/attestation.rb +++ b/app/models/supply_chain/attestation.rb @@ -26,6 +26,7 @@ class Attestation < ::ApplicationRecord scope :for_project, ->(project_id) { where(project_id: project_id) } scope :with_digest, ->(subject_digest) { where(subject_digest: subject_digest) } scope :with_predicate_kind, ->(predicate_kind) { where(predicate_kind: predicate_kind) } + scope :with_iid, ->(iid) { where(iid: iid) } attribute :file_store, default: -> { AttestationUploader.default_store } diff --git a/lib/api/supply_chain/attestations.rb b/lib/api/supply_chain/attestations.rb index 06bf0dc19ff31f..f9cf65046be82c 100644 --- a/lib/api/supply_chain/attestations.rb +++ b/lib/api/supply_chain/attestations.rb @@ -9,9 +9,7 @@ class Attestations < ::API::Base urgency :low before do - project = find_project!(params[:id]) - - not_found! unless Feature.enabled?(:slsa_provenance_statement, project) + not_found! unless Feature.enabled?(:slsa_provenance_statement, user_project) authorize_read_attestations! end @@ -21,35 +19,42 @@ class Attestations < ::API::Base resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do desc 'Fetch the list of all attestations for a specific project and artifact hash' do detail 'This feature was introduced in GitLab 18.7' # TODO: update when FF is removed + success ::API::Entities::SupplyChain::Attestation failure [ { code: 404, message: 'Artifact SHA-256 not found' } ] + tags ['attestations'] end params do requires :subject_digest, type: String, desc: 'The SHA-256 hash of the artifact' end - get ':id/attestations/:subject_digest', - urgency: :low, format: false, requirements: { subject_digest: /[A-Fa-f0-9]{64}/ } do + get ':id/attestations/:subject_digest', urgency: :low, format: false, + requirements: { subject_digest: /[A-Fa-f0-9]{64}/ } do subject_digest = params[:subject_digest] - project = find_project!(params[:id]) - - attestations = ::SupplyChain::Attestation.for_project(project.id).with_digest(subject_digest) + attestations = ::SupplyChain::Attestation.for_project(user_project.id).with_digest(subject_digest) present paginate(attestations), with: ::API::Entities::SupplyChain::Attestation end desc 'Fetch a specific bundle by iid' do - detail 'This feature was introduced in GitLab 18.7' # TODO: update when FF is removed + success code: 200 failure [ - { code: 404, message: 'Attestation iid not found' } + { code: 404, message: 'Artifact SHA-256 not found' } ] + detail 'This feature was introduced in GitLab 18.7' # TODO: update when FF is removed + tags ['attestations'] end params do requires :attestation_iid, types: [String, Integer], desc: 'The iid of the attestation' end get ':id/attestations/:attestation_iid/download', urgency: :low, format: false do - present "hello, world" + attestation = ::SupplyChain::Attestation.for_project(user_project.id).with_iid(params[:attestation_iid]).sole + + content_type 'application/vnd.dev.sigstore.bundle.v0.3+json' + header['Content-Disposition'] = "attachment; filename=sigstore.bundle" + + body attestation.file.read end end end diff --git a/public/19/58/19581e27de7ced00ff1ce50b2047e7a567c76b1cbaebabe5ef03f7c3017bb5b7/attestations/6/20251113-67866-q7dg7k b/public/19/58/19581e27de7ced00ff1ce50b2047e7a567c76b1cbaebabe5ef03f7c3017bb5b7/attestations/6/20251113-67866-q7dg7k new file mode 100644 index 00000000000000..2c0e268e59f1af --- /dev/null +++ b/public/19/58/19581e27de7ced00ff1ce50b2047e7a567c76b1cbaebabe5ef03f7c3017bb5b7/attestations/6/20251113-67866-q7dg7k @@ -0,0 +1 @@ +sample file contents \ No newline at end of file -- GitLab From ba25b5ccfb155d9cc06ebff2d515d1f88fb7a0aa Mon Sep 17 00:00:00 2001 From: Sam Joan Roque-Worcel Date: Thu, 13 Nov 2025 11:40:42 +1300 Subject: [PATCH 05/13] Update documentation --- doc/api/openapi/openapi_v2.yaml | 54 +++++++++++++++++++++++++++++++-- 1 file changed, 51 insertions(+), 3 deletions(-) diff --git a/doc/api/openapi/openapi_v2.yaml b/doc/api/openapi/openapi_v2.yaml index fa6e1114a1dfc7..cb6713982decdb 100644 --- a/doc/api/openapi/openapi_v2.yaml +++ b/doc/api/openapi/openapi_v2.yaml @@ -71,6 +71,8 @@ tags: description: Operations about protected_tags - name: remote_mirrors description: Operations about remote_mirrors +- name: attestations + description: Operations about attestations - name: tags description: Operations about tags - name: batched_background_migrations @@ -38344,10 +38346,12 @@ paths: '200': description: Fetch the list of all attestations for a specific project and artifact hash + schema: + "$ref": "#/definitions/API_Entities_SupplyChain_Attestation" '404': description: Artifact SHA-256 not found tags: - - projects + - attestations operationId: getApiV4ProjectsIdAttestationsSubjectDigest "/api/v4/projects/{id}/attestations/{attestation_iid}/download": get: @@ -38370,9 +38374,9 @@ paths: '200': description: Fetch a specific bundle by iid '404': - description: Attestation iid not found + description: Artifact SHA-256 not found tags: - - projects + - attestations operationId: getApiV4ProjectsIdAttestationsAttestationIidDownload "/api/v4/projects/{id}/repository/tags": get: @@ -66816,6 +66820,50 @@ definitions: - commit_sha - branch description: Update existing submodule reference in repository + API_Entities_SupplyChain_Attestation: + type: object + properties: + id: + type: integer + format: int32 + example: 1 + iid: + type: integer + format: int32 + example: 14 + created_at: + type: string + format: date-time + example: '2025-09-17T02:26:10.898Z' + updated_at: + type: string + format: date-time + example: '2025-09-17T02:26:10.898Z' + expire_at: + type: string + format: date-time + example: '2025-09-17T02:26:10.898Z' + project_id: + type: integer + format: int32 + build_id: + type: integer + format: int32 + status: + type: string + example: success + predicate_kind: + type: string + example: provenance + predicate_type: + type: string + example: https://slsa.dev/provenance/v1 + subject_digest: + type: string + example: 5db1fee4b5703808c48078a76768b155b421b210c0761cd6a5d223f4d99f1eaa + download_url: + type: string + description: API_Entities_SupplyChain_Attestation model API_Entities_Tag: type: object properties: -- GitLab From 933176b1983a55de1b5d101ebb4baae0dc5bcd4b Mon Sep 17 00:00:00 2001 From: Sam Joan Roque-Worcel Date: Fri, 14 Nov 2025 07:00:42 +1300 Subject: [PATCH 06/13] Remove file I uploaded by accident --- .../attestations/6/20251113-67866-q7dg7k | 1 - 1 file changed, 1 deletion(-) delete mode 100644 public/19/58/19581e27de7ced00ff1ce50b2047e7a567c76b1cbaebabe5ef03f7c3017bb5b7/attestations/6/20251113-67866-q7dg7k diff --git a/public/19/58/19581e27de7ced00ff1ce50b2047e7a567c76b1cbaebabe5ef03f7c3017bb5b7/attestations/6/20251113-67866-q7dg7k b/public/19/58/19581e27de7ced00ff1ce50b2047e7a567c76b1cbaebabe5ef03f7c3017bb5b7/attestations/6/20251113-67866-q7dg7k deleted file mode 100644 index 2c0e268e59f1af..00000000000000 --- a/public/19/58/19581e27de7ced00ff1ce50b2047e7a567c76b1cbaebabe5ef03f7c3017bb5b7/attestations/6/20251113-67866-q7dg7k +++ /dev/null @@ -1 +0,0 @@ -sample file contents \ No newline at end of file -- GitLab From 0be7508d3c560e1b90437a75b728b5837756bfe3 Mon Sep 17 00:00:00 2001 From: Sam Joan Roque-Worcel Date: Fri, 14 Nov 2025 11:11:39 +1300 Subject: [PATCH 07/13] Add test for `with_iid` --- spec/models/supply_chain/attestation_spec.rb | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/spec/models/supply_chain/attestation_spec.rb b/spec/models/supply_chain/attestation_spec.rb index 764035490b1c11..0bd35f97bedec9 100644 --- a/spec/models/supply_chain/attestation_spec.rb +++ b/spec/models/supply_chain/attestation_spec.rb @@ -66,6 +66,16 @@ end end + describe '#with_iid' do + let(:iid) { 1337 } + + subject(:attestation) { create(:supply_chain_attestation, iid: iid) } + + it 'returns the appropriate attestation' do + expect(described_class.for_project(attestation.project).with_iid(iid).take).to eq(attestation) + end + end + describe 'modules' do let_it_be(:project) { create(:project) } -- GitLab From 3db81faf04b4cd9ad80d193c6eeeae10752c7964 Mon Sep 17 00:00:00 2001 From: Sam Joan Roque-Worcel Date: Fri, 14 Nov 2025 13:38:43 +1300 Subject: [PATCH 08/13] Add new fields to attestation spec --- spec/requests/api/supply_chain/attestations_spec.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/spec/requests/api/supply_chain/attestations_spec.rb b/spec/requests/api/supply_chain/attestations_spec.rb index 67a7a9dfab8c09..4c1b3eeb0a4bab 100644 --- a/spec/requests/api/supply_chain/attestations_spec.rb +++ b/spec/requests/api/supply_chain/attestations_spec.rb @@ -65,7 +65,10 @@ "status" => "success", "predicate_kind" => "provenance", "predicate_type" => "https://slsa.dev/provenance/v1", - "subject_digest" => subject_digest + "subject_digest" => subject_digest, + "iid" => attestation.iid, + "download_url" => end_with(api_v4_projects_attestations_download_path(id: attestation.project_id, + attestation_iid: attestation.iid)) }) end -- GitLab From 2c338febe18d410ee9afe83fab530206b8219735 Mon Sep 17 00:00:00 2001 From: Sam Joan Roque-Worcel Date: Mon, 17 Nov 2025 11:48:32 +1300 Subject: [PATCH 09/13] Fix issue with string encoding, add test In this commit, I've added a test that tests the basic functionality. While I was doing this, I found a bug with the implementation that I've also fixed. --- lib/api/supply_chain/attestations.rb | 3 ++- .../attestations/6/20251113-67866-q7dg7k | 1 + .../api/supply_chain/attestations_spec.rb | 16 ++++++++++++++++ 3 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 public/19/58/19581e27de7ced00ff1ce50b2047e7a567c76b1cbaebabe5ef03f7c3017bb5b7/attestations/6/20251113-67866-q7dg7k diff --git a/lib/api/supply_chain/attestations.rb b/lib/api/supply_chain/attestations.rb index f9cf65046be82c..7bba9417af3e82 100644 --- a/lib/api/supply_chain/attestations.rb +++ b/lib/api/supply_chain/attestations.rb @@ -53,8 +53,9 @@ class Attestations < ::API::Base content_type 'application/vnd.dev.sigstore.bundle.v0.3+json' header['Content-Disposition'] = "attachment; filename=sigstore.bundle" + env['api.format'] = :txt - body attestation.file.read + present attestation.file.read end end end diff --git a/public/19/58/19581e27de7ced00ff1ce50b2047e7a567c76b1cbaebabe5ef03f7c3017bb5b7/attestations/6/20251113-67866-q7dg7k b/public/19/58/19581e27de7ced00ff1ce50b2047e7a567c76b1cbaebabe5ef03f7c3017bb5b7/attestations/6/20251113-67866-q7dg7k new file mode 100644 index 00000000000000..4b5fa63702dd96 --- /dev/null +++ b/public/19/58/19581e27de7ced00ff1ce50b2047e7a567c76b1cbaebabe5ef03f7c3017bb5b7/attestations/6/20251113-67866-q7dg7k @@ -0,0 +1 @@ +hello, world diff --git a/spec/requests/api/supply_chain/attestations_spec.rb b/spec/requests/api/supply_chain/attestations_spec.rb index 4c1b3eeb0a4bab..c3b34e2b5fd7e3 100644 --- a/spec/requests/api/supply_chain/attestations_spec.rb +++ b/spec/requests/api/supply_chain/attestations_spec.rb @@ -184,4 +184,20 @@ end end end + + describe 'GET /projects/:id/attestations/:iid/download' do + let_it_be(:attestation) { create(:supply_chain_attestation, project: project) } + + subject(:download_attestation) do + url = "/projects/#{project.id}/attestations/#{attestation.iid}/download" + get api(url, api_user) + end + + it "returns the contents of the attestation" do + status_code = download_attestation + + expect(status_code).to eq(200) + expect(response.body).to eq(attestation.file.read) + end + end end -- GitLab From 5864a59e553e2610ad5d38365bb9f7f0899aa689 Mon Sep 17 00:00:00 2001 From: Sam Joan Roque-Worcel Date: Mon, 17 Nov 2025 14:18:40 +1300 Subject: [PATCH 10/13] Add tests for attestations Add unit tests for FF check, authentication checks and also 404 cases. I've slightly refactored the code so that the same checks can be used in both endpoints. --- .../api/supply_chain/attestations_spec.rb | 192 +++++++++++------- 1 file changed, 116 insertions(+), 76 deletions(-) diff --git a/spec/requests/api/supply_chain/attestations_spec.rb b/spec/requests/api/supply_chain/attestations_spec.rb index c3b34e2b5fd7e3..9ba1c9bf7ca09e 100644 --- a/spec/requests/api/supply_chain/attestations_spec.rb +++ b/spec/requests/api/supply_chain/attestations_spec.rb @@ -34,10 +34,88 @@ project.add_developer(developer) end + shared_examples 'authorization checks' do + context 'when user is anonymous' do + let(:api_user) { nil } + + context 'when project is public' do + before do + project.update_column(:visibility_level, Gitlab::VisibilityLevel::PUBLIC) + project.update_column(:public_builds, true) + end + + it 'allows to access attestations' do + get_endpoint + + expect(response).to have_gitlab_http_status(:ok) + end + end + + context 'when project is private' do + it 'rejects access and hides existence of attestations' do + get_endpoint + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + + context 'when user is guest' do + let(:api_user) { guest } + + it 'allows to access attestations if has access' do + get_endpoint + + expect(response).to have_gitlab_http_status(:ok) + end + + it 'disallows access if does not have access' do + get_endpoint_other_project + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'when user is developer' do + let(:api_user) { developer } + + it 'allows to access attestations if has access' do + get_endpoint + + expect(response).to have_gitlab_http_status(:ok) + end + + it 'disallows access if does not have access' do + get_endpoint_other_project + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'when user is reporter' do + let(:api_user) { reporter } + + it 'allows to access attestations if has access' do + get_endpoint + + expect(response).to have_gitlab_http_status(:ok) + end + + it 'disallows access if does not have access' do + get_endpoint_other_project + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + describe 'GET /projects/:id/attestations/:subject_digest' do let_it_be(:subject_digest) { "5db1fee4b5703808c48078a76768b155b421b210c0761cd6a5d223f4d99f1eaa" } let_it_be(:other_subject_digest) { "fffffee4b5703808c48078a76768b155b421b210c0761cd6a5d223f4d99f1eaa" } let_it_be(:attestation) { create(:supply_chain_attestation, project: project, subject_digest: subject_digest) } + let(:get_endpoint) { get_attestations } + let(:get_endpoint_other_project) { get_attestations_other_project } + let_it_be(:other_attestation) do create(:supply_chain_attestation, project: other_project, subject_digest: subject_digest) end @@ -84,80 +162,7 @@ end end - describe 'authorization checks' do - context 'when user is anonymous' do - let(:api_user) { nil } - - context 'when project is public' do - before do - project.update_column(:visibility_level, Gitlab::VisibilityLevel::PUBLIC) - project.update_column(:public_builds, true) - end - - it 'allows to access attestations' do - get_attestations - - expect(response).to have_gitlab_http_status(:ok) - end - end - - context 'when project is private' do - it 'rejects access and hides existence of attestations' do - get_attestations - - expect(response).to have_gitlab_http_status(:not_found) - end - end - end - - context 'when user is guest' do - let(:api_user) { guest } - - it 'allows to access attestations if has access' do - get_attestations - - expect(response).to have_gitlab_http_status(:ok) - end - - it 'disallows access if does not have access' do - get_attestations_other_project - - expect(response).to have_gitlab_http_status(:not_found) - end - end - - context 'when user is developer' do - let(:api_user) { developer } - - it 'allows to access attestations if has access' do - get_attestations - - expect(response).to have_gitlab_http_status(:ok) - end - - it 'disallows access if does not have access' do - get_attestations_other_project - - expect(response).to have_gitlab_http_status(:not_found) - end - end - - context 'when user is reporter' do - let(:api_user) { reporter } - - it 'allows to access attestations if has access' do - get_attestations - - expect(response).to have_gitlab_http_status(:ok) - end - - it 'disallows access if does not have access' do - get_attestations_other_project - - expect(response).to have_gitlab_http_status(:not_found) - end - end - end + include_examples "authorization checks" end context 'when accessing via the project id' do @@ -186,10 +191,21 @@ end describe 'GET /projects/:id/attestations/:iid/download' do - let_it_be(:attestation) { create(:supply_chain_attestation, project: project) } + let!(:attestation) { create(:supply_chain_attestation, project: project) } + let!(:other_attestation) do + create(:supply_chain_attestation, project: other_project) + end + + let(:target_url) { "/projects/#{project.id}/attestations/#{other_attestation.iid}/download" } + let(:get_endpoint) { download_attestation } + let(:get_endpoint_other_project) { download_attestation_other_project } + let(:download_attestation_other_project) do + url = "/projects/#{other_project.id}/attestations/#{attestation.iid}/download" + get api(url, api_user) + end subject(:download_attestation) do - url = "/projects/#{project.id}/attestations/#{attestation.iid}/download" + url = target_url get api(url, api_user) end @@ -199,5 +215,29 @@ expect(status_code).to eq(200) expect(response.body).to eq(attestation.file.read) end + + context 'when slsa_provenance_statement is disabled' do + before do + stub_feature_flags(slsa_provenance_statement: false) + end + + it 'returns 404' do + download_attestation + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'when an invalid iid is passed' do + let(:target_url) { "/projects/#{project.id}/attestations/1337/download" } + + it 'returns 404' do + download_attestation + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + include_examples "authorization checks" end end -- GitLab From 375d2976bce03df67d467d30033016cecdd2dfcb Mon Sep 17 00:00:00 2001 From: Sam Joan Roque-Worcel Date: Mon, 17 Nov 2025 14:29:36 +1300 Subject: [PATCH 11/13] Remove accidentally added file --- .../attestations/6/20251113-67866-q7dg7k | 1 - 1 file changed, 1 deletion(-) delete mode 100644 public/19/58/19581e27de7ced00ff1ce50b2047e7a567c76b1cbaebabe5ef03f7c3017bb5b7/attestations/6/20251113-67866-q7dg7k diff --git a/public/19/58/19581e27de7ced00ff1ce50b2047e7a567c76b1cbaebabe5ef03f7c3017bb5b7/attestations/6/20251113-67866-q7dg7k b/public/19/58/19581e27de7ced00ff1ce50b2047e7a567c76b1cbaebabe5ef03f7c3017bb5b7/attestations/6/20251113-67866-q7dg7k deleted file mode 100644 index 4b5fa63702dd96..00000000000000 --- a/public/19/58/19581e27de7ced00ff1ce50b2047e7a567c76b1cbaebabe5ef03f7c3017bb5b7/attestations/6/20251113-67866-q7dg7k +++ /dev/null @@ -1 +0,0 @@ -hello, world -- GitLab From 35602a5d42a6bcb6854e61de0175d83598100309 Mon Sep 17 00:00:00 2001 From: Sam Joan Roque-Worcel Date: Tue, 18 Nov 2025 06:56:36 +1300 Subject: [PATCH 12/13] Modify test to use default value from factory --- spec/models/supply_chain/attestation_spec.rb | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/spec/models/supply_chain/attestation_spec.rb b/spec/models/supply_chain/attestation_spec.rb index 0bd35f97bedec9..3c80b573a736a7 100644 --- a/spec/models/supply_chain/attestation_spec.rb +++ b/spec/models/supply_chain/attestation_spec.rb @@ -67,12 +67,10 @@ end describe '#with_iid' do - let(:iid) { 1337 } - - subject(:attestation) { create(:supply_chain_attestation, iid: iid) } + subject(:attestation) { create(:supply_chain_attestation) } it 'returns the appropriate attestation' do - expect(described_class.for_project(attestation.project).with_iid(iid).take).to eq(attestation) + expect(described_class.for_project(attestation.project).with_iid(attestation.iid).take).to eq(attestation) end end -- GitLab From 57db7d5ad50d3bb8db59096c8a82a77245a915fb Mon Sep 17 00:00:00 2001 From: Sam Joan Roque-Worcel Date: Wed, 19 Nov 2025 09:29:02 +1300 Subject: [PATCH 13/13] Change content-disposition and type --- lib/api/supply_chain/attestations.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/api/supply_chain/attestations.rb b/lib/api/supply_chain/attestations.rb index 7bba9417af3e82..647fad58ce7de1 100644 --- a/lib/api/supply_chain/attestations.rb +++ b/lib/api/supply_chain/attestations.rb @@ -51,8 +51,7 @@ class Attestations < ::API::Base get ':id/attestations/:attestation_iid/download', urgency: :low, format: false do attestation = ::SupplyChain::Attestation.for_project(user_project.id).with_iid(params[:attestation_iid]).sole - content_type 'application/vnd.dev.sigstore.bundle.v0.3+json' - header['Content-Disposition'] = "attachment; filename=sigstore.bundle" + content_type 'text/json' env['api.format'] = :txt present attestation.file.read -- GitLab