diff --git a/app/models/packages/nuget/symbol.rb b/app/models/packages/nuget/symbol.rb index adcfaef4f4cdd50f06c1b2b505a04dda4418e84b..f15b045f303ffd2089ec44d8092fd666f01a517e 100644 --- a/app/models/packages/nuget/symbol.rb +++ b/app/models/packages/nuget/symbol.rb @@ -26,6 +26,9 @@ class Symbol < ApplicationRecord scope :stale, -> { where(package_id: nil) } scope :pending_destruction, -> { stale.default } + scope :with_file_name, ->(file_name) { where(file: file_name) } + scope :with_signature, ->(signature) { where(signature: signature) } + scope :with_file_sha256, ->(checksums) { where(file_sha256: checksums) } private diff --git a/config/feature_flags/development/nuget_symbolfiles_endpoint.yml b/config/feature_flags/development/nuget_symbolfiles_endpoint.yml new file mode 100644 index 0000000000000000000000000000000000000000..a8512c7190ae9f4434de7c56d4d6e0277bc35934 --- /dev/null +++ b/config/feature_flags/development/nuget_symbolfiles_endpoint.yml @@ -0,0 +1,8 @@ +--- +name: nuget_symbolfiles_endpoint +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/134564 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/433528 +milestone: '16.7' +type: development +group: group::package registry +default_enabled: false diff --git a/lib/api/concerns/packages/nuget/public_endpoints.rb b/lib/api/concerns/packages/nuget/public_endpoints.rb index b0c9177f4526e72bbe7a1e15c0063b61f5d3420f..740ff97e20c5934bac5eee0c92bb8e7c11de20f3 100644 --- a/lib/api/concerns/packages/nuget/public_endpoints.rb +++ b/lib/api/concerns/packages/nuget/public_endpoints.rb @@ -14,6 +14,8 @@ module Nuget module PublicEndpoints extend ActiveSupport::Concern + SHA256_REGEX = /SHA256:([a-f0-9]{64})/i + included do # https://docs.microsoft.com/en-us/nuget/api/service-index desc 'The NuGet V3 Feed Service Index' do @@ -43,6 +45,56 @@ module PublicEndpoints ] tags %w[nuget_packages] end + + namespace :symbolfiles do + after_validation do + not_found! if Feature.disabled?(:nuget_symbolfiles_endpoint, project_or_group_without_auth) + end + + desc 'The NuGet Symbol File Download Endpoint' do + detail 'This feature was introduced in GitLab 16.7' + success code: 200 + failure [ + { code: 400, message: 'Bad Request' }, + { code: 404, message: 'Not Found' } + ] + headers Symbolchecksum: { + type: String, + desc: 'The SHA256 checksums of the symbol file', + required: true + } + tags %w[nuget_packages] + end + params do + requires :file_name, allow_blank: false, type: String, desc: 'The symbol file name', + regexp: API::NO_SLASH_URL_PART_REGEX, documentation: { example: 'mynugetpkg.pdb' } + requires :signature, allow_blank: false, type: String, desc: 'The symbol file signature', + regexp: API::NO_SLASH_URL_PART_REGEX, + documentation: { example: 'k813f89485474661234z7109cve5709eFFFFFFFF' } + requires :same_file_name, same_as: :file_name + end + get '*file_name/*signature/*same_file_name', format: false, urgency: :low do + bad_request!('Missing checksum header') if headers['Symbolchecksum'].blank? + + project_or_group_without_auth + + # upcase the age part of the signature in case we received it in lowercase: + # https://github.com/dotnet/symstore/blob/main/docs/specs/SSQP_Key_Conventions.md#key-formatting-basic-rules + signature = declared_params[:signature].sub(/.{8}\z/, &:upcase) + checksums = headers['Symbolchecksum'].scan(SHA256_REGEX).flatten + + symbol = ::Packages::Nuget::Symbol + .with_signature(signature) + .with_file_name(declared_params[:file_name]) + .with_file_sha256(checksums) + .first + + not_found!('Symbol') unless symbol + + present_carrierwave_file!(symbol.file) + end + end + namespace '/v2' do get format: :xml, urgency: :low do env['api.format'] = :xml diff --git a/spec/models/packages/nuget/symbol_spec.rb b/spec/models/packages/nuget/symbol_spec.rb index 8d8604bb84a9a0efad45fcf602b97e94d6d1e7ea..bae8f90c7d50b060ea403848d8d9de7a81de4625 100644 --- a/spec/models/packages/nuget/symbol_spec.rb +++ b/spec/models/packages/nuget/symbol_spec.rb @@ -45,6 +45,43 @@ it { is_expected.to contain_exactly(stale_symbol) } end + + describe '.with_signature' do + subject(:with_signature) { described_class.with_signature(signature) } + + let_it_be(:signature) { 'signature' } + let_it_be(:symbol) { create(:nuget_symbol, signature: signature) } + + it 'returns symbols with the given signature' do + expect(with_signature).to eq([symbol]) + end + end + + describe '.with_file_name' do + subject(:with_file_name) { described_class.with_file_name(file_name) } + + let_it_be(:file_name) { 'file_name' } + let_it_be(:symbol) { create(:nuget_symbol) } + + before do + symbol.update_column(:file, file_name) + end + + it 'returns symbols with the given file_name' do + expect(with_file_name).to eq([symbol]) + end + end + + describe '.with_file_sha256' do + subject(:with_file_sha256) { described_class.with_file_sha256(checksums) } + + let_it_be(:checksums) { OpenSSL::Digest.hexdigest('SHA256', 'checksums') } + let_it_be(:symbol) { create(:nuget_symbol, file_sha256: checksums) } + + it 'returns symbols with the given checksums' do + expect(with_file_sha256).to eq([symbol]) + end + end end describe 'callbacks' do diff --git a/spec/requests/api/nuget_group_packages_spec.rb b/spec/requests/api/nuget_group_packages_spec.rb index 92eb869b871023415a67211fda738dc40690b447..4a763b3bbda7dc9a9a03233dbe6f7f38331533f8 100644 --- a/spec/requests/api/nuget_group_packages_spec.rb +++ b/spec/requests/api/nuget_group_packages_spec.rb @@ -188,6 +188,14 @@ def update_visibility_to(visibility) end end + describe 'GET /api/v4/groups/:id/-/packages/nuget/token/*token/symbolfiles/*file_name/*signature/*file_name' do + it_behaves_like 'nuget symbol file endpoint' do + let(:url) do + "/groups/#{target.id}/-/packages/nuget/symbolfiles/#{filename}/#{signature}/#{filename}" + end + end + end + def update_visibility_to(visibility) project.update!(visibility_level: visibility) subgroup.update!(visibility_level: visibility) diff --git a/spec/requests/api/nuget_project_packages_spec.rb b/spec/requests/api/nuget_project_packages_spec.rb index a116be84b3ef965168c2dbf0f57d5880cfc472e4..8252fc1c4cdfa510c49288d40c977403caa76aa5 100644 --- a/spec/requests/api/nuget_project_packages_spec.rb +++ b/spec/requests/api/nuget_project_packages_spec.rb @@ -419,6 +419,12 @@ def snowplow_context(user_role: :developer, event_user: user) end end + describe 'GET /api/v4/projects/:id/packages/nuget/symbolfiles/*file_name/*signature/*file_name' do + it_behaves_like 'nuget symbol file endpoint' do + let(:url) { "/projects/#{target.id}/packages/nuget/symbolfiles/#{filename}/#{signature}/#{filename}" } + end + end + def update_visibility_to(visibility) project.update!(visibility_level: visibility) end diff --git a/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb index c23d514abfc4fcbb6858f97505964880bf15094e..df7e36202ec3c9cd6928e4f3085fd1e836366238 100644 --- a/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb @@ -741,3 +741,61 @@ end end end + +RSpec.shared_examples 'nuget symbol file endpoint' do + let_it_be(:symbol) { create(:nuget_symbol) } + let_it_be(:filename) { symbol.file.filename } + let_it_be(:signature) { symbol.signature } + let_it_be(:checksum) { symbol.file_sha256.delete("\n") } + + let(:headers) { { 'Symbolchecksum' => "SHA256:#{checksum}" } } + + subject { get api(url), headers: headers } + + it { is_expected.to have_request_urgency(:low) } + + context 'with valid target' do + it 'returns the symbol file' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(response.media_type).to eq('application/octet-stream') + expect(response.body).to eq(symbol.file.read) + end + end + + context 'when nuget_symbolfiles_endpoint feature flag is disabled' do + before do + stub_feature_flags(nuget_symbolfiles_endpoint: false) + end + + it_behaves_like 'returning response status', :not_found + end + + context 'when target does not exist' do + let(:target) { double(id: 1234567890) } + + it_behaves_like 'returning response status', :not_found + end + + context 'when target exists' do + context 'when symbol file does not exist' do + let(:filename) { 'non-existent-file.pdb' } + let(:signature) { 'non-existent-signature' } + + it_behaves_like 'returning response status', :not_found + end + + context 'when symbol file checksum does not match' do + let(:checksum) { 'non-matching-checksum' } + + it_behaves_like 'returning response status', :not_found + end + + context 'when symbol file checksum is missing' do + let(:headers) { {} } + + it_behaves_like 'returning response status', :bad_request + end + end +end