From 0b3a1e151bdbdfab6159fa03fcbaf6f799db23c6 Mon Sep 17 00:00:00 2001
From: Moaz Khalifa <mkhalifa@gitlab.com>
Date: Mon, 12 Jun 2023 17:09:46 +0000
Subject: [PATCH] Support dotnet nuget api-key option

Dotnet users are now able to publish NuGet packages to the NuGet package repository using this command: dotnet nuget push **/*.nupkg --source https://gitlab.com/api/v4/projects/1/packages/nuget/index.json --api-key PAT_TOKEN

Changelog: added
---
 .../packages/nuget/service_index_presenter.rb |  21 +-
 doc/api/packages/nuget.md                     |   8 +-
 doc/user/packages/nuget_repository/index.md   |  17 ++
 .../packages/nuget/private_endpoints.rb       | 153 ++++++++++
 .../packages/nuget/public_endpoints.rb        |  42 +++
 lib/api/concerns/packages/nuget_endpoints.rb  | 159 ----------
 lib/api/nuget_group_packages.rb               |  39 ++-
 lib/api/nuget_project_packages.rb             | 280 ++++++++++--------
 .../api_authentication/token_locator.rb       |  44 ++-
 .../api_authentication/token_locator_spec.rb  |  23 +-
 .../requests/api/nuget_group_packages_spec.rb |   8 +-
 .../api/nuget_endpoints_shared_examples.rb    | 100 ++-----
 .../api/nuget_packages_shared_examples.rb     | 146 ++++++---
 13 files changed, 588 insertions(+), 452 deletions(-)
 create mode 100644 lib/api/concerns/packages/nuget/private_endpoints.rb
 create mode 100644 lib/api/concerns/packages/nuget/public_endpoints.rb
 delete mode 100644 lib/api/concerns/packages/nuget_endpoints.rb

diff --git a/app/presenters/packages/nuget/service_index_presenter.rb b/app/presenters/packages/nuget/service_index_presenter.rb
index 033a1845c1ce..b262735508ce 100644
--- a/app/presenters/packages/nuget/service_index_presenter.rb
+++ b/app/presenters/packages/nuget/service_index_presenter.rb
@@ -35,12 +35,13 @@ def version
       end
 
       def resources
-        available_services.map { |service| build_service(service) }
-                          .flatten
+        available_services.flat_map { |service| build_service(service) }
       end
 
       private
 
+      attr_reader :project_or_group
+
       def available_services
         case scope
         when :group
@@ -77,13 +78,13 @@ def build_service_url(service_type)
       end
 
       def scope
-        return :project if @project_or_group.is_a?(::Project)
-        return :group if @project_or_group.is_a?(::Group)
+        return :project if project_or_group.is_a?(::Project)
+        return :group if project_or_group.is_a?(::Group)
       end
 
       def download_service_url
         params = {
-          id: @project_or_group.id,
+          id: project_or_group.id,
           package_name: nil,
           package_version: nil,
           package_filename: nil
@@ -97,7 +98,7 @@ def download_service_url
 
       def metadata_service_url
         params = {
-          id: @project_or_group.id,
+          id: project_or_group.id,
           package_name: nil,
           package_version: nil
         }
@@ -119,18 +120,18 @@ def metadata_service_url
       def search_service_url
         case scope
         when :group
-          api_v4_groups___packages_nuget_query_path(id: @project_or_group.id)
+          api_v4_groups___packages_nuget_query_path(id: project_or_group.id)
         when :project
-          api_v4_projects_packages_nuget_query_path(id: @project_or_group.id)
+          api_v4_projects_packages_nuget_query_path(id: project_or_group.id)
         end
       end
 
       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
 
       def symbol_service_url
-        api_v4_projects_packages_nuget_symbolpackage_path(id: @project_or_group.id)
+        api_v4_projects_packages_nuget_symbolpackage_path(id: project_or_group.id)
       end
     end
   end
diff --git a/doc/api/packages/nuget.md b/doc/api/packages/nuget.md
index aa2b4586e9c7..afe384b5a29c 100644
--- a/doc/api/packages/nuget.md
+++ b/doc/api/packages/nuget.md
@@ -158,9 +158,11 @@ The examples in this document all use the project-level prefix.
 
 ## Service Index
 
-> Introduced in GitLab 12.6.
+> - Introduced in GitLab 12.6.
+> - [Changed](https://gitlab.com/gitlab-org/gitlab/-/issues/214674) to be public in GitLab 16.1.
 
-Returns a list of available API resources:
+Returns a list of available API resources.
+Authentication is not required:
 
 ```plaintext
 GET <route-prefix>/index
@@ -169,7 +171,7 @@ GET <route-prefix>/index
 Example Request:
 
 ```shell
-curl --user <username>:<personal_access_token> "https://gitlab.example.com/api/v4/projects/1/packages/nuget/index"
+curl "https://gitlab.example.com/api/v4/projects/1/packages/nuget/index"
 ```
 
 Example response:
diff --git a/doc/user/packages/nuget_repository/index.md b/doc/user/packages/nuget_repository/index.md
index c97ce9ec5933..ea9bfd7defb9 100644
--- a/doc/user/packages/nuget_repository/index.md
+++ b/doc/user/packages/nuget_repository/index.md
@@ -316,6 +316,8 @@ nuget push <package_file> -Source <source_name>
 
 ### Publish a package with the .NET CLI
 
+> Publishing a package with `--api-key` [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/214674) in GitLab 16.1.
+
 Prerequisites:
 
 - [A NuGet package created with .NET CLI](https://learn.microsoft.com/en-us/nuget/create-packages/creating-a-package-dotnet-cli).
@@ -336,6 +338,21 @@ For example:
 dotnet nuget push MyPackage.1.0.0.nupkg --source gitlab
 ```
 
+You can publish a package using the `--api-key` option instead of `username` and `password`:
+
+```shell
+dotnet nuget push <package_file> --source <source_url> --api-key <gitlab_personal_access_token, deploy_token or job token>
+```
+
+- `<package_file>` is your package filename, ending in `.nupkg`.
+- `<source_url>` is the URL of the NuGet Package Registry.
+
+For example:
+
+```shell
+dotnet nuget push MyPackage.1.0.0.nupkg --source https://gitlab.example.com/api/v4/projects/<your_project_id>/packages/nuget/index.json --api-key <gitlab_personal_access_token, deploy_token or job token>
+```
+
 ### Publish a NuGet package by using CI/CD
 
 > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/36424) in GitLab 13.3.
diff --git a/lib/api/concerns/packages/nuget/private_endpoints.rb b/lib/api/concerns/packages/nuget/private_endpoints.rb
new file mode 100644
index 000000000000..20c02f0a285b
--- /dev/null
+++ b/lib/api/concerns/packages/nuget/private_endpoints.rb
@@ -0,0 +1,153 @@
+# frozen_string_literal: true
+
+#
+# NuGet Package Manager Client API
+#
+# These API endpoints are not consumed directly by users, so there is no documentation for the
+# individual endpoints. They are called by the NuGet package manager client when users run commands
+# like `nuget install` or `nuget push`. The usage of the GitLab NuGet registry is documented here:
+# https://docs.gitlab.com/ee/user/packages/nuget_repository/
+#
+# Technical debt: https://gitlab.com/gitlab-org/gitlab/issues/35798
+module API
+  module Concerns
+    module Packages
+      module Nuget
+        module PrivateEndpoints
+          extend ActiveSupport::Concern
+
+          POSITIVE_INTEGER_REGEX = %r{\A[1-9]\d*\z}
+          NON_NEGATIVE_INTEGER_REGEX = %r{\A(0|[1-9]\d*)\z}
+
+          included do
+            helpers do
+              def find_packages(package_name)
+                packages = package_finder(package_name).execute
+
+                not_found!('Packages') unless packages.exists?
+
+                packages
+              end
+
+              def find_package(package_name, package_version)
+                package = package_finder(package_name, package_version).execute
+                                                                       .first
+
+                not_found!('Package') unless package
+
+                package
+              end
+
+              def package_finder(package_name, package_version = nil)
+                ::Packages::Nuget::PackageFinder.new(
+                  current_user,
+                  project_or_group,
+                  package_name: package_name,
+                  package_version: package_version
+                )
+              end
+
+              def search_packages(_search_term, search_options)
+                ::Packages::Nuget::SearchService
+                  .new(current_user, project_or_group, params[:q], search_options)
+                  .execute
+              end
+            end
+
+            # https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource
+            params do
+              requires :package_name, type: String, desc: 'The NuGet package name',
+                regexp: API::NO_SLASH_URL_PART_REGEX, documentation: { example: 'MyNuGetPkg' }
+            end
+            namespace '/metadata/*package_name' do
+              after_validation do
+                authorize_packages_access!(project_or_group, required_permission)
+              end
+
+              desc 'The NuGet Metadata Service - Package name level' do
+                detail 'This feature was introduced in GitLab 12.8'
+                success code: 200, model: ::API::Entities::Nuget::PackagesMetadata
+                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::PackagesMetadataPresenter.new(find_packages(params[:package_name])),
+                  with: ::API::Entities::Nuget::PackagesMetadata
+              end
+
+              desc 'The NuGet Metadata Service - Package name and version level' do
+                detail 'This feature was introduced in GitLab 12.8'
+                success code: 200, model: ::API::Entities::Nuget::PackageMetadata
+                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.0.0' }
+              end
+              get '*package_version', format: :json, urgency: :low do
+                present ::Packages::Nuget::PackageMetadataPresenter.new(
+                  find_package(params[:package_name],
+                    params[:package_version])
+                ),
+                  with: ::API::Entities::Nuget::PackageMetadata
+              end
+            end
+
+            # https://docs.microsoft.com/en-us/nuget/api/search-query-service-resource
+            params do
+              optional :q, type: String, desc: 'The search term', documentation: { example: 'MyNuGet' }
+              optional :skip, type: Integer, desc: 'The number of results to skip', default: 0,
+                regexp: NON_NEGATIVE_INTEGER_REGEX, documentation: { example: 1 }
+              optional :take, type: Integer, desc: 'The number of results to return',
+                default: Kaminari.config.default_per_page, regexp: POSITIVE_INTEGER_REGEX, documentation: { example: 1 }
+              optional :prerelease, type: ::Grape::API::Boolean, desc: 'Include prerelease versions', default: true
+            end
+            namespace '/query' do
+              after_validation do
+                authorize_packages_access!(project_or_group, required_permission)
+              end
+
+              desc 'The NuGet Search Service' do
+                detail 'This feature was introduced in GitLab 12.8'
+                success code: 200, model: ::API::Entities::Nuget::SearchResults
+                failure [
+                  { code: 401, message: 'Unauthorized' },
+                  { code: 403, message: 'Forbidden' },
+                  { code: 404, message: 'Not Found' }
+                ]
+                tags %w[nuget_packages]
+              end
+              get format: :json, urgency: :low do
+                search_options = {
+                  include_prerelease_versions: params[:prerelease],
+                  per_page: params[:take],
+                  padding: params[:skip]
+                }
+
+                results = search_packages(params[:q], search_options)
+
+                track_package_event(
+                  'search_package',
+                  :nuget,
+                  **snowplow_gitlab_standard_context.merge(category: 'API::NugetPackages')
+                )
+
+                present ::Packages::Nuget::SearchResultsPresenter.new(results),
+                  with: ::API::Entities::Nuget::SearchResults
+              end
+            end
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/lib/api/concerns/packages/nuget/public_endpoints.rb b/lib/api/concerns/packages/nuget/public_endpoints.rb
new file mode 100644
index 000000000000..37b503212d92
--- /dev/null
+++ b/lib/api/concerns/packages/nuget/public_endpoints.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+# NuGet Package Manager Client API
+
+# These API endpoints are not consumed directly by users, so there is no documentation for the
+# individual endpoints. They are called by the NuGet package manager client when users run commands
+# like `nuget install` or `nuget push`. The usage of the GitLab NuGet registry is documented here:
+# https://docs.gitlab.com/ee/user/packages/nuget_repository/
+
+module API
+  module Concerns
+    module Packages
+      module Nuget
+        module PublicEndpoints
+          extend ActiveSupport::Concern
+
+          included do
+            # https://docs.microsoft.com/en-us/nuget/api/service-index
+            desc 'The NuGet Service Index' do
+              detail 'This feature was introduced in GitLab 12.6'
+              success code: 200, model: ::API::Entities::Nuget::ServiceIndex
+              failure [
+                { code: 404, message: 'Not Found' }
+              ]
+              tags %w[nuget_packages]
+            end
+            get 'index', format: :json, urgency: :default do
+              track_package_event(
+                'cli_metadata',
+                :nuget,
+                **snowplow_gitlab_standard_context_without_auth.merge(category: 'API::NugetPackages')
+              )
+
+              present ::Packages::Nuget::ServiceIndexPresenter.new(project_or_group_without_auth),
+                with: ::API::Entities::Nuget::ServiceIndex
+            end
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/lib/api/concerns/packages/nuget_endpoints.rb b/lib/api/concerns/packages/nuget_endpoints.rb
deleted file mode 100644
index 5f32f0544f45..000000000000
--- a/lib/api/concerns/packages/nuget_endpoints.rb
+++ /dev/null
@@ -1,159 +0,0 @@
-# frozen_string_literal: true
-#
-# NuGet Package Manager Client API
-#
-# These API endpoints are not consumed directly by users, so there is no documentation for the
-# individual endpoints. They are called by the NuGet package manager client when users run commands
-# like `nuget install` or `nuget push`. The usage of the GitLab NuGet registry is documented here:
-# https://docs.gitlab.com/ee/user/packages/nuget_repository/
-#
-# Technical debt: https://gitlab.com/gitlab-org/gitlab/issues/35798
-module API
-  module Concerns
-    module Packages
-      module NugetEndpoints
-        extend ActiveSupport::Concern
-
-        POSITIVE_INTEGER_REGEX = %r{\A[1-9]\d*\z}.freeze
-        NON_NEGATIVE_INTEGER_REGEX = %r{\A(0|[1-9]\d*)\z}.freeze
-
-        included do
-          helpers do
-            def find_packages(package_name)
-              packages = package_finder(package_name).execute
-
-              not_found!('Packages') unless packages.exists?
-
-              packages
-            end
-
-            def find_package(package_name, package_version)
-              package = package_finder(package_name, package_version).execute
-                                                                     .first
-
-              not_found!('Package') unless package
-
-              package
-            end
-
-            def package_finder(package_name, package_version = nil)
-              ::Packages::Nuget::PackageFinder.new(
-                current_user,
-                project_or_group,
-                package_name: package_name,
-                package_version: package_version
-              )
-            end
-
-            def search_packages(search_term, search_options)
-              ::Packages::Nuget::SearchService
-                .new(current_user, project_or_group, params[:q], search_options)
-                .execute
-            end
-          end
-
-          # https://docs.microsoft.com/en-us/nuget/api/service-index
-          desc 'The NuGet Service Index' do
-            detail 'This feature was introduced in GitLab 12.6'
-            success code: 200, model: ::API::Entities::Nuget::ServiceIndex
-            failure [
-              { code: 401, message: 'Unauthorized' },
-              { code: 403, message: 'Forbidden' },
-              { code: 404, message: 'Not Found' }
-            ]
-            tags %w[nuget_packages]
-          end
-          get 'index', format: :json, urgency: :default do
-            authorize_packages_access!(project_or_group, required_permission)
-
-            track_package_event('cli_metadata', :nuget, **snowplow_gitlab_standard_context.merge(category: 'API::NugetPackages'))
-
-            present ::Packages::Nuget::ServiceIndexPresenter.new(project_or_group),
-                    with: ::API::Entities::Nuget::ServiceIndex
-          end
-
-          # https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource
-          params do
-            requires :package_name, type: String, desc: 'The NuGet package name', regexp: API::NO_SLASH_URL_PART_REGEX, documentation: { example: 'MyNuGetPkg' }
-          end
-          namespace '/metadata/*package_name' do
-            after_validation do
-              authorize_packages_access!(project_or_group, required_permission)
-            end
-
-            desc 'The NuGet Metadata Service - Package name level' do
-              detail 'This feature was introduced in GitLab 12.8'
-              success code: 200, model: ::API::Entities::Nuget::PackagesMetadata
-              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::PackagesMetadataPresenter.new(find_packages(params[:package_name])),
-                      with: ::API::Entities::Nuget::PackagesMetadata
-            end
-
-            desc 'The NuGet Metadata Service - Package name and version level' do
-              detail 'This feature was introduced in GitLab 12.8'
-              success code: 200, model: ::API::Entities::Nuget::PackageMetadata
-              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.0.0' }
-            end
-            get '*package_version', format: :json, urgency: :low do
-              present ::Packages::Nuget::PackageMetadataPresenter.new(find_package(params[:package_name], params[:package_version])),
-                      with: ::API::Entities::Nuget::PackageMetadata
-            end
-          end
-
-          # https://docs.microsoft.com/en-us/nuget/api/search-query-service-resource
-          params do
-            optional :q, type: String, desc: 'The search term', documentation: { example: 'MyNuGet' }
-            optional :skip, type: Integer, desc: 'The number of results to skip', default: 0, regexp: NON_NEGATIVE_INTEGER_REGEX, documentation: { example: 1 }
-            optional :take, type: Integer, desc: 'The number of results to return', default: Kaminari.config.default_per_page, regexp: POSITIVE_INTEGER_REGEX, documentation: { example: 1 }
-            optional :prerelease, type: ::Grape::API::Boolean, desc: 'Include prerelease versions', default: true
-          end
-          namespace '/query' do
-            after_validation do
-              authorize_packages_access!(project_or_group, required_permission)
-            end
-
-            desc 'The NuGet Search Service' do
-              detail 'This feature was introduced in GitLab 12.8'
-              success code: 200, model: ::API::Entities::Nuget::SearchResults
-              failure [
-                { code: 401, message: 'Unauthorized' },
-                { code: 403, message: 'Forbidden' },
-                { code: 404, message: 'Not Found' }
-              ]
-              tags %w[nuget_packages]
-            end
-            get format: :json, urgency: :low do
-              search_options = {
-                include_prerelease_versions: params[:prerelease],
-                per_page: params[:take],
-                padding: params[:skip]
-              }
-
-              results = search_packages(params[:q], search_options)
-
-              track_package_event('search_package', :nuget, **snowplow_gitlab_standard_context.merge(category: 'API::NugetPackages'))
-
-              present ::Packages::Nuget::SearchResultsPresenter.new(results),
-                      with: ::API::Entities::Nuget::SearchResults
-            end
-          end
-        end
-      end
-    end
-  end
-end
diff --git a/lib/api/nuget_group_packages.rb b/lib/api/nuget_group_packages.rb
index 2afcb915b06e..229032f7a5ae 100644
--- a/lib/api/nuget_group_packages.rb
+++ b/lib/api/nuget_group_packages.rb
@@ -17,11 +17,6 @@ class NugetGroupPackages < ::API::Base
 
     default_format :json
 
-    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
-
     rescue_from ArgumentError do |e|
       render_api_error!(e.message, 400)
     end
@@ -31,10 +26,17 @@ class NugetGroupPackages < ::API::Base
     end
 
     helpers do
+      include ::Gitlab::Utils::StrongMemoize
+
       def project_or_group
         find_authorized_group!
       end
 
+      def project_or_group_without_auth
+        find_group(params[:id]).presence || not_found!
+      end
+      strong_memoize_attr :project_or_group_without_auth
+
       def require_authenticated!
         unauthorized! unless current_user
       end
@@ -43,23 +45,38 @@ def snowplow_gitlab_standard_context
         { namespace: find_authorized_group! }
       end
 
+      def snowplow_gitlab_standard_context_without_auth
+        { namespace: project_or_group_without_auth }
+      end
+
       def required_permission
         :read_group
       end
     end
 
     params do
-      requires :id, types: [Integer, String], desc: 'The group ID or full group path.', regexp: ::API::Concerns::Packages::NugetEndpoints::POSITIVE_INTEGER_REGEX
+      requires :id, types: [Integer, String], desc: 'The group ID or full group path.', regexp: ::API::Concerns::Packages::Nuget::PrivateEndpoints::POSITIVE_INTEGER_REGEX
     end
 
     resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
-      namespace ':id/-/packages/nuget' do
-        after_validation do
-          # This API can't be accessed anonymously
-          require_authenticated!
+      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
 
-        include ::API::Concerns::Packages::NugetEndpoints
+        namespace '/nuget' do
+          after_validation do
+            # This API can't be accessed anonymously
+            require_authenticated!
+          end
+
+          include ::API::Concerns::Packages::Nuget::PrivateEndpoints
+        end
       end
     end
   end
diff --git a/lib/api/nuget_project_packages.rb b/lib/api/nuget_project_packages.rb
index cd16aaf6b5f0..2716d6f0b64a 100644
--- a/lib/api/nuget_project_packages.rb
+++ b/lib/api/nuget_project_packages.rb
@@ -17,14 +17,10 @@ class NugetProjectPackages < ::API::Base
 
     PACKAGE_FILENAME = 'package.nupkg'
     SYMBOL_PACKAGE_FILENAME = 'package.snupkg'
+    API_KEY_HEADER = 'X-Nuget-Apikey'
 
     default_format :json
 
-    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
-
     rescue_from ArgumentError do |e|
       render_api_error!(e.message, 400)
     end
@@ -34,6 +30,8 @@ class NugetProjectPackages < ::API::Base
     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
@@ -42,10 +40,19 @@ 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 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!(
@@ -97,115 +104,127 @@ def required_permission
     end
 
     params do
-      requires :id, types: [String, Integer], desc: 'The ID or URL-encoded path of the project', regexp: ::API::Concerns::Packages::NugetEndpoints::POSITIVE_INTEGER_REGEX
+      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/nuget' do
-        include ::API::Concerns::Packages::NugetEndpoints
-
-        # https://docs.microsoft.com/en-us/nuget/api/package-publish-resource
-        desc 'The NuGet 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]
+      namespace ':id/packages' do
+        namespace '/nuget' do
+          include ::API::Concerns::Packages::Nuget::PublicEndpoints
         end
 
-        params do
-          use :file_params
+        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
-        put urgency: :low do
-          upload_nuget_package_file do |package|
-            track_package_event(
-              'push_package',
-              :nuget,
-              category: 'API::NugetPackages',
-              project: package.project,
-              namespace: package.project.namespace
-            )
-          end
-        rescue ObjectStorage::RemoteStoreError => e
-          Gitlab::ErrorTracking.track_exception(e, extra: { file_name: params[:file_name], project_id: project_or_group.id })
 
-          forbidden!
-        end
+        namespace '/nuget' do
+          include ::API::Concerns::Packages::Nuget::PrivateEndpoints
 
-        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
-          upload_nuget_package_file(symbol_package: true) do |package|
-            track_package_event(
-              'push_symbol_package',
-              :nuget,
-              category: 'API::NugetPackages',
-              project: package.project,
-              namespace: package.project.namespace
-            )
+          # 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(params[:package_name])),
+                      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
+              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)
+                                                          .execute
+
+              not_found!('Package') unless package_file
+
+              track_package_event(
+                params[:format] == 'snupkg' ? 'pull_symbol_package' : 'pull_package',
+                :nuget,
+                category: 'API::NugetPackages',
+                project: package_file.project,
+                namespace: package_file.project.namespace
+              )
+
+              # 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
-        rescue ObjectStorage::RemoteStoreError => e
-          Gitlab::ErrorTracking.track_exception(e, extra: { file_name: params[:file_name], project_id: project_or_group.id })
-
-          forbidden!
         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
+        # To support an additional authentication option for download 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
 
-        # 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)
+        namespace '/nuget' do
+          # https://docs.microsoft.com/en-us/nuget/api/package-publish-resource
+          desc 'The NuGet 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
+            upload_nuget_package_file do |package|
+              track_package_event(
+                'push_package',
+                :nuget,
+                category: 'API::NugetPackages',
+                project: package.project,
+                namespace: package.project.namespace
+              )
+            end
+          rescue ObjectStorage::RemoteStoreError => e
+            Gitlab::ErrorTracking.track_exception(e, extra: { file_name: params[:file_name], project_id: project_or_group.id })
+
+            forbidden!
           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
+          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' },
@@ -213,15 +232,16 @@ def required_permission
             ]
             tags %w[nuget_packages]
           end
-          get 'index', format: :json, urgency: :low do
-            present ::Packages::Nuget::PackagesVersionsPresenter.new(find_packages(params[:package_name])),
-                    with: ::API::Entities::Nuget::PackagesVersions
+          put 'authorize', urgency: :low do
+            authorize_nuget_upload
           end
 
-          desc 'The NuGet Content Service - content request' do
-            detail 'This feature was introduced in GitLab 12.8'
-            success code: 200
+          # 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' }
@@ -229,26 +249,36 @@ def required_permission
             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' }
+            use :file_params
+          end
+          put 'symbolpackage', urgency: :low do
+            upload_nuget_package_file(symbol_package: true) do |package|
+              track_package_event(
+                'push_symbol_package',
+                :nuget,
+                category: 'API::NugetPackages',
+                project: package.project,
+                namespace: package.project.namespace
+              )
+            end
+          rescue ObjectStorage::RemoteStoreError => e
+            Gitlab::ErrorTracking.track_exception(e, extra: { file_name: params[:file_name], project_id: project_or_group.id })
+
+            forbidden!
+          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
-          get '*package_version/*package_filename', format: [:nupkg, :snupkg], urgency: :low do
-            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)
-                                                        .execute
-
-            not_found!('Package') unless package_file
-
-            track_package_event(
-              params[:format] == 'snupkg' ? 'pull_symbol_package' : 'pull_package',
-              :nuget,
-              category: 'API::NugetPackages',
-              project: package_file.project,
-              namespace: package_file.project.namespace
-            )
-
-            # 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)
+          put 'symbolpackage/authorize', urgency: :low do
+            authorize_nuget_upload
           end
         end
       end
diff --git a/lib/gitlab/api_authentication/token_locator.rb b/lib/gitlab/api_authentication/token_locator.rb
index df342905d2ef..5656ea0d1201 100644
--- a/lib/gitlab/api_authentication/token_locator.rb
+++ b/lib/gitlab/api_authentication/token_locator.rb
@@ -8,22 +8,23 @@ class TokenLocator
       include ActiveModel::Validations
       include ActionController::HttpAuthentication::Basic
 
+      VALID_LOCATIONS = %i[
+        http_basic_auth
+        http_token
+        http_bearer_token
+        http_deploy_token_header
+        http_job_token_header
+        http_private_token_header
+        http_header
+        token_param
+      ].freeze
+
       attr_reader :location
 
-      validates :location, inclusion: {
-        in: %i[
-          http_basic_auth
-          http_token
-          http_bearer_token
-          http_deploy_token_header
-          http_job_token_header
-          http_private_token_header
-          token_param
-        ]
-      }
+      validates :location, inclusion: { in: VALID_LOCATIONS }
 
       def initialize(location)
-        @location = location
+        @location = extract_location(location)
         validate!
       end
 
@@ -41,6 +42,8 @@ def extract(request)
           extract_from_http_job_token_header request
         when :http_private_token_header
           extract_from_http_private_token_header request
+        when :http_header
+          extract_from_http_header request
         when :token_param
           extract_from_token_param request
         end
@@ -48,6 +51,16 @@ def extract(request)
 
       private
 
+      def extract_location(location)
+        case location
+        when Symbol
+          location
+        when Hash
+          result, @token_identifier = location.detect { |k, _v| VALID_LOCATIONS.include?(k) }
+          result
+        end
+      end
+
       def extract_from_http_basic_auth(request)
         username, password = user_name_and_password(request)
         return unless username.present? && password.present?
@@ -96,6 +109,13 @@ def extract_from_token_param(request)
 
         UsernameAndPassword.new(nil, password)
       end
+
+      def extract_from_http_header(request)
+        password = request.headers[@token_identifier]
+        return unless password.present?
+
+        UsernameAndPassword.new(nil, password)
+      end
     end
   end
 end
diff --git a/spec/lib/gitlab/api_authentication/token_locator_spec.rb b/spec/lib/gitlab/api_authentication/token_locator_spec.rb
index 4b19a3d5846d..9b33d4439601 100644
--- a/spec/lib/gitlab/api_authentication/token_locator_spec.rb
+++ b/spec/lib/gitlab/api_authentication/token_locator_spec.rb
@@ -2,7 +2,7 @@
 
 require 'spec_helper'
 
-RSpec.describe Gitlab::APIAuthentication::TokenLocator do
+RSpec.describe Gitlab::APIAuthentication::TokenLocator, feature_category: :system_access do
   let_it_be(:user) { create(:user) }
   let_it_be(:project, reload: true) { create(:project, :public) }
   let_it_be(:personal_access_token) { create(:personal_access_token, user: user) }
@@ -157,6 +157,27 @@
       end
     end
 
+    context 'with :http_header' do
+      let(:type) { { http_header: 'Api-Key' } }
+
+      context 'without credentials' do
+        let(:request) { double(headers: {}) }
+
+        it 'returns nil' do
+          expect(subject).to be(nil)
+        end
+      end
+
+      context 'with credentials' do
+        let(:password) { 'bar' }
+        let(:request) { double(headers: { 'Api-Key' => password }) }
+
+        it 'returns the credentials' do
+          expect(subject.password).to eq(password)
+        end
+      end
+    end
+
     context 'with :token_param' do
       let(:type) { :token_param }
 
diff --git a/spec/requests/api/nuget_group_packages_spec.rb b/spec/requests/api/nuget_group_packages_spec.rb
index facbc01220dd..07199119cb58 100644
--- a/spec/requests/api/nuget_group_packages_spec.rb
+++ b/spec/requests/api/nuget_group_packages_spec.rb
@@ -26,13 +26,7 @@ def snowplow_context(user_role: :developer)
 
   shared_examples 'handling all endpoints' do
     describe 'GET /api/v4/groups/:id/-/packages/nuget' do
-      it_behaves_like 'handling nuget service requests',
-                      example_names_with_status: {
-                        anonymous_requests_example_name: 'rejects nuget packages access',
-                        anonymous_requests_status: :unauthorized,
-                        guest_requests_example_name: 'process nuget service index request',
-                        guest_requests_status: :success
-                      } do
+      it_behaves_like 'handling nuget service requests' do
         let(:url) { "/groups/#{target.id}/-/packages/nuget/index.json" }
       end
     end
diff --git a/spec/support/shared_examples/requests/api/nuget_endpoints_shared_examples.rb b/spec/support/shared_examples/requests/api/nuget_endpoints_shared_examples.rb
index 7cafe8bb3680..432e67ee21e7 100644
--- a/spec/support/shared_examples/requests/api/nuget_endpoints_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/nuget_endpoints_shared_examples.rb
@@ -1,100 +1,40 @@
 # frozen_string_literal: true
 
-RSpec.shared_examples 'handling nuget service requests' do |example_names_with_status: {}|
-  anonymous_requests_example_name = example_names_with_status.fetch(:anonymous_requests_example_name, 'process nuget service index request')
-  anonymous_requests_status = example_names_with_status.fetch(:anonymous_requests_status, :success)
-  guest_requests_example_name = example_names_with_status.fetch(:guest_requests_example_name, 'rejects nuget packages access')
-  guest_requests_status = example_names_with_status.fetch(:guest_requests_status, :forbidden)
-
+RSpec.shared_examples 'handling nuget service requests' do
   subject { get api(url) }
 
   context 'with valid target' do
     using RSpec::Parameterized::TableSyntax
 
-    context 'personal token' do
-      where(:visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do
-        'PUBLIC'  | :developer  | true  | true  | 'process nuget service index request' | :success
-        'PUBLIC'  | :guest      | true  | true  | 'process nuget service index request' | :success
-        'PUBLIC'  | :developer  | true  | false | 'rejects nuget packages access'       | :unauthorized
-        'PUBLIC'  | :guest      | true  | false | 'rejects nuget packages access'       | :unauthorized
-        'PUBLIC'  | :developer  | false | true  | 'process nuget service index request' | :success
-        'PUBLIC'  | :guest      | false | true  | 'process nuget service index request' | :success
-        'PUBLIC'  | :developer  | false | false | 'rejects nuget packages access'       | :unauthorized
-        'PUBLIC'  | :guest      | false | false | 'rejects nuget packages access'       | :unauthorized
-        'PUBLIC'  | :anonymous  | false | true  | anonymous_requests_example_name       | anonymous_requests_status
-        'PRIVATE' | :developer  | true  | true  | 'process nuget service index request' | :success
-        'PRIVATE' | :guest      | true  | true  | guest_requests_example_name           | guest_requests_status
-        '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(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) }
-        let(:snowplow_gitlab_standard_context) { snowplow_context(user_role: user_role) }
-
-        subject { get api(url), headers: 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
+    where(:visibility_level, :user_role, :member, :shared_examples_name, :expected_status) do
+      'PUBLIC'  | :developer  | true  | 'process nuget service index request' | :success
+      'PUBLIC'  | :guest      | true  | 'process nuget service index request' | :success
+      'PUBLIC'  | :developer  | false | 'process nuget service index request' | :success
+      'PUBLIC'  | :guest      | false | 'process nuget service index request' | :success
+      'PUBLIC'  | :anonymous  | false | 'process nuget service index request' | :success
+      'PRIVATE' | :developer  | true  | 'process nuget service index request' | :success
+      'PRIVATE' | :guest      | true  | 'process nuget service index request' | :success
+      'PRIVATE' | :developer  | false | 'process nuget service index request' | :success
+      'PRIVATE' | :guest      | false | 'process nuget service index request' | :success
+      'PRIVATE' | :anonymous  | false | 'process nuget service index request' | :success
     end
 
-    context 'with job token' do
-      where(:visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do
-        'PUBLIC'  | :developer  | true  | true  | 'process nuget service index request' | :success
-        'PUBLIC'  | :guest      | true  | true  | 'process nuget service index request' | :success
-        'PUBLIC'  | :developer  | true  | false | 'rejects nuget packages access'       | :unauthorized
-        'PUBLIC'  | :guest      | true  | false | 'rejects nuget packages access'       | :unauthorized
-        'PUBLIC'  | :developer  | false | true  | 'process nuget service index request' | :success
-        'PUBLIC'  | :guest      | false | true  | 'process nuget service index request' | :success
-        'PUBLIC'  | :developer  | false | false | 'rejects nuget packages access'       | :unauthorized
-        'PUBLIC'  | :guest      | false | false | 'rejects nuget packages access'       | :unauthorized
-        'PUBLIC'  | :anonymous  | false | true  | anonymous_requests_example_name       | anonymous_requests_status
-        'PRIVATE' | :developer  | true  | true  | 'process nuget service index request' | :success
-        'PRIVATE' | :guest      | true  | true  | guest_requests_example_name           | guest_requests_status
-        '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(:job) { user_token ? create(:ci_build, project: project, user: user, status: :running) : double(token: 'wrong') }
-        let(:headers) { user_role == :anonymous ? {} : job_basic_auth_header(job) }
-        let(:snowplow_gitlab_standard_context) { snowplow_context(user_role: user_role) }
-
-        subject { get api(url), headers: headers }
+    with_them do
+      let(:snowplow_gitlab_standard_context) { snowplow_context(user_role: :anonymous) }
 
-        before do
-          update_visibility_to(Gitlab::VisibilityLevel.const_get(visibility_level, false))
-        end
+      subject { get api(url) }
 
-        it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member]
+      before do
+        update_visibility_to(Gitlab::VisibilityLevel.const_get(visibility_level, false))
       end
-    end
-  end
 
-  it_behaves_like 'deploy token for package GET requests' do
-    before do
-      update_visibility_to(Gitlab::VisibilityLevel::PRIVATE)
+      it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member]
     end
   end
 
-  it_behaves_like 'rejects nuget access with unknown target id'
+  it_behaves_like 'rejects nuget access with unknown target id', not_found_response: :not_found
 
-  it_behaves_like 'rejects nuget access with invalid target id'
+  it_behaves_like 'rejects nuget access with invalid target id', not_found_response: :not_found
 end
 
 RSpec.shared_examples 'handling nuget metadata requests with package name' do |example_names_with_status: {}|
diff --git a/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb
index 3abe545db594..d6a0055700db 100644
--- a/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb
@@ -399,7 +399,7 @@
   it_behaves_like 'a package tracking event', 'API::NugetPackages', 'search_package'
 end
 
-RSpec.shared_examples 'rejects nuget access with invalid target id' do
+RSpec.shared_examples 'rejects nuget access with invalid target id' do |not_found_response: :unauthorized|
   context 'with a target id with invalid integers' do
     using RSpec::Parameterized::TableSyntax
 
@@ -411,7 +411,7 @@
       '%20'        | :bad_request
       '%2e%2e%2f'  | :bad_request
       'NaN'        | :bad_request
-      00002345     | :unauthorized
+      00002345     | not_found_response
       'anything25' | :bad_request
     end
 
@@ -421,12 +421,12 @@
   end
 end
 
-RSpec.shared_examples 'rejects nuget access with unknown target id' do
+RSpec.shared_examples 'rejects nuget access with unknown target id' do |not_found_response: :unauthorized|
   context 'with an unknown target' do
     let(:target) { double(id: 1234567890) }
 
     context 'as anonymous' do
-      it_behaves_like 'rejects nuget packages access', :anonymous, :unauthorized
+      it_behaves_like 'rejects nuget packages access', :anonymous, not_found_response
     end
 
     context 'as authenticated user' do
@@ -441,30 +441,59 @@
   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
+    where(:visibility_level, :user_role, :member, :user_token, :sent_through, :shared_examples_name, :expected_status) do
+      'PUBLIC'  | :developer  | true  | true  | :basic_auth | 'process nuget workhorse authorization' | :success
+      'PUBLIC'  | :guest      | true  | true  | :basic_auth | 'rejects nuget packages access'         | :forbidden
+      'PUBLIC'  | :developer  | true  | false | :basic_auth | 'rejects nuget packages access'         | :unauthorized
+      'PUBLIC'  | :guest      | true  | false | :basic_auth | 'rejects nuget packages access'         | :unauthorized
+      'PUBLIC'  | :developer  | false | true  | :basic_auth | 'rejects nuget packages access'         | :forbidden
+      'PUBLIC'  | :guest      | false | true  | :basic_auth | 'rejects nuget packages access'         | :forbidden
+      'PUBLIC'  | :developer  | false | false | :basic_auth | 'rejects nuget packages access'         | :unauthorized
+      'PUBLIC'  | :guest      | false | false | :basic_auth | 'rejects nuget packages access'         | :unauthorized
+      'PRIVATE' | :developer  | true  | true  | :basic_auth | 'process nuget workhorse authorization' | :success
+      'PRIVATE' | :guest      | true  | true  | :basic_auth | 'rejects nuget packages access'         | :forbidden
+      'PRIVATE' | :developer  | true  | false | :basic_auth | 'rejects nuget packages access'         | :unauthorized
+      'PRIVATE' | :guest      | true  | false | :basic_auth | 'rejects nuget packages access'         | :unauthorized
+      'PRIVATE' | :developer  | false | true  | :basic_auth | 'rejects nuget packages access'         | :not_found
+      'PRIVATE' | :guest      | false | true  | :basic_auth | 'rejects nuget packages access'         | :not_found
+      'PRIVATE' | :developer  | false | false | :basic_auth | 'rejects nuget packages access'         | :unauthorized
+      'PRIVATE' | :guest      | false | false | :basic_auth | 'rejects nuget packages access'         | :unauthorized
+
+      'PUBLIC'  | :developer  | true  | true  | :api_key    | 'process nuget workhorse authorization' | :success
+      'PUBLIC'  | :guest      | true  | true  | :api_key    | 'rejects nuget packages access'         | :forbidden
+      'PUBLIC'  | :developer  | true  | false | :api_key    | 'rejects nuget packages access'         | :unauthorized
+      'PUBLIC'  | :guest      | true  | false | :api_key    | 'rejects nuget packages access'         | :unauthorized
+      'PUBLIC'  | :developer  | false | true  | :api_key    | 'rejects nuget packages access'         | :forbidden
+      'PUBLIC'  | :guest      | false | true  | :api_key    | 'rejects nuget packages access'         | :forbidden
+      'PUBLIC'  | :developer  | false | false | :api_key    | 'rejects nuget packages access'         | :unauthorized
+      'PUBLIC'  | :guest      | false | false | :api_key    | 'rejects nuget packages access'         | :unauthorized
+      'PRIVATE' | :developer  | true  | true  | :api_key    | 'process nuget workhorse authorization' | :success
+      'PRIVATE' | :guest      | true  | true  | :api_key    | 'rejects nuget packages access'         | :forbidden
+      'PRIVATE' | :developer  | true  | false | :api_key    | 'rejects nuget packages access'         | :unauthorized
+      'PRIVATE' | :guest      | true  | false | :api_key    | 'rejects nuget packages access'         | :unauthorized
+      'PRIVATE' | :developer  | false | true  | :api_key    | 'rejects nuget packages access'         | :not_found
+      'PRIVATE' | :guest      | false | true  | :api_key    | 'rejects nuget packages access'         | :not_found
+      'PRIVATE' | :developer  | false | false | :api_key    | 'rejects nuget packages access'         | :unauthorized
+      'PRIVATE' | :guest      | false | false | :api_key    | 'rejects nuget packages access'         | :unauthorized
+
+      'PUBLIC'  | :anonymous  | false | true  | nil         | 'rejects nuget packages access'         | :unauthorized
+      'PRIVATE' | :anonymous  | false | true  | nil         | '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(:user_headers) do
+        case sent_through
+        when :basic_auth
+          basic_auth_header(user.username, token)
+        when :api_key
+          { 'X-NuGet-ApiKey' => token }
+        else
+          {}
+        end
+      end
+
       let(:headers) { user_headers.merge(workhorse_headers) }
 
       before do
@@ -490,30 +519,59 @@
   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
+    where(:visibility_level, :user_role, :member, :user_token, :sent_through, :shared_examples_name, :expected_status) do
+      'PUBLIC'  | :developer  | true  | true  | :basic_auth | 'process nuget upload'          | :created
+      'PUBLIC'  | :guest      | true  | true  | :basic_auth | 'rejects nuget packages access' | :forbidden
+      'PUBLIC'  | :developer  | true  | false | :basic_auth | 'rejects nuget packages access' | :unauthorized
+      'PUBLIC'  | :guest      | true  | false | :basic_auth | 'rejects nuget packages access' | :unauthorized
+      'PUBLIC'  | :developer  | false | true  | :basic_auth | 'rejects nuget packages access' | :forbidden
+      'PUBLIC'  | :guest      | false | true  | :basic_auth | 'rejects nuget packages access' | :forbidden
+      'PUBLIC'  | :developer  | false | false | :basic_auth | 'rejects nuget packages access' | :unauthorized
+      'PUBLIC'  | :guest      | false | false | :basic_auth | 'rejects nuget packages access' | :unauthorized
+      'PRIVATE' | :developer  | true  | true  | :basic_auth | 'process nuget upload'          | :created
+      'PRIVATE' | :guest      | true  | true  | :basic_auth | 'rejects nuget packages access' | :forbidden
+      'PRIVATE' | :developer  | true  | false | :basic_auth | 'rejects nuget packages access' | :unauthorized
+      'PRIVATE' | :guest      | true  | false | :basic_auth | 'rejects nuget packages access' | :unauthorized
+      'PRIVATE' | :developer  | false | true  | :basic_auth | 'rejects nuget packages access' | :not_found
+      'PRIVATE' | :guest      | false | true  | :basic_auth | 'rejects nuget packages access' | :not_found
+      'PRIVATE' | :developer  | false | false | :basic_auth | 'rejects nuget packages access' | :unauthorized
+      'PRIVATE' | :guest      | false | false | :basic_auth | 'rejects nuget packages access' | :unauthorized
+
+      'PUBLIC'  | :developer  | true  | true  | :api_key    | 'process nuget upload'          | :created
+      'PUBLIC'  | :guest      | true  | true  | :api_key    | 'rejects nuget packages access' | :forbidden
+      'PUBLIC'  | :developer  | true  | false | :api_key    | 'rejects nuget packages access' | :unauthorized
+      'PUBLIC'  | :guest      | true  | false | :api_key    | 'rejects nuget packages access' | :unauthorized
+      'PUBLIC'  | :developer  | false | true  | :api_key    | 'rejects nuget packages access' | :forbidden
+      'PUBLIC'  | :guest      | false | true  | :api_key    | 'rejects nuget packages access' | :forbidden
+      'PUBLIC'  | :developer  | false | false | :api_key    | 'rejects nuget packages access' | :unauthorized
+      'PUBLIC'  | :guest      | false | false | :api_key    | 'rejects nuget packages access' | :unauthorized
+      'PRIVATE' | :developer  | true  | true  | :api_key    | 'process nuget upload'          | :created
+      'PRIVATE' | :guest      | true  | true  | :api_key    | 'rejects nuget packages access' | :forbidden
+      'PRIVATE' | :developer  | true  | false | :api_key    | 'rejects nuget packages access' | :unauthorized
+      'PRIVATE' | :guest      | true  | false | :api_key    | 'rejects nuget packages access' | :unauthorized
+      'PRIVATE' | :developer  | false | true  | :api_key    | 'rejects nuget packages access' | :not_found
+      'PRIVATE' | :guest      | false | true  | :api_key    | 'rejects nuget packages access' | :not_found
+      'PRIVATE' | :developer  | false | false | :api_key    | 'rejects nuget packages access' | :unauthorized
+      'PRIVATE' | :guest      | false | false | :api_key    | 'rejects nuget packages access' | :unauthorized
+
+      'PUBLIC'  | :anonymous  | false | true  | nil         | 'rejects nuget packages access' | :unauthorized
+      'PRIVATE' | :anonymous  | false | true  | nil         | '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(:user_headers) do
+        case sent_through
+        when :basic_auth
+          basic_auth_header(user.username, token)
+        when :api_key
+          { 'X-NuGet-ApiKey' => token }
+        else
+          {}
+        end
+      end
+
       let(:headers) { user_headers.merge(workhorse_headers) }
       let(:snowplow_gitlab_standard_context) { { project: project, user: user, namespace: project.namespace, property: 'i_package_nuget_user' } }
 
-- 
GitLab