Commit 3cb09d05 authored by Steve Abrams's avatar Steve Abrams Committed by David Fernandez

NuGet symbol package support

parent 09ad4f3b
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
module Packages module Packages
module Nuget module Nuget
TEMPORARY_PACKAGE_NAME = 'NuGet.Temporary.Package' TEMPORARY_PACKAGE_NAME = 'NuGet.Temporary.Package'
TEMPORARY_SYMBOL_PACKAGE_NAME = 'NuGet.Temporary.SymbolPackage'
def self.table_name_prefix def self.table_name_prefix
'packages_nuget_' 'packages_nuget_'
......
...@@ -8,6 +8,7 @@ module Packages ...@@ -8,6 +8,7 @@ module Packages
SERVICE_VERSIONS = { SERVICE_VERSIONS = {
download: %w[PackageBaseAddress/3.0.0], download: %w[PackageBaseAddress/3.0.0],
search: %w[SearchQueryService SearchQueryService/3.0.0-beta SearchQueryService/3.0.0-rc], search: %w[SearchQueryService SearchQueryService/3.0.0-beta SearchQueryService/3.0.0-rc],
symbol: %w[SymbolPackagePublish/4.9.0],
publish: %w[PackagePublish/2.0.0], publish: %w[PackagePublish/2.0.0],
metadata: %w[RegistrationsBaseUrl RegistrationsBaseUrl/3.0.0-beta RegistrationsBaseUrl/3.0.0-rc] metadata: %w[RegistrationsBaseUrl RegistrationsBaseUrl/3.0.0-beta RegistrationsBaseUrl/3.0.0-rc]
}.freeze }.freeze
...@@ -15,13 +16,14 @@ module Packages ...@@ -15,13 +16,14 @@ module Packages
SERVICE_COMMENTS = { SERVICE_COMMENTS = {
download: 'Get package content (.nupkg).', download: 'Get package content (.nupkg).',
search: 'Filter and search for packages by keyword.', search: 'Filter and search for packages by keyword.',
symbol: 'Push symbol packages.',
publish: 'Push and delete (or unlist) packages.', publish: 'Push and delete (or unlist) packages.',
metadata: 'Get package metadata.' metadata: 'Get package metadata.'
}.freeze }.freeze
VERSION = '3.0.0' VERSION = '3.0.0'
PROJECT_LEVEL_SERVICES = %i[download publish].freeze PROJECT_LEVEL_SERVICES = %i[download publish symbol].freeze
GROUP_LEVEL_SERVICES = %i[search metadata].freeze GROUP_LEVEL_SERVICES = %i[search metadata].freeze
def initialize(project_or_group) def initialize(project_or_group)
...@@ -63,6 +65,8 @@ module Packages ...@@ -63,6 +65,8 @@ module Packages
download_service_url download_service_url
when :search when :search
search_service_url search_service_url
when :symbol
symbol_service_url
when :metadata when :metadata
metadata_service_url metadata_service_url
when :publish when :publish
...@@ -124,6 +128,10 @@ module Packages ...@@ -124,6 +128,10 @@ module Packages
def publish_service_url def publish_service_url
api_v4_projects_packages_nuget_path(id: @project_or_group.id) api_v4_projects_packages_nuget_path(id: @project_or_group.id)
end end
def symbol_service_url
api_v4_projects_packages_nuget_symbolpackage_path(id: @project_or_group.id)
end
end end
end end
end end
...@@ -18,6 +18,7 @@ module Packages ...@@ -18,6 +18,7 @@ module Packages
XPATH_DEPENDENCIES = '//xmlns:package/xmlns:metadata/xmlns:dependencies/xmlns:dependency' XPATH_DEPENDENCIES = '//xmlns:package/xmlns:metadata/xmlns:dependencies/xmlns:dependency'
XPATH_DEPENDENCY_GROUPS = '//xmlns:package/xmlns:metadata/xmlns:dependencies/xmlns:group' XPATH_DEPENDENCY_GROUPS = '//xmlns:package/xmlns:metadata/xmlns:dependencies/xmlns:group'
XPATH_TAGS = '//xmlns:package/xmlns:metadata/xmlns:tags' XPATH_TAGS = '//xmlns:package/xmlns:metadata/xmlns:tags'
XPATH_PACKAGE_TYPES = '//xmlns:package/xmlns:metadata/xmlns:packageTypes/xmlns:packageType'
MAX_FILE_SIZE = 4.megabytes.freeze MAX_FILE_SIZE = 4.megabytes.freeze
...@@ -57,6 +58,7 @@ module Packages ...@@ -57,6 +58,7 @@ module Packages
.tap do |metadata| .tap do |metadata|
metadata[:package_dependencies] = extract_dependencies(doc) metadata[:package_dependencies] = extract_dependencies(doc)
metadata[:package_tags] = extract_tags(doc) metadata[:package_tags] = extract_tags(doc)
metadata[:package_types] = extract_package_types(doc)
end end
end end
...@@ -85,6 +87,10 @@ module Packages ...@@ -85,6 +87,10 @@ module Packages
}.compact }.compact
end end
def extract_package_types(doc)
doc.xpath(XPATH_PACKAGE_TYPES).map { |node| node.attr('name') }.uniq
end
def extract_tags(doc) def extract_tags(doc)
tags = doc.xpath(XPATH_TAGS).text tags = doc.xpath(XPATH_TAGS).text
......
...@@ -8,6 +8,7 @@ module Packages ...@@ -8,6 +8,7 @@ module Packages
# used by ExclusiveLeaseGuard # used by ExclusiveLeaseGuard
DEFAULT_LEASE_TIMEOUT = 1.hour.to_i.freeze DEFAULT_LEASE_TIMEOUT = 1.hour.to_i.freeze
SYMBOL_PACKAGE_IDENTIFIER = 'SymbolsPackage'
InvalidMetadataError = Class.new(StandardError) InvalidMetadataError = Class.new(StandardError)
...@@ -20,7 +21,13 @@ module Packages ...@@ -20,7 +21,13 @@ module Packages
try_obtain_lease do try_obtain_lease do
@package_file.transaction do @package_file.transaction do
package = existing_package ? link_to_existing_package : update_linked_package if existing_package
package = link_to_existing_package
elsif symbol_package?
raise InvalidMetadataError, 'symbol package is invalid, matching package does not exist'
else
package = update_linked_package
end
update_package(package) update_package(package)
...@@ -39,6 +46,8 @@ module Packages ...@@ -39,6 +46,8 @@ module Packages
private private
def update_package(package) def update_package(package)
return if symbol_package?
::Packages::Nuget::SyncMetadatumService ::Packages::Nuget::SyncMetadatumService
.new(package, metadata.slice(:project_url, :license_url, :icon_url)) .new(package, metadata.slice(:project_url, :license_url, :icon_url))
.execute .execute
...@@ -103,6 +112,14 @@ module Packages ...@@ -103,6 +112,14 @@ module Packages
metadata.fetch(:package_tags, []) metadata.fetch(:package_tags, [])
end end
def package_types
metadata.fetch(:package_types, [])
end
def symbol_package?
package_types.include?(SYMBOL_PACKAGE_IDENTIFIER)
end
def metadata def metadata
strong_memoize(:metadata) do strong_memoize(:metadata) do
::Packages::Nuget::MetadataExtractionService.new(@package_file.id).execute ::Packages::Nuget::MetadataExtractionService.new(@package_file.id).execute
...@@ -110,7 +127,7 @@ module Packages ...@@ -110,7 +127,7 @@ module Packages
end end
def package_filename def package_filename
"#{package_name.downcase}.#{package_version.downcase}.nupkg" "#{package_name.downcase}.#{package_version.downcase}.#{symbol_package? ? 'snupkg' : 'nupkg'}"
end end
# used by ExclusiveLeaseGuard # used by ExclusiveLeaseGuard
......
...@@ -8,6 +8,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w ...@@ -8,6 +8,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/20050) in GitLab Premium 12.8. > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/20050) in GitLab Premium 12.8.
> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/221259) to GitLab Free in 13.3. > - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/221259) to GitLab Free in 13.3.
> - Symbol package support [added](https://gitlab.com/gitlab-org/gitlab/-/issues/262081) in GitLab 14.1.
Publish NuGet packages in your project's Package Registry. Then, install the Publish NuGet packages in your project's Package Registry. Then, install the
packages whenever you need to use them as a dependency. packages whenever you need to use them as a dependency.
...@@ -394,6 +395,24 @@ dotnet add package <package_id> \ ...@@ -394,6 +395,24 @@ dotnet add package <package_id> \
- `<package_id>` is the package ID. - `<package_id>` is the package ID.
- `<package_version>` is the package version. Optional. - `<package_version>` is the package version. Optional.
## Symbol packages
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/262081) in GitLab 14.1.
If you push a `.nupkg`, symbol package files in the `.snupkg` format are uploaded automatically. You
can also push them manually:
```shell
nuget push My.Package.snupkg -Source <source_name>
```
Consuming symbol packages is not yet guaranteed using clients such as Visual Studio or
dotnet-symbol. The `.snupkg` files are available for download through the UI or the
[API](../../../api/packages/nuget.md#download-a-package-file).
Follow the [NuGet symbol package issue](https://gitlab.com/gitlab-org/gitlab/-/issues/262081)
for further updates.
## Supported CLI commands ## Supported CLI commands
The GitLab NuGet repository supports the following commands for the NuGet CLI (`nuget`) and the .NET The GitLab NuGet repository supports the following commands for the NuGet CLI (`nuget`) and the .NET
......
...@@ -16,6 +16,7 @@ module API ...@@ -16,6 +16,7 @@ module API
feature_category :package_registry feature_category :package_registry
PACKAGE_FILENAME = 'package.nupkg' PACKAGE_FILENAME = 'package.nupkg'
SYMBOL_PACKAGE_FILENAME = 'package.snupkg'
default_format :json default_format :json
...@@ -33,6 +34,10 @@ module API ...@@ -33,6 +34,10 @@ module API
end end
helpers do helpers do
params :file_params do
requires :package, type: ::API::Validations::Types::WorkhorseFile, desc: 'The package file to be published (generated by Multipart middleware)'
end
def project_or_group def project_or_group
authorized_user_project authorized_user_project
end end
...@@ -40,6 +45,49 @@ module API ...@@ -40,6 +45,49 @@ module API
def snowplow_gitlab_standard_context def snowplow_gitlab_standard_context
{ project: authorized_user_project, namespace: authorized_user_project.namespace } { project: authorized_user_project, namespace: authorized_user_project.namespace }
end end
def authorize_nuget_upload
authorize_workhorse!(
subject: project_or_group,
has_length: false,
maximum_size: project_or_group.actual_limits.nuget_max_file_size
)
end
def temp_file_name(symbol_package)
return ::Packages::Nuget::TEMPORARY_SYMBOL_PACKAGE_NAME if symbol_package
::Packages::Nuget::TEMPORARY_PACKAGE_NAME
end
def file_name(symbol_package)
return SYMBOL_PACKAGE_FILENAME if symbol_package
PACKAGE_FILENAME
end
def upload_nuget_package_file(symbol_package: false)
authorize_upload!(project_or_group)
bad_request!('File is too large') if project_or_group.actual_limits.exceeded?(:nuget_max_file_size, params[:package].size)
file_params = params.merge(
file: params[:package],
file_name: file_name(symbol_package)
)
package = ::Packages::CreateTemporaryPackageService.new(
project_or_group, current_user, declared_params.merge(build: current_authenticated_job)
).execute(:nuget, name: temp_file_name(symbol_package))
package_file = ::Packages::CreatePackageFileService.new(package, file_params.merge(build: current_authenticated_job))
.execute
yield(package) if block_given?
::Packages::Nuget::ExtractionWorker.perform_async(package_file.id) # rubocop:disable CodeReuse/Worker
created!
end
end end
params do params do
...@@ -55,40 +103,45 @@ module API ...@@ -55,40 +103,45 @@ module API
end end
params do params do
requires :package, type: ::API::Validations::Types::WorkhorseFile, desc: 'The package file to be published (generated by Multipart middleware)' use :file_params
end end
put do put do
authorize_upload!(project_or_group) upload_nuget_package_file do |package|
bad_request!('File is too large') if project_or_group.actual_limits.exceeded?(:nuget_max_file_size, params[:package].size) track_package_event(
'push_package',
file_params = params.merge( :nuget,
file: params[:package], category: 'API::NugetPackages',
file_name: PACKAGE_FILENAME user: current_user,
) project: package.project,
namespace: package.project.namespace
package = ::Packages::CreateTemporaryPackageService.new( )
project_or_group, current_user, declared_params.merge(build: current_authenticated_job) end
).execute(:nuget, name: ::Packages::Nuget::TEMPORARY_PACKAGE_NAME) rescue ObjectStorage::RemoteStoreError => e
Gitlab::ErrorTracking.track_exception(e, extra: { file_name: params[:file_name], project_id: project_or_group.id })
package_file = ::Packages::CreatePackageFileService.new(package, file_params.merge(build: current_authenticated_job))
.execute
track_package_event('push_package', :nuget, category: 'API::NugetPackages', user: current_user, project: package.project, namespace: package.project.namespace) forbidden!
end
put 'authorize' do
authorize_nuget_upload
end
::Packages::Nuget::ExtractionWorker.perform_async(package_file.id) # rubocop:disable CodeReuse/Worker # https://docs.microsoft.com/en-us/nuget/api/symbol-package-publish-resource
desc 'The NuGet Symbol Package Publish endpoint' do
detail 'This feature was introduced in GitLab 14.1'
end
created! params do
use :file_params
end
put 'symbolpackage' do
upload_nuget_package_file(symbol_package: true)
rescue ObjectStorage::RemoteStoreError => e rescue ObjectStorage::RemoteStoreError => e
Gitlab::ErrorTracking.track_exception(e, extra: { file_name: params[:file_name], project_id: project_or_group.id }) Gitlab::ErrorTracking.track_exception(e, extra: { file_name: params[:file_name], project_id: project_or_group.id })
forbidden! forbidden!
end end
put 'authorize' do put 'symbolpackage/authorize' do
authorize_workhorse!( authorize_nuget_upload
subject: project_or_group,
has_length: false,
maximum_size: project_or_group.actual_limits.nuget_max_file_size
)
end end
# https://docs.microsoft.com/en-us/nuget/api/package-base-address-resource # https://docs.microsoft.com/en-us/nuget/api/package-base-address-resource
...@@ -115,14 +168,22 @@ module API ...@@ -115,14 +168,22 @@ module API
requires :package_version, type: String, desc: 'The NuGet package version', regexp: API::NO_SLASH_URL_PART_REGEX requires :package_version, type: String, desc: 'The NuGet package version', regexp: API::NO_SLASH_URL_PART_REGEX
requires :package_filename, type: String, desc: 'The NuGet package filename', regexp: API::NO_SLASH_URL_PART_REGEX requires :package_filename, type: String, desc: 'The NuGet package filename', regexp: API::NO_SLASH_URL_PART_REGEX
end end
get '*package_version/*package_filename', format: :nupkg do get '*package_version/*package_filename', format: [:nupkg, :snupkg] do
filename = "#{params[:package_filename]}.#{params[:format]}" filename = "#{params[:package_filename]}.#{params[:format]}"
package_file = ::Packages::PackageFileFinder.new(find_package(params[:package_name], params[:package_version]), filename, with_file_name_like: true) package_file = ::Packages::PackageFileFinder.new(find_package(params[:package_name], params[:package_version]), filename, with_file_name_like: true)
.execute .execute
not_found!('Package') unless package_file not_found!('Package') unless package_file
track_package_event('pull_package', :nuget, category: 'API::NugetPackages', project: package_file.project, namespace: package_file.project.namespace) if params[:format] == 'nupkg'
track_package_event(
'pull_package',
:nuget,
category: 'API::NugetPackages',
project: package_file.project,
namespace: package_file.project.namespace
)
end
# nuget and dotnet don't support 302 Moved status codes, supports_direct_download has to be set to false # nuget and dotnet don't support 302 Moved status codes, supports_direct_download has to be set to false
present_carrierwave_file!(package_file.file, supports_direct_download: false) present_carrierwave_file!(package_file.file, supports_direct_download: false)
......
...@@ -162,6 +162,12 @@ FactoryBot.define do ...@@ -162,6 +162,12 @@ FactoryBot.define do
pkg.nuget_metadatum = build(:nuget_metadatum) pkg.nuget_metadatum = build(:nuget_metadatum)
end end
end end
trait(:with_symbol_package) do
after :create do |package|
create :package_file, :snupkg, package: package, file_name: "#{package.name}.#{package.version}.snupkg"
end
end
end end
factory :pypi_package do factory :pypi_package do
......
...@@ -271,6 +271,14 @@ FactoryBot.define do ...@@ -271,6 +271,14 @@ FactoryBot.define do
size { 300.kilobytes } size { 300.kilobytes }
end end
trait(:snupkg) do
package
file_fixture { 'spec/fixtures/packages/nuget/package.snupkg' }
file_name { 'package.snupkg' }
file_sha1 { '5fe852b2a6abd96c22c11fa1ff2fb19d9ce58b57' }
size { 300.kilobytes }
end
trait(:gem) do trait(:gem) do
package package
file_fixture { 'spec/fixtures/packages/rubygems/package-0.0.1.gem' } file_fixture { 'spec/fixtures/packages/rubygems/package-0.0.1.gem' }
......
<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd">
<metadata>
<id>Test.Package</id>
<version>3.5.2</version>
<authors>Test Author</authors>
<owners>Test Owner</owners>
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<description>Package Description</description>
<packageTypes>
<packageType name="SymbolsPackage" />
</packageTypes>
</metadata>
</package>
...@@ -27,7 +27,7 @@ RSpec.describe ::Packages::Nuget::ServiceIndexPresenter do ...@@ -27,7 +27,7 @@ RSpec.describe ::Packages::Nuget::ServiceIndexPresenter do
describe '#resources' do describe '#resources' do
subject { presenter.resources } subject { presenter.resources }
shared_examples 'returning valid resources' do |resources_count: 8, include_publish_service: true| shared_examples 'returning valid resources' do |resources_count: 9, include_publish_service: true|
it 'has valid resources' do it 'has valid resources' do
expect(subject.size).to eq resources_count expect(subject.size).to eq resources_count
subject.each do |resource| subject.each do |resource|
...@@ -38,10 +38,15 @@ RSpec.describe ::Packages::Nuget::ServiceIndexPresenter do ...@@ -38,10 +38,15 @@ RSpec.describe ::Packages::Nuget::ServiceIndexPresenter do
end end
end end
it "does #{'not ' unless include_publish_service}return the publish resource" do it "does #{'not ' unless include_publish_service}return the publish resource", :aggregate_failures do
services_types = subject.map { |res| res[:@type] } services_types = subject.map { |res| res[:@type] }
described_class::SERVICE_VERSIONS[:publish].each do |publish_service_version| publish_service_versions = [
described_class::SERVICE_VERSIONS[:publish],
described_class::SERVICE_VERSIONS[:symbol]
].flatten
publish_service_versions.each do |publish_service_version|
if include_publish_service if include_publish_service
expect(services_types).to include(publish_service_version) expect(services_types).to include(publish_service_version)
else else
...@@ -54,7 +59,7 @@ RSpec.describe ::Packages::Nuget::ServiceIndexPresenter do ...@@ -54,7 +59,7 @@ RSpec.describe ::Packages::Nuget::ServiceIndexPresenter do
context 'for a group' do context 'for a group' do
let(:target) { group } let(:target) { group }
# at the group level we don't have the publish and download service # at the group level we don't have the publish, symbol, and download service
it_behaves_like 'returning valid resources', resources_count: 6, include_publish_service: false it_behaves_like 'returning valid resources', resources_count: 6, include_publish_service: false
end end
......
...@@ -92,9 +92,10 @@ RSpec.describe API::NugetProjectPackages do ...@@ -92,9 +92,10 @@ RSpec.describe API::NugetProjectPackages do
describe 'GET /api/v4/projects/:id/packages/nuget/download/*package_name/*package_version/*package_filename' do describe 'GET /api/v4/projects/:id/packages/nuget/download/*package_name/*package_version/*package_filename' do
let_it_be(:package_name) { 'Dummy.Package' } let_it_be(:package_name) { 'Dummy.Package' }
let_it_be(:package) { create(:nuget_package, project: project, name: package_name) } let_it_be(:package) { create(:nuget_package, :with_symbol_package, project: project, name: package_name) }
let(:url) { "/projects/#{target.id}/packages/nuget/download/#{package.name}/#{package.version}/#{package.name}.#{package.version}.nupkg" } let(:format) { 'nupkg' }
let(:url) { "/projects/#{target.id}/packages/nuget/download/#{package.name}/#{package.version}/#{package.name}.#{package.version}.#{format}" }
subject { get api(url) } subject { get api(url) }
...@@ -154,50 +155,7 @@ RSpec.describe API::NugetProjectPackages do ...@@ -154,50 +155,7 @@ RSpec.describe API::NugetProjectPackages do
subject { put api(url), headers: headers } subject { put api(url), headers: headers }
context 'with valid project' do it_behaves_like 'nuget authorize upload endpoint'
where(:visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do
'PUBLIC' | :developer | true | true | 'process nuget workhorse authorization' | :success
'PUBLIC' | :guest | true | true | 'rejects nuget packages access' | :forbidden
'PUBLIC' | :developer | true | false | 'rejects nuget packages access' | :unauthorized
'PUBLIC' | :guest | true | false | 'rejects nuget packages access' | :unauthorized
'PUBLIC' | :developer | false | true | 'rejects nuget packages access' | :forbidden
'PUBLIC' | :guest | false | true | 'rejects nuget packages access' | :forbidden
'PUBLIC' | :developer | false | false | 'rejects nuget packages access' | :unauthorized
'PUBLIC' | :guest | false | false | 'rejects nuget packages access' | :unauthorized
'PUBLIC' | :anonymous | false | true | 'rejects nuget packages access' | :unauthorized
'PRIVATE' | :developer | true | true | 'process nuget workhorse authorization' | :success
'PRIVATE' | :guest | true | true | 'rejects nuget packages access' | :forbidden
'PRIVATE' | :developer | true | false | 'rejects nuget packages access' | :unauthorized
'PRIVATE' | :guest | true | false | 'rejects nuget packages access' | :unauthorized
'PRIVATE' | :developer | false | true | 'rejects nuget packages access' | :not_found
'PRIVATE' | :guest | false | true | 'rejects nuget packages access' | :not_found
'PRIVATE' | :developer | false | false | 'rejects nuget packages access' | :unauthorized
'PRIVATE' | :guest | false | false | 'rejects nuget packages access' | :unauthorized
'PRIVATE' | :anonymous | false | true | 'rejects nuget packages access' | :unauthorized
end
with_them do
let(:token) { user_token ? personal_access_token.token : 'wrong' }
let(:user_headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) }
let(:headers) { user_headers.merge(workhorse_headers) }
before do
update_visibility_to(Gitlab::VisibilityLevel.const_get(visibility_level, false))
end
it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member]
end
end
it_behaves_like 'deploy token for package uploads'
it_behaves_like 'job token for package uploads', authorize_endpoint: true do
let_it_be(:job) { create(:ci_build, :running, user: user, project: project) }
end
it_behaves_like 'rejects nuget access with unknown target id'
it_behaves_like 'rejects nuget access with invalid target id'
end end
describe 'PUT /api/v4/projects/:id/packages/nuget' do describe 'PUT /api/v4/projects/:id/packages/nuget' do
...@@ -221,63 +179,42 @@ RSpec.describe API::NugetProjectPackages do ...@@ -221,63 +179,42 @@ RSpec.describe API::NugetProjectPackages do
) )
end end
context 'with valid project' do it_behaves_like 'nuget upload endpoint'
where(:visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do end
'PUBLIC' | :developer | true | true | 'process nuget upload' | :created
'PUBLIC' | :guest | true | true | 'rejects nuget packages access' | :forbidden
'PUBLIC' | :developer | true | false | 'rejects nuget packages access' | :unauthorized
'PUBLIC' | :guest | true | false | 'rejects nuget packages access' | :unauthorized
'PUBLIC' | :developer | false | true | 'rejects nuget packages access' | :forbidden
'PUBLIC' | :guest | false | true | 'rejects nuget packages access' | :forbidden
'PUBLIC' | :developer | false | false | 'rejects nuget packages access' | :unauthorized
'PUBLIC' | :guest | false | false | 'rejects nuget packages access' | :unauthorized
'PUBLIC' | :anonymous | false | true | 'rejects nuget packages access' | :unauthorized
'PRIVATE' | :developer | true | true | 'process nuget upload' | :created
'PRIVATE' | :guest | true | true | 'rejects nuget packages access' | :forbidden
'PRIVATE' | :developer | true | false | 'rejects nuget packages access' | :unauthorized
'PRIVATE' | :guest | true | false | 'rejects nuget packages access' | :unauthorized
'PRIVATE' | :developer | false | true | 'rejects nuget packages access' | :not_found
'PRIVATE' | :guest | false | true | 'rejects nuget packages access' | :not_found
'PRIVATE' | :developer | false | false | 'rejects nuget packages access' | :unauthorized
'PRIVATE' | :guest | false | false | 'rejects nuget packages access' | :unauthorized
'PRIVATE' | :anonymous | false | true | 'rejects nuget packages access' | :unauthorized
end
with_them do
let(:token) { user_token ? personal_access_token.token : 'wrong' }
let(:user_headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) }
let(:headers) { user_headers.merge(workhorse_headers) }
let(:snowplow_gitlab_standard_context) { { project: project, user: user, namespace: project.namespace } }
before do
update_visibility_to(Gitlab::VisibilityLevel.const_get(visibility_level, false))
end
it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member]
end
end
it_behaves_like 'deploy token for package uploads' describe 'PUT /api/v4/projects/:id/packages/nuget/symbolpackage/authorize' do
include_context 'workhorse headers'
it_behaves_like 'job token for package uploads' do let(:url) { "/projects/#{target.id}/packages/nuget/symbolpackage/authorize" }
let_it_be(:job) { create(:ci_build, :running, user: user, project: project) } let(:headers) { {} }
end
it_behaves_like 'rejects nuget access with unknown target id' subject { put api(url), headers: headers }
it_behaves_like 'rejects nuget access with invalid target id' it_behaves_like 'nuget authorize upload endpoint'
end
context 'file size above maximum limit' do describe 'PUT /api/v4/projects/:id/packages/nuget/symbolpackage' do
let(:headers) { basic_auth_header(deploy_token.username, deploy_token.token).merge(workhorse_headers) } include_context 'workhorse headers'
before do let_it_be(:file_name) { 'package.snupkg' }
allow_next_instance_of(UploadedFile) do |uploaded_file| let(:url) { "/projects/#{target.id}/packages/nuget/symbolpackage" }
allow(uploaded_file).to receive(:size).and_return(project.actual_limits.nuget_max_file_size + 1) let(:headers) { {} }
end let(:params) { { package: temp_file(file_name) } }
end let(:file_key) { :package }
let(:send_rewritten_field) { true }
it_behaves_like 'returning response status', :bad_request subject do
workhorse_finalize(
api(url),
method: :put,
file_key: file_key,
params: params,
headers: headers,
send_rewritten_field: send_rewritten_field
)
end end
it_behaves_like 'nuget upload endpoint', symbol_package: true
end end
def update_visibility_to(visibility) def update_visibility_to(visibility)
......
...@@ -21,7 +21,8 @@ RSpec.describe Packages::Nuget::MetadataExtractionService do ...@@ -21,7 +21,8 @@ RSpec.describe Packages::Nuget::MetadataExtractionService do
version: '12.0.3' version: '12.0.3'
} }
], ],
package_tags: [] package_tags: [],
package_types: []
} }
it { is_expected.to eq(expected_metadata) } it { is_expected.to eq(expected_metadata) }
...@@ -47,6 +48,16 @@ RSpec.describe Packages::Nuget::MetadataExtractionService do ...@@ -47,6 +48,16 @@ RSpec.describe Packages::Nuget::MetadataExtractionService do
end end
end end
context 'with package types' do
let(:nuspec_filepath) { 'packages/nuget/with_package_types.nuspec' }
it { is_expected.to have_key(:package_types) }
it 'extracts package types' do
expect(subject[:package_types]).to include('SymbolsPackage')
end
end
context 'with a nuspec file with metadata' do context 'with a nuspec file with metadata' do
let(:nuspec_filepath) { 'packages/nuget/with_metadata.nuspec' } let(:nuspec_filepath) { 'packages/nuget/with_metadata.nuspec' }
......
...@@ -5,7 +5,7 @@ require 'spec_helper' ...@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Packages::Nuget::UpdatePackageFromMetadataService, :clean_gitlab_redis_shared_state do RSpec.describe Packages::Nuget::UpdatePackageFromMetadataService, :clean_gitlab_redis_shared_state do
include ExclusiveLeaseHelpers include ExclusiveLeaseHelpers
let(:package) { create(:nuget_package, :processing) } let(:package) { create(:nuget_package, :processing, :with_symbol_package) }
let(:package_file) { package.package_files.first } let(:package_file) { package.package_files.first }
let(:service) { described_class.new(package_file) } let(:service) { described_class.new(package_file) }
let(:package_name) { 'DummyProject.DummyPackage' } let(:package_name) { 'DummyProject.DummyPackage' }
...@@ -201,6 +201,41 @@ RSpec.describe Packages::Nuget::UpdatePackageFromMetadataService, :clean_gitlab_ ...@@ -201,6 +201,41 @@ RSpec.describe Packages::Nuget::UpdatePackageFromMetadataService, :clean_gitlab_
it_behaves_like 'raising an', ::Packages::Nuget::MetadataExtractionService::ExtractionError it_behaves_like 'raising an', ::Packages::Nuget::MetadataExtractionService::ExtractionError
end end
context 'with a symbol package' do
let(:package_file) { package.package_files.last }
let(:package_file_name) { 'dummyproject.dummypackage.1.0.0.snupkg' }
context 'with no existing package' do
let(:package_id) { package.id }
it_behaves_like 'raising an', ::Packages::Nuget::UpdatePackageFromMetadataService::InvalidMetadataError
end
context 'with existing package' do
let!(:existing_package) { create(:nuget_package, project: package.project, name: package_name, version: package_version) }
let(:package_id) { existing_package.id }
it 'link existing package and updates package file', :aggregate_failures do
expect(service).to receive(:try_obtain_lease).and_call_original
expect(::Packages::Nuget::SyncMetadatumService).not_to receive(:new)
expect(::Packages::UpdateTagsService).not_to receive(:new)
expect { subject }
.to change { ::Packages::Package.count }.by(-1)
.and change { Packages::Dependency.count }.by(0)
.and change { Packages::DependencyLink.count }.by(0)
.and change { Packages::Nuget::DependencyLinkMetadatum.count }.by(0)
.and change { ::Packages::Nuget::Metadatum.count }.by(0)
expect(package_file.reload.file_name).to eq(package_file_name)
expect(package_file.package).to eq(existing_package)
end
it_behaves_like 'taking the lease'
it_behaves_like 'not updating the package if the lease is taken'
end
end
context 'with an invalid package name' do context 'with an invalid package name' do
invalid_names = [ invalid_names = [
'', '',
......
...@@ -136,8 +136,8 @@ RSpec.shared_examples 'process nuget workhorse authorization' do |user_type, sta ...@@ -136,8 +136,8 @@ RSpec.shared_examples 'process nuget workhorse authorization' do |user_type, sta
end end
end end
RSpec.shared_examples 'process nuget upload' do |user_type, status, add_member = true| RSpec.shared_examples 'process nuget upload' do |user_type, status, add_member = true, symbol_package = false|
RSpec.shared_examples 'creates nuget package files' do shared_examples 'creates nuget package files' do
it 'creates package files' do it 'creates package files' do
expect(::Packages::Nuget::ExtractionWorker).to receive(:perform_async).once expect(::Packages::Nuget::ExtractionWorker).to receive(:perform_async).once
expect { subject } expect { subject }
...@@ -146,7 +146,7 @@ RSpec.shared_examples 'process nuget upload' do |user_type, status, add_member = ...@@ -146,7 +146,7 @@ RSpec.shared_examples 'process nuget upload' do |user_type, status, add_member =
expect(response).to have_gitlab_http_status(status) expect(response).to have_gitlab_http_status(status)
package_file = target.packages.last.package_files.reload.last package_file = target.packages.last.package_files.reload.last
expect(package_file.file_name).to eq('package.nupkg') expect(package_file.file_name).to eq(file_name)
end end
end end
...@@ -169,7 +169,10 @@ RSpec.shared_examples 'process nuget upload' do |user_type, status, add_member = ...@@ -169,7 +169,10 @@ RSpec.shared_examples 'process nuget upload' do |user_type, status, add_member =
context 'with correct params' do context 'with correct params' do
it_behaves_like 'package workhorse uploads' it_behaves_like 'package workhorse uploads'
it_behaves_like 'creates nuget package files' it_behaves_like 'creates nuget package files'
it_behaves_like 'a package tracking event', 'API::NugetPackages', 'push_package'
unless symbol_package
it_behaves_like 'a package tracking event', 'API::NugetPackages', 'push_package'
end
end end
end end
...@@ -300,6 +303,16 @@ RSpec.shared_examples 'process nuget download content request' do |user_type, st ...@@ -300,6 +303,16 @@ RSpec.shared_examples 'process nuget download content request' do |user_type, st
it_behaves_like 'rejects nuget packages access', :anonymous, :not_found it_behaves_like 'rejects nuget packages access', :anonymous, :not_found
end end
context 'with symbol package' do
let(:format) { 'snupkg' }
it 'returns a valid package archive' do
subject
expect(response.media_type).to eq('application/octet-stream')
end
end
context 'with lower case package name' do context 'with lower case package name' do
let_it_be(:package_name) { 'dummy.package' } let_it_be(:package_name) { 'dummy.package' }
...@@ -407,3 +420,114 @@ RSpec.shared_examples 'rejects nuget access with unknown target id' do ...@@ -407,3 +420,114 @@ RSpec.shared_examples 'rejects nuget access with unknown target id' do
end end
end end
end end
RSpec.shared_examples 'nuget authorize upload endpoint' do
using RSpec::Parameterized::TableSyntax
context 'with valid project' do
where(:visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do
'PUBLIC' | :developer | true | true | 'process nuget workhorse authorization' | :success
'PUBLIC' | :guest | true | true | 'rejects nuget packages access' | :forbidden
'PUBLIC' | :developer | true | false | 'rejects nuget packages access' | :unauthorized
'PUBLIC' | :guest | true | false | 'rejects nuget packages access' | :unauthorized
'PUBLIC' | :developer | false | true | 'rejects nuget packages access' | :forbidden
'PUBLIC' | :guest | false | true | 'rejects nuget packages access' | :forbidden
'PUBLIC' | :developer | false | false | 'rejects nuget packages access' | :unauthorized
'PUBLIC' | :guest | false | false | 'rejects nuget packages access' | :unauthorized
'PUBLIC' | :anonymous | false | true | 'rejects nuget packages access' | :unauthorized
'PRIVATE' | :developer | true | true | 'process nuget workhorse authorization' | :success
'PRIVATE' | :guest | true | true | 'rejects nuget packages access' | :forbidden
'PRIVATE' | :developer | true | false | 'rejects nuget packages access' | :unauthorized
'PRIVATE' | :guest | true | false | 'rejects nuget packages access' | :unauthorized
'PRIVATE' | :developer | false | true | 'rejects nuget packages access' | :not_found
'PRIVATE' | :guest | false | true | 'rejects nuget packages access' | :not_found
'PRIVATE' | :developer | false | false | 'rejects nuget packages access' | :unauthorized
'PRIVATE' | :guest | false | false | 'rejects nuget packages access' | :unauthorized
'PRIVATE' | :anonymous | false | true | 'rejects nuget packages access' | :unauthorized
end
with_them do
let(:token) { user_token ? personal_access_token.token : 'wrong' }
let(:user_headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) }
let(:headers) { user_headers.merge(workhorse_headers) }
before do
update_visibility_to(Gitlab::VisibilityLevel.const_get(visibility_level, false))
end
it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member]
end
end
it_behaves_like 'deploy token for package uploads'
it_behaves_like 'job token for package uploads', authorize_endpoint: true do
let_it_be(:job) { create(:ci_build, :running, user: user, project: project) }
end
it_behaves_like 'rejects nuget access with unknown target id'
it_behaves_like 'rejects nuget access with invalid target id'
end
RSpec.shared_examples 'nuget upload endpoint' do |symbol_package: false|
using RSpec::Parameterized::TableSyntax
context 'with valid project' do
where(:visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do
'PUBLIC' | :developer | true | true | 'process nuget upload' | :created
'PUBLIC' | :guest | true | true | 'rejects nuget packages access' | :forbidden
'PUBLIC' | :developer | true | false | 'rejects nuget packages access' | :unauthorized
'PUBLIC' | :guest | true | false | 'rejects nuget packages access' | :unauthorized
'PUBLIC' | :developer | false | true | 'rejects nuget packages access' | :forbidden
'PUBLIC' | :guest | false | true | 'rejects nuget packages access' | :forbidden
'PUBLIC' | :developer | false | false | 'rejects nuget packages access' | :unauthorized
'PUBLIC' | :guest | false | false | 'rejects nuget packages access' | :unauthorized
'PUBLIC' | :anonymous | false | true | 'rejects nuget packages access' | :unauthorized
'PRIVATE' | :developer | true | true | 'process nuget upload' | :created
'PRIVATE' | :guest | true | true | 'rejects nuget packages access' | :forbidden
'PRIVATE' | :developer | true | false | 'rejects nuget packages access' | :unauthorized
'PRIVATE' | :guest | true | false | 'rejects nuget packages access' | :unauthorized
'PRIVATE' | :developer | false | true | 'rejects nuget packages access' | :not_found
'PRIVATE' | :guest | false | true | 'rejects nuget packages access' | :not_found
'PRIVATE' | :developer | false | false | 'rejects nuget packages access' | :unauthorized
'PRIVATE' | :guest | false | false | 'rejects nuget packages access' | :unauthorized
'PRIVATE' | :anonymous | false | true | 'rejects nuget packages access' | :unauthorized
end
with_them do
let(:token) { user_token ? personal_access_token.token : 'wrong' }
let(:user_headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) }
let(:headers) { user_headers.merge(workhorse_headers) }
let(:snowplow_gitlab_standard_context) { { project: project, user: user, namespace: project.namespace } }
before do
update_visibility_to(Gitlab::VisibilityLevel.const_get(visibility_level, false))
end
it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member], symbol_package
end
end
it_behaves_like 'deploy token for package uploads'
it_behaves_like 'job token for package uploads' do
let_it_be(:job) { create(:ci_build, :running, user: user, project: project) }
end
it_behaves_like 'rejects nuget access with unknown target id'
it_behaves_like 'rejects nuget access with invalid target id'
context 'file size above maximum limit' do
let(:headers) { basic_auth_header(deploy_token.username, deploy_token.token).merge(workhorse_headers) }
before do
allow_next_instance_of(UploadedFile) do |uploaded_file|
allow(uploaded_file).to receive(:size).and_return(project.actual_limits.nuget_max_file_size + 1)
end
end
it_behaves_like 'returning response status', :bad_request
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment