Commit 5955e1d9 authored by Steve Abrams's avatar Steve Abrams

Deploy tokens as API auth method

Add deploy tokens as an authentication strategy for
API endpoints and add permissions to deploy tokens
on the project policy for package access.
Enables deploy tokens for Maven packages.
parent 9089200d
......@@ -522,12 +522,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)
......@@ -530,6 +540,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.
......@@ -3625,6 +3625,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
......
......@@ -672,4 +672,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