Commit d19d9aab authored by Fabio Pitino's avatar Fabio Pitino

Merge branch 'add-basic-auth-for-generic-packages' into 'master'

Add basic auth and deploy token support for generic packages

See merge request gitlab-org/gitlab!48540
parents 004cd454 8ab67149
---
title: "Allow HTTP Basic Auth and deploy token authentication for generic packages"
merge_request: 48540
author: Moshe Katz @kohenkatz
type: added
......@@ -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
......
......@@ -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)
......
......@@ -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
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment