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