From 8ab67149bcf2dccfd7b746661742ec1a8b0d2efd Mon Sep 17 00:00:00 2001
From: Moshe Katz <kohenkatz@gmail.com>
Date: Tue, 5 Jan 2021 15:25:13 +0000
Subject: [PATCH] Squashed commit for: generic package auth

Allows basic auth and deploy token  on all generic packages endpoints
---
 ...-allow-basic-auth-for-generic-packages.yml |   5 +
 doc/user/packages/generic_packages/index.md   |  21 +-
 lib/api/generic_packages.rb                   |   8 +-
 spec/requests/api/generic_packages_spec.rb    | 189 +++++++++++++++---
 4 files changed, 188 insertions(+), 35 deletions(-)
 create mode 100644 changelogs/unreleased/276965-allow-basic-auth-for-generic-packages.yml

diff --git a/changelogs/unreleased/276965-allow-basic-auth-for-generic-packages.yml b/changelogs/unreleased/276965-allow-basic-auth-for-generic-packages.yml
new file mode 100644
index 00000000000..2a4c3356a51
--- /dev/null
+++ b/changelogs/unreleased/276965-allow-basic-auth-for-generic-packages.yml
@@ -0,0 +1,5 @@
+---
+title: "Allow HTTP Basic Auth and deploy token authentication for generic packages"
+merge_request: 48540
+author: Moshe Katz @kohenkatz
+type: added
diff --git a/doc/user/packages/generic_packages/index.md b/doc/user/packages/generic_packages/index.md
index ebdc48d50b9..82c72481984 100644
--- a/doc/user/packages/generic_packages/index.md
+++ b/doc/user/packages/generic_packages/index.md
@@ -20,8 +20,14 @@ Publish generic files, like release binaries, in your project’s Package Regist
 
 ## Authenticate to the Package Registry
 
-To authenticate to the Package Registry, you need either a [personal access token](../../../api/README.md#personalproject-access-tokens)
-or [CI job token](../../../api/README.md#gitlab-ci-job-token).
+To authenticate to the Package Registry, you need either a [personal access token](../../../api/README.md#personalproject-access-tokens),
+[CI job token](../../../api/README.md#gitlab-ci-job-token), or [deploy token](../../project/deploy_tokens/index.md).
+
+In addition to the standard API authentication mechanisms, the generic package
+API allows authentication with HTTP Basic authentication for use with tools that
+do not support the other available mechanisms. The `user-id` is not checked and
+may be any value, and the `password` must be either a [personal access token](../../../api/README.md#personalproject-access-tokens),
+a [CI job token](../../../api/README.md#gitlab-ci-job-token), or a [deploy token](../../project/deploy_tokens/index.md).
 
 ## Publish a package file
 
@@ -31,7 +37,7 @@ If a package with the same name, version, and filename already exists, it is als
 
 Prerequisites:
 
-- You need to [authenticate with the API](../../../api/README.md#authentication).
+- You need to [authenticate with the API](../../../api/README.md#authentication). If authenticating with a deploy token, it must be configured with the `write_package_registry` scope.
 
 ```plaintext
 PUT /projects/:id/packages/generic/:package_name/:package_version/:file_name
@@ -70,7 +76,7 @@ If multiple packages have the same name, version, and filename, then the most re
 
 Prerequisites:
 
-- You need to [authenticate with the API](../../../api/README.md#authentication).
+- You need to [authenticate with the API](../../../api/README.md#authentication). If authenticating with a deploy token, it must be configured with the `read_package_registry` and/or `write_package_registry` scope.
 
 ```plaintext
 GET /projects/:id/packages/generic/:package_name/:package_version/:file_name
@@ -92,6 +98,13 @@ curl --header "PRIVATE-TOKEN: <your_access_token>" \
      "https://gitlab.example.com/api/v4/projects/24/packages/generic/my_package/0.0.1/file.txt"
 ```
 
+Example request that uses HTTP Basic authentication:
+
+```shell
+curl --user "user:<your_access_token>" \
+     https://gitlab.example.com/api/v4/projects/24/packages/generic/my_package/0.0.1/file.txt
+```
+
 ## Publish a generic package by using CI/CD
 
 To work with generic packages in [GitLab CI/CD](../../../ci/README.md), you can use
diff --git a/lib/api/generic_packages.rb b/lib/api/generic_packages.rb
index 3e1dd044c8d..167531fdaec 100644
--- a/lib/api/generic_packages.rb
+++ b/lib/api/generic_packages.rb
@@ -21,7 +21,7 @@ module API
     end
 
     resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
-      route_setting :authentication, job_token_allowed: true
+      route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true, deploy_token_allowed: true
 
       namespace ':id/packages/generic' do
         namespace ':package_name/*package_version/:file_name', requirements: GENERIC_PACKAGES_REQUIREMENTS do
@@ -29,7 +29,7 @@ module API
             detail 'This feature was introduced in GitLab 13.5'
           end
 
-          route_setting :authentication, job_token_allowed: true
+          route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true, deploy_token_allowed: true
 
           params do
             requires :package_name, type: String, desc: 'Package name', regexp: Gitlab::Regex.generic_package_name_regex, file_path: true
@@ -52,7 +52,7 @@ module API
             requires :file, type: ::API::Validations::Types::WorkhorseFile, desc: 'The package file to be published (generated by Multipart middleware)'
           end
 
-          route_setting :authentication, job_token_allowed: true
+          route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true, deploy_token_allowed: true
 
           put do
             authorize_upload!(project)
@@ -82,7 +82,7 @@ module API
             requires :file_name, type: String, desc: 'Package file name', regexp: Gitlab::Regex.generic_package_file_name_regex, file_path: true
           end
 
-          route_setting :authentication, job_token_allowed: true
+          route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true, deploy_token_allowed: true
 
           get do
             authorize_read_package!(project)
diff --git a/spec/requests/api/generic_packages_spec.rb b/spec/requests/api/generic_packages_spec.rb
index b8e79853486..d162d288129 100644
--- a/spec/requests/api/generic_packages_spec.rb
+++ b/spec/requests/api/generic_packages_spec.rb
@@ -3,8 +3,16 @@
 require 'spec_helper'
 
 RSpec.describe API::GenericPackages do
+  include HttpBasicAuthHelpers
+
   let_it_be(:personal_access_token) { create(:personal_access_token) }
   let_it_be(:project, reload: true) { create(:project) }
+  let_it_be(:deploy_token_rw) { create(:deploy_token, read_package_registry: true, write_package_registry: true) }
+  let_it_be(:project_deploy_token_rw) { create(:project_deploy_token, deploy_token: deploy_token_rw, project: project) }
+  let_it_be(:deploy_token_ro) { create(:deploy_token, read_package_registry: true, write_package_registry: false) }
+  let_it_be(:project_deploy_token_ro) { create(:project_deploy_token, deploy_token: deploy_token_ro, project: project) }
+  let_it_be(:deploy_token_wo) { create(:deploy_token, read_package_registry: false, write_package_registry: true) }
+  let_it_be(:project_deploy_token_wo) { create(:project_deploy_token, deploy_token: deploy_token_wo, project: project) }
   let(:workhorse_token) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') }
   let(:workhorse_header) { { 'GitLab-Workhorse' => '1.0', Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => workhorse_token } }
   let(:user) { personal_access_token.user }
@@ -22,6 +30,23 @@ RSpec.describe API::GenericPackages do
       personal_access_token_header('wrong token')
     when :invalid_job_token
       job_token_header('wrong token')
+    when :user_basic_auth
+      user_basic_auth_header(user)
+    when :invalid_user_basic_auth
+      basic_auth_header('invalid user', 'invalid password')
+    end
+  end
+
+  def deploy_token_auth_header
+    case authenticate_with
+    when :deploy_token_rw
+      deploy_token_header(deploy_token_rw.token)
+    when :deploy_token_ro
+      deploy_token_header(deploy_token_ro.token)
+    when :deploy_token_wo
+      deploy_token_header(deploy_token_wo.token)
+    when :invalid_deploy_token
+      deploy_token_header('wrong token')
     end
   end
 
@@ -33,6 +58,10 @@ RSpec.describe API::GenericPackages do
     { Gitlab::Auth::AuthFinders::JOB_TOKEN_HEADER => value || ci_build.token }
   end
 
+  def deploy_token_header(value)
+    { Gitlab::Auth::AuthFinders::DEPLOY_TOKEN_HEADER => value }
+  end
+
   shared_examples 'secure endpoint' do
     before do
       project.add_developer(user)
@@ -54,19 +83,35 @@ RSpec.describe API::GenericPackages do
         'PUBLIC'  | :guest     | true  | :personal_access_token         | :forbidden
         'PUBLIC'  | :developer | true  | :invalid_personal_access_token | :unauthorized
         'PUBLIC'  | :guest     | true  | :invalid_personal_access_token | :unauthorized
+        'PUBLIC'  | :developer | true  | :user_basic_auth               | :success
+        'PUBLIC'  | :guest     | true  | :user_basic_auth               | :forbidden
+        'PUBLIC'  | :developer | true  | :invalid_user_basic_auth       | :unauthorized
+        'PUBLIC'  | :guest     | true  | :invalid_user_basic_auth       | :unauthorized
         'PUBLIC'  | :developer | false | :personal_access_token         | :forbidden
         'PUBLIC'  | :guest     | false | :personal_access_token         | :forbidden
         'PUBLIC'  | :developer | false | :invalid_personal_access_token | :unauthorized
         'PUBLIC'  | :guest     | false | :invalid_personal_access_token | :unauthorized
+        'PUBLIC'  | :developer | false | :user_basic_auth               | :forbidden
+        'PUBLIC'  | :guest     | false | :user_basic_auth               | :forbidden
+        'PUBLIC'  | :developer | false | :invalid_user_basic_auth       | :unauthorized
+        'PUBLIC'  | :guest     | false | :invalid_user_basic_auth       | :unauthorized
         'PUBLIC'  | :anonymous | false | :none                          | :unauthorized
         'PRIVATE' | :developer | true  | :personal_access_token         | :success
         'PRIVATE' | :guest     | true  | :personal_access_token         | :forbidden
         'PRIVATE' | :developer | true  | :invalid_personal_access_token | :unauthorized
         'PRIVATE' | :guest     | true  | :invalid_personal_access_token | :unauthorized
+        'PRIVATE' | :developer | true  | :user_basic_auth               | :success
+        'PRIVATE' | :guest     | true  | :user_basic_auth               | :forbidden
+        'PRIVATE' | :developer | true  | :invalid_user_basic_auth       | :unauthorized
+        'PRIVATE' | :guest     | true  | :invalid_user_basic_auth       | :unauthorized
         'PRIVATE' | :developer | false | :personal_access_token         | :not_found
         'PRIVATE' | :guest     | false | :personal_access_token         | :not_found
         'PRIVATE' | :developer | false | :invalid_personal_access_token | :unauthorized
         'PRIVATE' | :guest     | false | :invalid_personal_access_token | :unauthorized
+        'PRIVATE' | :developer | false | :user_basic_auth               | :not_found
+        'PRIVATE' | :guest     | false | :user_basic_auth               | :not_found
+        'PRIVATE' | :developer | false | :invalid_user_basic_auth       | :unauthorized
+        'PRIVATE' | :guest     | false | :invalid_user_basic_auth       | :unauthorized
         'PRIVATE' | :anonymous | false | :none                          | :unauthorized
         'PUBLIC'  | :developer | true  | :job_token                     | :success
         'PUBLIC'  | :developer | true  | :invalid_job_token             | :unauthorized
@@ -90,6 +135,21 @@ RSpec.describe API::GenericPackages do
           expect(response).to have_gitlab_http_status(expected_status)
         end
       end
+
+      where(:authenticate_with, :expected_status) do
+        :deploy_token_rw      | :success
+        :deploy_token_wo      | :success
+        :deploy_token_ro      | :forbidden
+        :invalid_deploy_token | :unauthorized
+      end
+
+      with_them do
+        it "responds with #{params[:expected_status]}" do
+          authorize_upload_file(workhorse_header.merge(deploy_token_auth_header))
+
+          expect(response).to have_gitlab_http_status(expected_status)
+        end
+      end
     end
 
     context 'application security' do
@@ -138,20 +198,34 @@ RSpec.describe API::GenericPackages do
 
       where(:project_visibility, :user_role, :member?, :authenticate_with, :expected_status) do
         'PUBLIC'  | :guest     | true  | :personal_access_token         | :forbidden
+        'PUBLIC'  | :guest     | true  | :user_basic_auth               | :forbidden
         'PUBLIC'  | :developer | true  | :invalid_personal_access_token | :unauthorized
         'PUBLIC'  | :guest     | true  | :invalid_personal_access_token | :unauthorized
+        'PUBLIC'  | :developer | true  | :invalid_user_basic_auth       | :unauthorized
+        'PUBLIC'  | :guest     | true  | :invalid_user_basic_auth       | :unauthorized
         'PUBLIC'  | :developer | false | :personal_access_token         | :forbidden
         'PUBLIC'  | :guest     | false | :personal_access_token         | :forbidden
+        'PUBLIC'  | :developer | false | :user_basic_auth               | :forbidden
+        'PUBLIC'  | :guest     | false | :user_basic_auth               | :forbidden
         'PUBLIC'  | :developer | false | :invalid_personal_access_token | :unauthorized
         'PUBLIC'  | :guest     | false | :invalid_personal_access_token | :unauthorized
+        'PUBLIC'  | :developer | false | :invalid_user_basic_auth       | :unauthorized
+        'PUBLIC'  | :guest     | false | :invalid_user_basic_auth       | :unauthorized
         'PUBLIC'  | :anonymous | false | :none                          | :unauthorized
         'PRIVATE' | :guest     | true  | :personal_access_token         | :forbidden
+        'PRIVATE' | :guest     | true  | :user_basic_auth               | :forbidden
         'PRIVATE' | :developer | true  | :invalid_personal_access_token | :unauthorized
         'PRIVATE' | :guest     | true  | :invalid_personal_access_token | :unauthorized
+        'PRIVATE' | :developer | true  | :invalid_user_basic_auth       | :unauthorized
+        'PRIVATE' | :guest     | true  | :invalid_user_basic_auth       | :unauthorized
         'PRIVATE' | :developer | false | :personal_access_token         | :not_found
         'PRIVATE' | :guest     | false | :personal_access_token         | :not_found
+        'PRIVATE' | :developer | false | :user_basic_auth               | :not_found
+        'PRIVATE' | :guest     | false | :user_basic_auth               | :not_found
         'PRIVATE' | :developer | false | :invalid_personal_access_token | :unauthorized
         'PRIVATE' | :guest     | false | :invalid_personal_access_token | :unauthorized
+        'PRIVATE' | :developer | false | :invalid_user_basic_auth       | :unauthorized
+        'PRIVATE' | :guest     | false | :invalid_user_basic_auth       | :unauthorized
         'PRIVATE' | :anonymous | false | :none                          | :unauthorized
         'PUBLIC'  | :developer | true  | :invalid_job_token             | :unauthorized
         'PUBLIC'  | :developer | false | :job_token                     | :forbidden
@@ -175,6 +249,21 @@ RSpec.describe API::GenericPackages do
           expect(response).to have_gitlab_http_status(expected_status)
         end
       end
+
+      where(:authenticate_with, :expected_status) do
+        :deploy_token_ro      | :forbidden
+        :invalid_deploy_token | :unauthorized
+      end
+
+      with_them do
+        it "responds with #{params[:expected_status]}" do
+          headers = workhorse_header.merge(deploy_token_auth_header)
+
+          upload_file(params, headers)
+
+          expect(response).to have_gitlab_http_status(expected_status)
+        end
+      end
     end
 
     context 'when user can upload packages and has valid credentials' do
@@ -182,43 +271,58 @@ RSpec.describe API::GenericPackages do
         project.add_developer(user)
       end
 
-      it 'creates package and package file when valid personal access token is used' do
-        headers = workhorse_header.merge(personal_access_token_header)
+      shared_examples 'creates a package and package file' do
+        it 'creates a package and package file' do
+          headers = workhorse_header.merge(auth_header)
 
-        expect { upload_file(params, headers) }
-          .to change { project.packages.generic.count }.by(1)
-          .and change { Packages::PackageFile.count }.by(1)
+          expect { upload_file(params, headers) }
+            .to change { project.packages.generic.count }.by(1)
+            .and change { Packages::PackageFile.count }.by(1)
 
-        aggregate_failures do
-          expect(response).to have_gitlab_http_status(:created)
+          aggregate_failures do
+            expect(response).to have_gitlab_http_status(:created)
 
-          package = project.packages.generic.last
-          expect(package.name).to eq('mypackage')
-          expect(package.version).to eq('0.0.1')
-          expect(package.original_build_info).to be_nil
+            package = project.packages.generic.last
+            expect(package.name).to eq('mypackage')
+            expect(package.version).to eq('0.0.1')
 
-          package_file = package.package_files.last
-          expect(package_file.file_name).to eq('myfile.tar.gz')
+            if should_set_build_info
+              expect(package.original_build_info.pipeline).to eq(ci_build.pipeline)
+            else
+              expect(package.original_build_info).to be_nil
+            end
+
+            package_file = package.package_files.last
+            expect(package_file.file_name).to eq('myfile.tar.gz')
+          end
         end
       end
 
-      it 'creates package, package file, and package build info when valid job token is used' do
-        headers = workhorse_header.merge(job_token_header)
-
-        expect { upload_file(params, headers) }
-          .to change { project.packages.generic.count }.by(1)
-          .and change { Packages::PackageFile.count }.by(1)
+      context 'when valid personal access token is used' do
+        it_behaves_like 'creates a package and package file' do
+          let(:auth_header) { personal_access_token_header }
+          let(:should_set_build_info) { false }
+        end
+      end
 
-        aggregate_failures do
-          expect(response).to have_gitlab_http_status(:created)
+      context 'when valid basic auth is used' do
+        it_behaves_like 'creates a package and package file' do
+          let(:auth_header) { user_basic_auth_header(user) }
+          let(:should_set_build_info) { false }
+        end
+      end
 
-          package = project.packages.generic.last
-          expect(package.name).to eq('mypackage')
-          expect(package.version).to eq('0.0.1')
-          expect(package.original_build_info.pipeline).to eq(ci_build.pipeline)
+      context 'when valid deploy token is used' do
+        it_behaves_like 'creates a package and package file' do
+          let(:auth_header) { deploy_token_header(deploy_token_wo.token) }
+          let(:should_set_build_info) { false }
+        end
+      end
 
-          package_file = package.package_files.last
-          expect(package_file.file_name).to eq('myfile.tar.gz')
+      context 'when valid job token is used' do
+        it_behaves_like 'creates a package and package file' do
+          let(:auth_header) { job_token_header }
+          let(:should_set_build_info) { true }
         end
       end
 
@@ -309,21 +413,37 @@ RSpec.describe API::GenericPackages do
       where(:project_visibility, :user_role, :member?, :authenticate_with, :expected_status) do
         'PUBLIC'  | :developer | true  | :personal_access_token         | :success
         'PUBLIC'  | :guest     | true  | :personal_access_token         | :success
+        'PUBLIC'  | :developer | true  | :user_basic_auth               | :success
+        'PUBLIC'  | :guest     | true  | :user_basic_auth               | :success
         'PUBLIC'  | :developer | true  | :invalid_personal_access_token | :unauthorized
         'PUBLIC'  | :guest     | true  | :invalid_personal_access_token | :unauthorized
+        'PUBLIC'  | :developer | true  | :invalid_user_basic_auth       | :unauthorized
+        'PUBLIC'  | :guest     | true  | :invalid_user_basic_auth       | :unauthorized
         'PUBLIC'  | :developer | false | :personal_access_token         | :success
         'PUBLIC'  | :guest     | false | :personal_access_token         | :success
+        'PUBLIC'  | :developer | false | :user_basic_auth               | :success
+        'PUBLIC'  | :guest     | false | :user_basic_auth               | :success
         'PUBLIC'  | :developer | false | :invalid_personal_access_token | :unauthorized
         'PUBLIC'  | :guest     | false | :invalid_personal_access_token | :unauthorized
+        'PUBLIC'  | :developer | false | :invalid_user_basic_auth       | :unauthorized
+        'PUBLIC'  | :guest     | false | :invalid_user_basic_auth       | :unauthorized
         'PUBLIC'  | :anonymous | false | :none                          | :unauthorized
         'PRIVATE' | :developer | true  | :personal_access_token         | :success
         'PRIVATE' | :guest     | true  | :personal_access_token         | :forbidden
+        'PRIVATE' | :developer | true  | :user_basic_auth               | :success
+        'PRIVATE' | :guest     | true  | :user_basic_auth               | :forbidden
         'PRIVATE' | :developer | true  | :invalid_personal_access_token | :unauthorized
         'PRIVATE' | :guest     | true  | :invalid_personal_access_token | :unauthorized
+        'PRIVATE' | :developer | true  | :invalid_user_basic_auth       | :unauthorized
+        'PRIVATE' | :guest     | true  | :invalid_user_basic_auth       | :unauthorized
         'PRIVATE' | :developer | false | :personal_access_token         | :not_found
         'PRIVATE' | :guest     | false | :personal_access_token         | :not_found
+        'PRIVATE' | :developer | false | :user_basic_auth               | :not_found
+        'PRIVATE' | :guest     | false | :user_basic_auth               | :not_found
         'PRIVATE' | :developer | false | :invalid_personal_access_token | :unauthorized
         'PRIVATE' | :guest     | false | :invalid_personal_access_token | :unauthorized
+        'PRIVATE' | :developer | false | :invalid_user_basic_auth       | :unauthorized
+        'PRIVATE' | :guest     | false | :invalid_user_basic_auth       | :unauthorized
         'PRIVATE' | :anonymous | false | :none                          | :unauthorized
         'PUBLIC'  | :developer | true  | :job_token                     | :success
         'PUBLIC'  | :developer | true  | :invalid_job_token             | :unauthorized
@@ -347,6 +467,21 @@ RSpec.describe API::GenericPackages do
           expect(response).to have_gitlab_http_status(expected_status)
         end
       end
+
+      where(:authenticate_with, :expected_status) do
+        :deploy_token_rw      | :success
+        :deploy_token_wo      | :success
+        :deploy_token_ro      | :success
+        :invalid_deploy_token | :unauthorized
+      end
+
+      with_them do
+        it "responds with #{params[:expected_status]}" do
+          download_file(deploy_token_auth_header)
+
+          expect(response).to have_gitlab_http_status(expected_status)
+        end
+      end
     end
 
     context 'event tracking' do
-- 
2.30.9