Commit 44739865 authored by Ash McKenzie's avatar Ash McKenzie

Merge branch '213566-package-deploy-token-auth' into 'master'

Deploy tokens as API auth method

See merge request gitlab-org/gitlab!30332
parents a7b0553d 5955e1d9
......@@ -525,12 +525,14 @@ class Project < ApplicationRecord
def self.public_or_visible_to_user(user = nil, min_access_level = nil)
min_access_level = nil if user&.admin?
if user
return public_to_user unless user
if user.is_a?(DeployToken)
user.projects
else
where('EXISTS (?) OR projects.visibility_level IN (?)',
user.authorizations_for_projects(min_access_level: min_access_level),
Gitlab::VisibilityLevel.levels_for_user(user))
else
public_to_user
end
end
......
# frozen_string_literal: true
# Include this module if we want to pass something else than the user to
# check policies. This defines several methods which the policy checker
# would call and check.
# Include this module to have an object respond to user messages without being
# a user.
#
# Use Case 1:
# Pass something else than the user to check policies. This defines several
# methods which the policy checker would call and check.
#
# Use Case 2:
# Access the API with non-user object such as deploy tokens. This defines
# several methods which the API auth flow would call.
module PolicyActor
extend ActiveSupport::Concern
......@@ -37,6 +44,30 @@ module PolicyActor
def alert_bot?
false
end
def deactivated?
false
end
def confirmation_required_on_sign_in?
false
end
def can?(action, subject = :global)
Ability.allowed?(self, action, subject)
end
def preferred_language
nil
end
def requires_ldap_check?
false
end
def try_obtain_ldap_lease
nil
end
end
PolicyActor.prepend_if_ee('EE::PolicyActor')
......@@ -84,6 +84,16 @@ class ProjectPolicy < BasePolicy
project.merge_requests_allowing_push_to_user(user).any?
end
desc "Deploy token with read_package_registry scope"
condition(:read_package_registry_deploy_token) do
user.is_a?(DeployToken) && user.has_access_to?(project) && user.read_package_registry
end
desc "Deploy token with write_package_registry scope"
condition(:write_package_registry_deploy_token) do
user.is_a?(DeployToken) && user.has_access_to?(project) && user.write_package_registry
end
with_scope :subject
condition(:forking_allowed) do
@subject.feature_available?(:forking, @user)
......@@ -532,6 +542,16 @@ class ProjectPolicy < BasePolicy
prevent :destroy_design
end
rule { read_package_registry_deploy_token }.policy do
enable :read_package
enable :read_project
end
rule { write_package_registry_deploy_token }.policy do
enable :create_package
enable :read_project
end
private
def team_member?
......
---
title: Deploy token authentication for API with Maven endpoints
merge_request: 30332
author:
type: added
......@@ -207,9 +207,9 @@ Enter a project name or hit enter to use the directory name as project name.
The next step is to add the GitLab Package Registry as a Maven remote. If a
project is private or you want to upload Maven artifacts to GitLab,
credentials will need to be provided for authorization too. Support is available
for [personal access tokens](#authenticating-with-a-personal-access-token) and
[CI job tokens](#authenticating-with-a-ci-job-token) only.
[Deploy tokens](../../project/deploy_tokens/index.md) and regular username/password
for [personal access tokens](#authenticating-with-a-personal-access-token),
[CI job tokens](#authenticating-with-a-ci-job-token), and
[deploy tokens](../../project/deploy_tokens/index.md) only. Regular username/password
credentials do not work.
### Authenticating with a personal access token
......@@ -324,6 +324,59 @@ repositories {
}
```
### Authenticating with a deploy token
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/213566) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.0.
To authenticate with a [deploy token](./../../project/deploy_tokens/index.md),
set the scope to `api` when creating one, and add it to your Maven or Gradle configuration
files.
#### Authenticating with a deploy token in Maven
Add a corresponding section to your
[`settings.xml`](https://maven.apache.org/settings.html) file:
```xml
<settings>
<servers>
<server>
<id>gitlab-maven</id>
<configuration>
<httpHeaders>
<property>
<name>Deploy-Token</name>
<value>REPLACE_WITH_YOUR_DEPLOY_TOKEN</value>
</property>
</httpHeaders>
</configuration>
</server>
</servers>
</settings>
```
#### Authenticating with a deploy token in Gradle
To authenticate with a deploy token, add a repositories section to your
[`build.gradle`](https://docs.gradle.org/current/userguide/tutorial_using_tasks.html)
file:
```groovy
repositories {
maven {
url "https://<gitlab-url>/api/v4/groups/<group>/-/packages/maven"
name "GitLab"
credentials(HttpHeaderCredentials) {
name = 'Deploy-Token'
value = '<deploy-token>'
}
authentication {
header(HttpHeaderAuthentication)
}
}
}
```
## Configuring your project to use the GitLab Maven repository URL
To download and upload packages from GitLab, you need a `repository` and
......@@ -397,7 +450,7 @@ project's ID can be used for uploading.
### Group level Maven endpoint
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/8798) in GitLab Premium 11.7.
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/8798) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.7.
If you rely on many packages, it might be inefficient to include the `repository` section
with a unique URL for each package. Instead, you can use the group level endpoint for
......@@ -460,7 +513,7 @@ For retrieving artifacts, you can use either the
### Instance level Maven endpoint
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/8274) in GitLab Premium 11.7.
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/8274) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.7.
If you rely on many packages, it might be inefficient to include the `repository` section
with a unique URL for each package. Instead, you can use the instance level endpoint for
......
......@@ -84,7 +84,7 @@ module API
requires :path, type: String, desc: 'Package path'
requires :file_name, type: String, desc: 'Package file name'
end
route_setting :authentication, job_token_allowed: true
route_setting :authentication, job_token_allowed: true, deploy_token_allowed: true
get 'packages/maven/*path/:file_name', requirements: MAVEN_ENDPOINT_REQUIREMENTS do
file_name, format = extract_format(params[:file_name])
......@@ -125,7 +125,7 @@ module API
requires :path, type: String, desc: 'Package path'
requires :file_name, type: String, desc: 'Package file name'
end
route_setting :authentication, job_token_allowed: true
route_setting :authentication, job_token_allowed: true, deploy_token_allowed: true
get ':id/-/packages/maven/*path/:file_name', requirements: MAVEN_ENDPOINT_REQUIREMENTS do
file_name, format = extract_format(params[:file_name])
......@@ -170,7 +170,7 @@ module API
requires :path, type: String, desc: 'Package path'
requires :file_name, type: String, desc: 'Package file name'
end
route_setting :authentication, job_token_allowed: true
route_setting :authentication, job_token_allowed: true, deploy_token_allowed: true
get ':id/packages/maven/*path/:file_name', requirements: MAVEN_ENDPOINT_REQUIREMENTS do
authorize_read_package!(user_project)
......@@ -201,7 +201,7 @@ module API
requires :path, type: String, desc: 'Package path'
requires :file_name, type: String, desc: 'Package file name', regexp: Gitlab::Regex.maven_file_name_regex
end
route_setting :authentication, job_token_allowed: true
route_setting :authentication, job_token_allowed: true, deploy_token_allowed: true
put ':id/packages/maven/*path/:file_name/authorize', requirements: MAVEN_ENDPOINT_REQUIREMENTS do
authorize_upload!
......@@ -224,7 +224,7 @@ module API
optional 'file.sha1', type: String, desc: %q(sha1 checksum of the file (generated by Workhorse))
optional 'file.sha256', type: String, desc: %q(sha256 checksum of the file (generated by Workhorse))
end
route_setting :authentication, job_token_allowed: true
route_setting :authentication, job_token_allowed: true, deploy_token_allowed: true
put ':id/packages/maven/*path/:file_name', requirements: MAVEN_ENDPOINT_REQUIREMENTS do
authorize_upload!
......
......@@ -8,7 +8,8 @@ module EE
override :find_user_from_sources
def find_user_from_sources
find_user_from_bearer_token ||
deploy_token_from_request ||
find_user_from_bearer_token ||
find_user_from_job_token ||
find_user_from_warden
end
......
......@@ -11,10 +11,19 @@ describe API::MavenPackages do
let_it_be(:jar_file) { package.package_files.with_file_name_like('%.jar').first }
let_it_be(:personal_access_token) { create(:personal_access_token, user: user) }
let_it_be(:job) { create(:ci_build, user: user) }
let_it_be(:deploy_token) { create(:deploy_token, read_package_registry: true, write_package_registry: true) }
let_it_be(:project_deploy_token) { create(:project_deploy_token, deploy_token: deploy_token, project: project) }
let(:workhorse_token) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') }
let(:headers) { { 'GitLab-Workhorse' => '1.0', Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => workhorse_token } }
let(:headers_with_token) { headers.merge('Private-Token' => personal_access_token.token) }
let(:headers_with_deploy_token) do
headers.merge(
Gitlab::Auth::AuthFinders::DEPLOY_TOKEN_HEADER => deploy_token.token
)
end
let(:version) { '1.0-SNAPSHOT' }
before do
......@@ -78,6 +87,28 @@ describe API::MavenPackages do
end
end
shared_examples 'downloads with a deploy token' do
it 'allows download with deploy token' do
download_file(
package_file.file_name,
{},
Gitlab::Auth::AuthFinders::DEPLOY_TOKEN_HEADER => deploy_token.token
)
expect(response).to have_gitlab_http_status(:ok)
expect(response.media_type).to eq('application/octet-stream')
end
end
shared_examples 'downloads with a job token' do
it 'allows download with job token' do
download_file(package_file.file_name, job_token: job.token)
expect(response).to have_gitlab_http_status(:ok)
expect(response.media_type).to eq('application/octet-stream')
end
end
describe 'GET /api/v4/packages/maven/*path/:file_name' do
context 'a public project' do
subject { download_file(package_file.file_name) }
......@@ -123,12 +154,9 @@ describe API::MavenPackages do
expect(response).to have_gitlab_http_status(:forbidden)
end
it 'allows download with job token' do
download_file(package_file.file_name, job_token: job.token)
it_behaves_like 'downloads with a job token'
expect(response).to have_gitlab_http_status(:ok)
expect(response.media_type).to eq('application/octet-stream')
end
it_behaves_like 'downloads with a deploy token'
end
context 'private project' do
......@@ -161,12 +189,9 @@ describe API::MavenPackages do
expect(response).to have_gitlab_http_status(:forbidden)
end
it 'allows download with job token' do
download_file(package_file.file_name, job_token: job.token)
it_behaves_like 'downloads with a job token'
expect(response).to have_gitlab_http_status(:ok)
expect(response.media_type).to eq('application/octet-stream')
end
it_behaves_like 'downloads with a deploy token'
end
it 'rejects request if feature is not in the license' do
......@@ -254,12 +279,9 @@ describe API::MavenPackages do
expect(response).to have_gitlab_http_status(:not_found)
end
it 'allows download with job token' do
download_file(package_file.file_name, job_token: job.token)
it_behaves_like 'downloads with a job token'
expect(response).to have_gitlab_http_status(:ok)
expect(response.media_type).to eq('application/octet-stream')
end
it_behaves_like 'downloads with a deploy token'
end
context 'private project' do
......@@ -292,12 +314,9 @@ describe API::MavenPackages do
expect(response).to have_gitlab_http_status(:not_found)
end
it 'allows download with job token' do
download_file(package_file.file_name, job_token: job.token)
it_behaves_like 'downloads with a job token'
expect(response).to have_gitlab_http_status(:ok)
expect(response.media_type).to eq('application/octet-stream')
end
it_behaves_like 'downloads with a deploy token'
end
it 'rejects request if feature is not in the license' do
......@@ -375,12 +394,9 @@ describe API::MavenPackages do
expect(response).to have_gitlab_http_status(:not_found)
end
it 'allows download with job token' do
download_file(package_file.file_name, job_token: job.token)
it_behaves_like 'downloads with a job token'
expect(response).to have_gitlab_http_status(:ok)
expect(response.media_type).to eq('application/octet-stream')
end
it_behaves_like 'downloads with a deploy token'
end
it 'rejects request if feature is not in the license' do
......@@ -452,6 +468,12 @@ describe API::MavenPackages do
expect(response).to have_gitlab_http_status(:ok)
end
it 'authorizes upload with deploy token' do
authorize_upload({}, headers_with_deploy_token)
expect(response).to have_gitlab_http_status(:ok)
end
def authorize_upload(params = {}, request_headers = headers)
put api("/projects/#{project.id}/packages/maven/com/example/my-app/#{version}/maven-metadata.xml/authorize"), params: params, headers: request_headers
end
......@@ -531,6 +553,12 @@ describe API::MavenPackages do
expect(project.reload.packages.last.build_info.pipeline).to eq job.pipeline
end
it 'allows upload with deploy token' do
upload_file(params, headers_with_deploy_token)
expect(response).to have_gitlab_http_status(:ok)
end
context 'version is not correct' do
let(:version) { '$%123' }
......
......@@ -65,7 +65,8 @@ module API
end
def find_user_from_sources
find_user_from_access_token ||
deploy_token_from_request ||
find_user_from_access_token ||
find_user_from_job_token ||
find_user_from_warden
end
......@@ -90,12 +91,16 @@ module API
end
def api_access_allowed?(user)
Gitlab::UserAccess.new(user).allowed? && user.can?(:access_api)
user_allowed_or_deploy_token?(user) && user.can?(:access_api)
end
def api_access_denied_message(user)
Gitlab::Auth::UserAccessDeniedReason.new(user).rejection_message
end
def user_allowed_or_deploy_token?(user)
Gitlab::UserAccess.new(user).allowed? || user.is_a?(DeployToken)
end
end
class_methods do
......
......@@ -25,6 +25,7 @@ module Gitlab
PRIVATE_TOKEN_PARAM = :private_token
JOB_TOKEN_HEADER = 'HTTP_JOB_TOKEN'.freeze
JOB_TOKEN_PARAM = :job_token
DEPLOY_TOKEN_HEADER = 'HTTP_DEPLOY_TOKEN'.freeze
RUNNER_TOKEN_PARAM = :token
RUNNER_JOB_TOKEN_PARAM = :token
......@@ -101,6 +102,16 @@ module Gitlab
access_token.user || raise(UnauthorizedError)
end
# This returns a deploy token, not a user since a deploy token does not
# belong to a user.
def deploy_token_from_request
return unless route_authentication_setting[:deploy_token_allowed]
token = current_request.env[DEPLOY_TOKEN_HEADER].presence
DeployToken.active.find_by_token(token)
end
def find_runner_from_token
return unless api_request?
......
This diff is collapsed.
......@@ -3636,6 +3636,24 @@ describe Project do
expect(projects).to contain_exactly(public_project)
end
end
context 'with deploy token users' do
let_it_be(:private_project) { create(:project, :private) }
subject { described_class.all.public_or_visible_to_user(user) }
context 'deploy token user without project' do
let_it_be(:user) { create(:deploy_token) }
it { is_expected.to eq [] }
end
context 'deploy token user with project' do
let_it_be(:user) { create(:deploy_token, projects: [private_project]) }
it { is_expected.to include(private_project) }
end
end
end
describe '.ids_with_issuables_available_for' do
......
......@@ -691,4 +691,28 @@ describe ProjectPolicy do
end
end
end
context 'deploy token access' do
let!(:project_deploy_token) do
create(:project_deploy_token, project: project, deploy_token: deploy_token)
end
subject { described_class.new(deploy_token, project) }
context 'a deploy token with read_package_registry scope' do
let(:deploy_token) { create(:deploy_token, read_package_registry: true) }
it { is_expected.to be_allowed(:read_package) }
it { is_expected.to be_allowed(:read_project) }
it { is_expected.to be_disallowed(:create_package) }
end
context 'a deploy token with write_package_registry scope' do
let(:deploy_token) { create(:deploy_token, write_package_registry: true) }
it { is_expected.to be_allowed(:create_package) }
it { is_expected.to be_allowed(:read_project) }
it { is_expected.to be_disallowed(:destroy_package) }
end
end
end
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