Files
gitlab-foss/lib/api/nuget_project_packages.rb

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