mirror of
https://gitlab.com/gitlab-org/gitlab-foss.git
synced 2025-08-16 15:57:22 +00:00
505 lines
18 KiB
Ruby
505 lines
18 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
# NuGet Package Manager Client API
|
|
#
|
|
# These API endpoints are not meant to be consumed directly by users. They are
|
|
# called by the NuGet package manager client when users run commands
|
|
# like `nuget install` or `nuget push`.
|
|
#
|
|
# This is the project level API.
|
|
module API
|
|
class NugetProjectPackages < ::API::Base
|
|
helpers ::API::Helpers::PackagesHelpers
|
|
helpers ::API::Helpers::Packages::BasicAuthHelpers
|
|
helpers ::API::Helpers::Packages::Nuget
|
|
include ::API::Helpers::Authentication
|
|
|
|
feature_category :package_registry
|
|
|
|
PACKAGE_FILENAME = 'package.nupkg'
|
|
SYMBOL_PACKAGE_FILENAME = 'package.snupkg'
|
|
API_KEY_HEADER = 'X-Nuget-Apikey'
|
|
|
|
default_format :json
|
|
|
|
rescue_from ArgumentError do |e|
|
|
render_api_error!(e.message, 400)
|
|
end
|
|
|
|
after_validation do
|
|
require_packages_enabled!
|
|
end
|
|
|
|
helpers do
|
|
include ::Gitlab::Utils::StrongMemoize
|
|
|
|
params :file_params do
|
|
requires :package, type: ::API::Validations::Types::WorkhorseFile,
|
|
desc: 'The package file to be published (generated by Multipart middleware)', documentation: { type: 'file' }
|
|
end
|
|
|
|
def project_or_group
|
|
authorized_user_project(action: :read_package)
|
|
end
|
|
|
|
def project_or_group_without_auth
|
|
find_project(params[:id]).presence || not_found!
|
|
end
|
|
strong_memoize_attr :project_or_group_without_auth
|
|
|
|
def symbol_server_enabled?
|
|
project_or_group_without_auth.namespace.package_settings.nuget_symbol_server_enabled
|
|
end
|
|
|
|
def snowplow_gitlab_standard_context
|
|
{ project: project_or_group, namespace: project_or_group.namespace }
|
|
end
|
|
|
|
def snowplow_gitlab_standard_context_without_auth
|
|
{ project: project_or_group_without_auth, namespace: project_or_group_without_auth.namespace }
|
|
end
|
|
|
|
def authorize_nuget_upload
|
|
project = project_or_group
|
|
authorize_workhorse!(
|
|
subject: project,
|
|
has_length: false,
|
|
maximum_size: project.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)
|
|
|
|
if project_or_group.actual_limits.exceeded?(:nuget_max_file_size, params[:package].size)
|
|
bad_request!('File is too large')
|
|
end
|
|
|
|
file_params = params.merge(
|
|
file: params[:package],
|
|
file_name: file_name(symbol_package),
|
|
build: current_authenticated_job
|
|
)
|
|
|
|
if !symbol_package && extracted_metadata.success?
|
|
# Create or update package on the fly if nuspec file is extracted successfully,
|
|
# otherwise fallback to the background job
|
|
create_or_update_package(file_params)
|
|
elsif symbol_package || extracted_metadata.cause.nuspec_extraction_failed?
|
|
create_temp_package_and_enqueue_worker(file_params, symbol_package)
|
|
else
|
|
render_api_error!(extracted_metadata.message, extracted_metadata.reason)
|
|
end
|
|
end
|
|
|
|
def create_or_update_package(file_params)
|
|
response = Packages::Nuget::CreateOrUpdatePackageService
|
|
.new(project_or_group, current_user, file_params.merge(nuspec_file_content: extracted_metadata.payload))
|
|
.execute
|
|
|
|
render_api_error!(response.message, response.reason) if response.error?
|
|
end
|
|
|
|
def create_temp_package_and_enqueue_worker(file_params, symbol_package)
|
|
response = ::Packages::Nuget::CreateTemporaryPackageService.new(
|
|
project: project_or_group,
|
|
user: current_user,
|
|
params: {
|
|
package_params: declared_params.merge(
|
|
build: current_authenticated_job,
|
|
name: temp_file_name(symbol_package)
|
|
),
|
|
package_file_params: file_params
|
|
}
|
|
).execute
|
|
|
|
render_api_error!(response.message, response.reason) if response.error?
|
|
end
|
|
|
|
def extracted_metadata
|
|
if params['package.remote_url'].present?
|
|
::Packages::Nuget::ExtractRemoteMetadataFileService.new(params['package.remote_url']).execute
|
|
else # file on disk
|
|
Zip::InputStream.open(params[:package]) do |zip|
|
|
::Packages::Nuget::ExtractMetadataFileService.new(zip).execute
|
|
end
|
|
end
|
|
rescue ::Packages::Nuget::ExtractMetadataFileService::ExtractionError => e
|
|
ServiceResponse.error(message: e.message, reason: :bad_request)
|
|
end
|
|
strong_memoize_attr :extracted_metadata
|
|
|
|
def publish_package(symbol_package: false)
|
|
upload_nuget_package_file(symbol_package: symbol_package)
|
|
|
|
track_package_event(
|
|
symbol_package ? 'push_symbol_package' : 'push_package',
|
|
:nuget,
|
|
**track_package_event_attrs
|
|
)
|
|
created!
|
|
rescue ObjectStorage::RemoteStoreError => e
|
|
Gitlab::ErrorTracking.track_exception(
|
|
e,
|
|
extra: { file_name: params[:file_name], project_id: project_or_group.id }
|
|
)
|
|
|
|
forbidden!
|
|
end
|
|
|
|
def required_permission
|
|
:read_package
|
|
end
|
|
|
|
def format_filename(package)
|
|
return "#{params[:package_filename]}.#{params[:format]}" if package.version == params[:package_version]
|
|
|
|
return unless package.normalized_nuget_version == params[:package_version]
|
|
|
|
"#{params[:package_filename].sub(params[:package_version], package.version)}.#{params[:format]}"
|
|
end
|
|
|
|
def present_odata_entry
|
|
project = find_project(params[:project_id])
|
|
|
|
not_found! unless project
|
|
|
|
env['api.format'] = :binary
|
|
content_type 'application/xml; charset=utf-8'
|
|
|
|
odata_entry = ::Packages::Nuget::OdataPackageEntryService
|
|
.new(project, declared_params)
|
|
.execute
|
|
.payload
|
|
|
|
present odata_entry
|
|
end
|
|
|
|
def track_package_event_attrs
|
|
attrs = {
|
|
category: 'API::NugetPackages',
|
|
project: project_or_group,
|
|
namespace: project_or_group.namespace
|
|
}
|
|
attrs[:feed] = 'v2' if request.path.include?('nuget/v2')
|
|
attrs
|
|
end
|
|
end
|
|
|
|
params do
|
|
requires :id, types: [String, Integer], desc: 'The ID or URL-encoded path of the project',
|
|
regexp: ::API::Concerns::Packages::Nuget::PrivateEndpoints::POSITIVE_INTEGER_REGEX
|
|
end
|
|
resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
|
|
namespace ':id/packages' do
|
|
namespace '/nuget' do
|
|
include ::API::Concerns::Packages::Nuget::PublicEndpoints
|
|
end
|
|
|
|
authenticate_with do |accept|
|
|
accept.token_types(
|
|
:personal_access_token_with_username,
|
|
:deploy_token_with_username,
|
|
:job_token_with_username
|
|
).sent_through(:http_basic_auth)
|
|
end
|
|
|
|
namespace '/nuget' do
|
|
include ::API::Concerns::Packages::Nuget::PrivateEndpoints
|
|
|
|
# https://docs.microsoft.com/en-us/nuget/api/package-base-address-resource
|
|
params do
|
|
requires :package_name, type: String, desc: 'The NuGet package name', regexp: API::NO_SLASH_URL_PART_REGEX,
|
|
documentation: { example: 'mynugetpkg.1.3.0.17.nupkg' }
|
|
end
|
|
namespace '/download/*package_name' do
|
|
after_validation do
|
|
authorize_read_package!(project_or_group)
|
|
end
|
|
|
|
desc 'The NuGet Content Service - index request' do
|
|
detail 'This feature was introduced in GitLab 12.8'
|
|
success code: 200, model: ::API::Entities::Nuget::PackagesVersions
|
|
failure [
|
|
{ code: 401, message: 'Unauthorized' },
|
|
{ code: 403, message: 'Forbidden' },
|
|
{ code: 404, message: 'Not Found' }
|
|
]
|
|
tags %w[nuget_packages]
|
|
end
|
|
get 'index', format: :json, urgency: :low do
|
|
present ::Packages::Nuget::PackagesVersionsPresenter.new(find_packages),
|
|
with: ::API::Entities::Nuget::PackagesVersions
|
|
end
|
|
|
|
desc 'The NuGet Content Service - content request' do
|
|
detail 'This feature was introduced in GitLab 12.8'
|
|
success code: 200
|
|
failure [
|
|
{ code: 401, message: 'Unauthorized' },
|
|
{ code: 403, message: 'Forbidden' },
|
|
{ code: 404, message: 'Not Found' }
|
|
]
|
|
tags %w[nuget_packages]
|
|
end
|
|
params do
|
|
requires :package_version, type: String, desc: 'The NuGet package version',
|
|
regexp: API::NO_SLASH_URL_PART_REGEX, documentation: { example: '1.3.0.17' }
|
|
requires :package_filename, type: String, desc: 'The NuGet package filename',
|
|
regexp: API::NO_SLASH_URL_PART_REGEX, documentation: { example: 'mynugetpkg.1.3.0.17.nupkg' }
|
|
end
|
|
get '*package_version/*package_filename', format: [:nupkg, :snupkg], urgency: :low do
|
|
package = find_package
|
|
filename = format_filename(package)
|
|
package_file = ::Packages::PackageFileFinder.new(package, filename, with_file_name_like: true)
|
|
.execute
|
|
|
|
not_found!('Package') unless package_file
|
|
|
|
track_package_event(
|
|
params[:format] == 'snupkg' ? 'pull_symbol_package' : 'pull_package',
|
|
:nuget,
|
|
**track_package_event_attrs
|
|
)
|
|
|
|
# nuget and dotnet don't support 302 Moved status codes, supports_direct_download has to be set to false
|
|
present_package_file!(package_file, supports_direct_download: false)
|
|
end
|
|
end
|
|
end
|
|
|
|
# To support an additional authentication option for publish/delete endpoints,
|
|
# we redefine the `authenticate_with` method by combining the previous
|
|
# authentication option with the new one.
|
|
authenticate_with do |accept|
|
|
accept.token_types(
|
|
:personal_access_token_with_username,
|
|
:deploy_token_with_username,
|
|
:job_token_with_username
|
|
).sent_through(:http_basic_auth)
|
|
accept.token_types(:personal_access_token, :deploy_token, :job_token)
|
|
.sent_through(http_header: API_KEY_HEADER)
|
|
end
|
|
|
|
namespace '/nuget' do
|
|
# https://docs.microsoft.com/en-us/nuget/api/package-publish-resource
|
|
desc 'The NuGet V3 Feed Package Publish endpoint' do
|
|
detail 'This feature was introduced in GitLab 12.6'
|
|
success code: 201
|
|
failure [
|
|
{ code: 400, message: 'Bad Request' },
|
|
{ code: 401, message: 'Unauthorized' },
|
|
{ code: 403, message: 'Forbidden' },
|
|
{ code: 404, message: 'Not Found' }
|
|
]
|
|
tags %w[nuget_packages]
|
|
end
|
|
|
|
params do
|
|
use :file_params
|
|
end
|
|
put urgency: :low do
|
|
publish_package
|
|
end
|
|
|
|
desc 'The NuGet Package Authorize endpoint' do
|
|
detail 'This feature was introduced in GitLab 14.1'
|
|
success code: 200
|
|
failure [
|
|
{ code: 401, message: 'Unauthorized' },
|
|
{ code: 403, message: 'Forbidden' },
|
|
{ code: 404, message: 'Not Found' }
|
|
]
|
|
tags %w[nuget_packages]
|
|
end
|
|
put 'authorize', urgency: :low do
|
|
authorize_nuget_upload
|
|
end
|
|
|
|
# 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'
|
|
success code: 201
|
|
failure [
|
|
{ code: 400, message: 'Bad Request' },
|
|
{ code: 401, message: 'Unauthorized' },
|
|
{ code: 403, message: 'Forbidden' },
|
|
{ code: 404, message: 'Not Found' }
|
|
]
|
|
tags %w[nuget_packages]
|
|
end
|
|
params do
|
|
use :file_params
|
|
end
|
|
put 'symbolpackage', urgency: :low do
|
|
publish_package(symbol_package: true)
|
|
end
|
|
|
|
desc 'The NuGet Symbol Package Authorize endpoint' do
|
|
detail 'This feature was introduced in GitLab 14.1'
|
|
success code: 200
|
|
failure [
|
|
{ code: 401, message: 'Unauthorized' },
|
|
{ code: 403, message: 'Forbidden' },
|
|
{ code: 404, message: 'Not Found' }
|
|
]
|
|
tags %w[nuget_packages]
|
|
end
|
|
put 'symbolpackage/authorize', urgency: :low do
|
|
authorize_nuget_upload
|
|
end
|
|
|
|
desc 'The NuGet Package Delete endpoint' do
|
|
detail 'This feature was introduced in GitLab 16.5'
|
|
success code: 204
|
|
failure [
|
|
{ code: 401, message: 'Unauthorized' },
|
|
{ code: 403, message: 'Forbidden' },
|
|
{ code: 404, message: 'Not Found' }
|
|
]
|
|
tags %w[nuget_packages]
|
|
end
|
|
params do
|
|
requires :package_name, type: String, allow_blank: false, desc: 'The NuGet package name',
|
|
regexp: Gitlab::Regex.nuget_package_name_regex, documentation: { example: 'mynugetpkg' }
|
|
requires :package_version, type: String, allow_blank: false, desc: 'The NuGet package version',
|
|
regexp: Gitlab::Regex.nuget_version_regex, documentation: { example: '1.0.1' }
|
|
end
|
|
delete '*package_name/*package_version', format: false, urgency: :low do
|
|
authorize_destroy_package!(project_or_group)
|
|
|
|
destroy_conditionally!(find_package) do |package|
|
|
::Packages::MarkPackageForDestructionService.new(container: package, current_user: current_user).execute
|
|
|
|
track_package_event(
|
|
'delete_package',
|
|
:nuget,
|
|
category: 'API::NugetPackages',
|
|
project: package.project,
|
|
namespace: package.project.namespace
|
|
)
|
|
end
|
|
end
|
|
|
|
namespace '/v2' do
|
|
desc 'The NuGet V2 Feed Package Publish endpoint' do
|
|
detail 'This feature was introduced in GitLab 16.2'
|
|
success code: 201
|
|
failure [
|
|
{ code: 400, message: 'Bad Request' },
|
|
{ code: 401, message: 'Unauthorized' },
|
|
{ code: 403, message: 'Forbidden' },
|
|
{ code: 404, message: 'Not Found' }
|
|
]
|
|
tags %w[nuget_packages]
|
|
end
|
|
|
|
params do
|
|
use :file_params
|
|
end
|
|
put do
|
|
publish_package
|
|
end
|
|
|
|
desc 'The NuGet V2 Feed Package Authorize endpoint' do
|
|
detail 'This feature was introduced in GitLab 16.2'
|
|
success code: 200
|
|
failure [
|
|
{ code: 401, message: 'Unauthorized' },
|
|
{ code: 403, message: 'Forbidden' },
|
|
{ code: 404, message: 'Not Found' }
|
|
]
|
|
tags %w[nuget_packages]
|
|
end
|
|
|
|
put 'authorize', urgency: :low do
|
|
authorize_nuget_upload
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
params do
|
|
requires :project_id, types: [String, Integer], desc: 'The ID or URL-encoded path of the project',
|
|
regexp: ::API::Concerns::Packages::Nuget::PrivateEndpoints::POSITIVE_INTEGER_REGEX
|
|
end
|
|
resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
|
|
namespace ':project_id/packages/nuget/v2' do
|
|
# https://joelverhagen.github.io/NuGetUndocs/?http#endpoint-find-packages-by-id
|
|
desc 'The NuGet V2 Feed Find Packages by ID endpoint' do
|
|
detail 'This feature was introduced in GitLab 16.4'
|
|
success code: 200
|
|
failure [
|
|
{ code: 404, message: 'Not Found' },
|
|
{ code: 400, message: 'Bad Request' }
|
|
]
|
|
tags %w[nuget_packages]
|
|
end
|
|
|
|
params do
|
|
requires :id, as: :package_name, type: String, allow_blank: false, coerce_with: ->(val) { val.delete("'") },
|
|
desc: 'The NuGet package name', regexp: Gitlab::Regex.nuget_package_name_regex,
|
|
documentation: { example: 'mynugetpkg' }
|
|
end
|
|
get 'FindPackagesById\(\)', urgency: :low do
|
|
present_odata_entry
|
|
end
|
|
|
|
# https://joelverhagen.github.io/NuGetUndocs/?http#endpoint-enumerate-packages
|
|
desc 'The NuGet V2 Feed Enumerate Packages endpoint' do
|
|
detail 'This feature was introduced in GitLab 16.4'
|
|
success code: 200
|
|
failure [
|
|
{ code: 404, message: 'Not Found' },
|
|
{ code: 400, message: 'Bad Request' }
|
|
]
|
|
tags %w[nuget_packages]
|
|
end
|
|
|
|
params do
|
|
requires :$filter, as: :package_name, type: String, allow_blank: false,
|
|
coerce_with: ->(val) { val.match(/tolower\(Id\) eq '(.+?)'/)&.captures&.first },
|
|
desc: 'The NuGet package name', regexp: Gitlab::Regex.nuget_package_name_regex,
|
|
documentation: { example: 'mynugetpkg' }
|
|
end
|
|
get 'Packages\(\)', urgency: :low do
|
|
present_odata_entry
|
|
end
|
|
|
|
# https://joelverhagen.github.io/NuGetUndocs/?http#endpoint-get-a-single-package
|
|
desc 'The NuGet V2 Feed Single Package Metadata endpoint' do
|
|
detail 'This feature was introduced in GitLab 16.4'
|
|
success code: 200
|
|
failure [
|
|
{ code: 404, message: 'Not Found' },
|
|
{ code: 400, message: 'Bad Request' }
|
|
]
|
|
tags %w[nuget_packages]
|
|
end
|
|
params do
|
|
requires :package_name, type: String, allow_blank: false, desc: 'The NuGet package name',
|
|
regexp: Gitlab::Regex.nuget_package_name_regex, documentation: { example: 'mynugetpkg' }
|
|
requires :package_version, type: String, allow_blank: false, desc: 'The NuGet package version',
|
|
regexp: Gitlab::Regex.nuget_version_regex, documentation: { example: '1.3.0.17' }
|
|
end
|
|
get 'Packages\(Id=\'*package_name\',Version=\'*package_version\'\)', urgency: :low do
|
|
present_odata_entry
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|