452 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			Ruby
		
	
	
	
			
		
		
	
	
			452 lines
		
	
	
		
			17 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)
 | 
						|
        project = project_or_group
 | 
						|
        authorize_upload!(project)
 | 
						|
        bad_request!('File is too large') if project.actual_limits.exceeded?(:nuget_max_file_size, params[:package].size)
 | 
						|
 | 
						|
        file_params = params.merge(
 | 
						|
          file: params[:package],
 | 
						|
          file_name: file_name(symbol_package)
 | 
						|
        )
 | 
						|
 | 
						|
        check_duplicate(file_params, symbol_package)
 | 
						|
 | 
						|
        package = ::Packages::CreateTemporaryPackageService.new(
 | 
						|
          project, 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
 | 
						|
 | 
						|
      def check_duplicate(file_params, symbol_package)
 | 
						|
        return if symbol_package
 | 
						|
 | 
						|
        service_params = file_params.merge(remote_url: params['package.remote_url'])
 | 
						|
        response = ::Packages::Nuget::CheckDuplicatesService.new(project_or_group, current_user, service_params).execute
 | 
						|
        render_api_error!(response.message, response.reason) if response.error?
 | 
						|
      end
 | 
						|
 | 
						|
      def publish_package(symbol_package: false)
 | 
						|
        upload_nuget_package_file(symbol_package: symbol_package) do |package|
 | 
						|
          track_package_event(
 | 
						|
            symbol_package ? 'push_symbol_package' : 'push_package',
 | 
						|
            :nuget,
 | 
						|
            **track_package_event_attrs(package.project)
 | 
						|
          )
 | 
						|
        end
 | 
						|
      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 "#{params[:package_filename].sub(params[:package_version], package.version)}.#{params[:format]}" if package.normalized_nuget_version == params[:package_version]
 | 
						|
      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(project)
 | 
						|
        attrs = {
 | 
						|
          category: 'API::NugetPackages',
 | 
						|
          project: project,
 | 
						|
          namespace: project.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(package.project)
 | 
						|
              )
 | 
						|
 | 
						|
              # 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
 |