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 ...@@ -525,12 +525,14 @@ class Project < ApplicationRecord
def self.public_or_visible_to_user(user = nil, min_access_level = nil) def self.public_or_visible_to_user(user = nil, min_access_level = nil)
min_access_level = nil if user&.admin? 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 (?)', where('EXISTS (?) OR projects.visibility_level IN (?)',
user.authorizations_for_projects(min_access_level: min_access_level), user.authorizations_for_projects(min_access_level: min_access_level),
Gitlab::VisibilityLevel.levels_for_user(user)) Gitlab::VisibilityLevel.levels_for_user(user))
else
public_to_user
end end
end end
......
# frozen_string_literal: true # frozen_string_literal: true
# Include this module if we want to pass something else than the user to # Include this module to have an object respond to user messages without being
# check policies. This defines several methods which the policy checker # a user.
# would call and check. #
# 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 module PolicyActor
extend ActiveSupport::Concern extend ActiveSupport::Concern
...@@ -37,6 +44,30 @@ module PolicyActor ...@@ -37,6 +44,30 @@ module PolicyActor
def alert_bot? def alert_bot?
false false
end 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 end
PolicyActor.prepend_if_ee('EE::PolicyActor') PolicyActor.prepend_if_ee('EE::PolicyActor')
...@@ -84,6 +84,16 @@ class ProjectPolicy < BasePolicy ...@@ -84,6 +84,16 @@ class ProjectPolicy < BasePolicy
project.merge_requests_allowing_push_to_user(user).any? project.merge_requests_allowing_push_to_user(user).any?
end 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 with_scope :subject
condition(:forking_allowed) do condition(:forking_allowed) do
@subject.feature_available?(:forking, @user) @subject.feature_available?(:forking, @user)
...@@ -532,6 +542,16 @@ class ProjectPolicy < BasePolicy ...@@ -532,6 +542,16 @@ class ProjectPolicy < BasePolicy
prevent :destroy_design prevent :destroy_design
end 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 private
def team_member? 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. ...@@ -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 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, project is private or you want to upload Maven artifacts to GitLab,
credentials will need to be provided for authorization too. Support is available credentials will need to be provided for authorization too. Support is available
for [personal access tokens](#authenticating-with-a-personal-access-token) and for [personal access tokens](#authenticating-with-a-personal-access-token),
[CI job tokens](#authenticating-with-a-ci-job-token) only. [CI job tokens](#authenticating-with-a-ci-job-token), and
[Deploy tokens](../../project/deploy_tokens/index.md) and regular username/password [deploy tokens](../../project/deploy_tokens/index.md) only. Regular username/password
credentials do not work. credentials do not work.
### Authenticating with a personal access token ### Authenticating with a personal access token
...@@ -324,6 +324,59 @@ repositories { ...@@ -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 ## Configuring your project to use the GitLab Maven repository URL
To download and upload packages from GitLab, you need a `repository` and To download and upload packages from GitLab, you need a `repository` and
...@@ -397,7 +450,7 @@ project's ID can be used for uploading. ...@@ -397,7 +450,7 @@ project's ID can be used for uploading.
### Group level Maven endpoint ### 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 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 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 ...@@ -460,7 +513,7 @@ For retrieving artifacts, you can use either the
### Instance level Maven endpoint ### 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 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 with a unique URL for each package. Instead, you can use the instance level endpoint for
......
...@@ -84,7 +84,7 @@ module API ...@@ -84,7 +84,7 @@ module API
requires :path, type: String, desc: 'Package path' requires :path, type: String, desc: 'Package path'
requires :file_name, type: String, desc: 'Package file name' requires :file_name, type: String, desc: 'Package file name'
end 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 get 'packages/maven/*path/:file_name', requirements: MAVEN_ENDPOINT_REQUIREMENTS do
file_name, format = extract_format(params[:file_name]) file_name, format = extract_format(params[:file_name])
...@@ -125,7 +125,7 @@ module API ...@@ -125,7 +125,7 @@ module API
requires :path, type: String, desc: 'Package path' requires :path, type: String, desc: 'Package path'
requires :file_name, type: String, desc: 'Package file name' requires :file_name, type: String, desc: 'Package file name'
end 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 get ':id/-/packages/maven/*path/:file_name', requirements: MAVEN_ENDPOINT_REQUIREMENTS do
file_name, format = extract_format(params[:file_name]) file_name, format = extract_format(params[:file_name])
...@@ -170,7 +170,7 @@ module API ...@@ -170,7 +170,7 @@ module API
requires :path, type: String, desc: 'Package path' requires :path, type: String, desc: 'Package path'
requires :file_name, type: String, desc: 'Package file name' requires :file_name, type: String, desc: 'Package file name'
end 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 get ':id/packages/maven/*path/:file_name', requirements: MAVEN_ENDPOINT_REQUIREMENTS do
authorize_read_package!(user_project) authorize_read_package!(user_project)
...@@ -201,7 +201,7 @@ module API ...@@ -201,7 +201,7 @@ module API
requires :path, type: String, desc: 'Package path' requires :path, type: String, desc: 'Package path'
requires :file_name, type: String, desc: 'Package file name', regexp: Gitlab::Regex.maven_file_name_regex requires :file_name, type: String, desc: 'Package file name', regexp: Gitlab::Regex.maven_file_name_regex
end 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 put ':id/packages/maven/*path/:file_name/authorize', requirements: MAVEN_ENDPOINT_REQUIREMENTS do
authorize_upload! authorize_upload!
...@@ -224,7 +224,7 @@ module API ...@@ -224,7 +224,7 @@ module API
optional 'file.sha1', type: String, desc: %q(sha1 checksum of the file (generated by Workhorse)) 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)) optional 'file.sha256', type: String, desc: %q(sha256 checksum of the file (generated by Workhorse))
end 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 put ':id/packages/maven/*path/:file_name', requirements: MAVEN_ENDPOINT_REQUIREMENTS do
authorize_upload! authorize_upload!
......
...@@ -8,7 +8,8 @@ module EE ...@@ -8,7 +8,8 @@ module EE
override :find_user_from_sources override :find_user_from_sources
def 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_job_token ||
find_user_from_warden find_user_from_warden
end end
......
...@@ -11,10 +11,19 @@ describe API::MavenPackages do ...@@ -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(: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(:personal_access_token) { create(:personal_access_token, user: user) }
let_it_be(:job) { create(:ci_build, 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(: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) { { '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_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' } let(:version) { '1.0-SNAPSHOT' }
before do before do
...@@ -78,6 +87,28 @@ describe API::MavenPackages do ...@@ -78,6 +87,28 @@ describe API::MavenPackages do
end end
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 describe 'GET /api/v4/packages/maven/*path/:file_name' do
context 'a public project' do context 'a public project' do
subject { download_file(package_file.file_name) } subject { download_file(package_file.file_name) }
...@@ -123,12 +154,9 @@ describe API::MavenPackages do ...@@ -123,12 +154,9 @@ describe API::MavenPackages do
expect(response).to have_gitlab_http_status(:forbidden) expect(response).to have_gitlab_http_status(:forbidden)
end end
it 'allows download with job token' do it_behaves_like 'downloads with a job token'
download_file(package_file.file_name, job_token: job.token)
expect(response).to have_gitlab_http_status(:ok) it_behaves_like 'downloads with a deploy token'
expect(response.media_type).to eq('application/octet-stream')
end
end end
context 'private project' do context 'private project' do
...@@ -161,12 +189,9 @@ describe API::MavenPackages do ...@@ -161,12 +189,9 @@ describe API::MavenPackages do
expect(response).to have_gitlab_http_status(:forbidden) expect(response).to have_gitlab_http_status(:forbidden)
end end
it 'allows download with job token' do it_behaves_like 'downloads with a job token'
download_file(package_file.file_name, job_token: job.token)
expect(response).to have_gitlab_http_status(:ok) it_behaves_like 'downloads with a deploy token'
expect(response.media_type).to eq('application/octet-stream')
end
end end
it 'rejects request if feature is not in the license' do it 'rejects request if feature is not in the license' do
...@@ -254,12 +279,9 @@ describe API::MavenPackages do ...@@ -254,12 +279,9 @@ describe API::MavenPackages do
expect(response).to have_gitlab_http_status(:not_found) expect(response).to have_gitlab_http_status(:not_found)
end end
it 'allows download with job token' do it_behaves_like 'downloads with a job token'
download_file(package_file.file_name, job_token: job.token)
expect(response).to have_gitlab_http_status(:ok) it_behaves_like 'downloads with a deploy token'
expect(response.media_type).to eq('application/octet-stream')
end
end end
context 'private project' do context 'private project' do
...@@ -292,12 +314,9 @@ describe API::MavenPackages do ...@@ -292,12 +314,9 @@ describe API::MavenPackages do
expect(response).to have_gitlab_http_status(:not_found) expect(response).to have_gitlab_http_status(:not_found)
end end
it 'allows download with job token' do it_behaves_like 'downloads with a job token'
download_file(package_file.file_name, job_token: job.token)
expect(response).to have_gitlab_http_status(:ok) it_behaves_like 'downloads with a deploy token'
expect(response.media_type).to eq('application/octet-stream')
end
end end
it 'rejects request if feature is not in the license' do it 'rejects request if feature is not in the license' do
...@@ -375,12 +394,9 @@ describe API::MavenPackages do ...@@ -375,12 +394,9 @@ describe API::MavenPackages do
expect(response).to have_gitlab_http_status(:not_found) expect(response).to have_gitlab_http_status(:not_found)
end end
it 'allows download with job token' do it_behaves_like 'downloads with a job token'
download_file(package_file.file_name, job_token: job.token)
expect(response).to have_gitlab_http_status(:ok) it_behaves_like 'downloads with a deploy token'
expect(response.media_type).to eq('application/octet-stream')
end
end end
it 'rejects request if feature is not in the license' do it 'rejects request if feature is not in the license' do
...@@ -452,6 +468,12 @@ describe API::MavenPackages do ...@@ -452,6 +468,12 @@ describe API::MavenPackages do
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
end 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) 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 put api("/projects/#{project.id}/packages/maven/com/example/my-app/#{version}/maven-metadata.xml/authorize"), params: params, headers: request_headers
end end
...@@ -531,6 +553,12 @@ describe API::MavenPackages do ...@@ -531,6 +553,12 @@ describe API::MavenPackages do
expect(project.reload.packages.last.build_info.pipeline).to eq job.pipeline expect(project.reload.packages.last.build_info.pipeline).to eq job.pipeline
end 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 context 'version is not correct' do
let(:version) { '$%123' } let(:version) { '$%123' }
......
...@@ -65,7 +65,8 @@ module API ...@@ -65,7 +65,8 @@ module API
end end
def find_user_from_sources 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_job_token ||
find_user_from_warden find_user_from_warden
end end
...@@ -90,12 +91,16 @@ module API ...@@ -90,12 +91,16 @@ module API
end end
def api_access_allowed?(user) 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 end
def api_access_denied_message(user) def api_access_denied_message(user)
Gitlab::Auth::UserAccessDeniedReason.new(user).rejection_message Gitlab::Auth::UserAccessDeniedReason.new(user).rejection_message
end end
def user_allowed_or_deploy_token?(user)
Gitlab::UserAccess.new(user).allowed? || user.is_a?(DeployToken)
end
end end
class_methods do class_methods do
......
...@@ -25,6 +25,7 @@ module Gitlab ...@@ -25,6 +25,7 @@ module Gitlab
PRIVATE_TOKEN_PARAM = :private_token PRIVATE_TOKEN_PARAM = :private_token
JOB_TOKEN_HEADER = 'HTTP_JOB_TOKEN'.freeze JOB_TOKEN_HEADER = 'HTTP_JOB_TOKEN'.freeze
JOB_TOKEN_PARAM = :job_token JOB_TOKEN_PARAM = :job_token
DEPLOY_TOKEN_HEADER = 'HTTP_DEPLOY_TOKEN'.freeze
RUNNER_TOKEN_PARAM = :token RUNNER_TOKEN_PARAM = :token
RUNNER_JOB_TOKEN_PARAM = :token RUNNER_JOB_TOKEN_PARAM = :token
...@@ -101,6 +102,16 @@ module Gitlab ...@@ -101,6 +102,16 @@ module Gitlab
access_token.user || raise(UnauthorizedError) access_token.user || raise(UnauthorizedError)
end 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 def find_runner_from_token
return unless api_request? return unless api_request?
......
This diff is collapsed.
...@@ -3636,6 +3636,24 @@ describe Project do ...@@ -3636,6 +3636,24 @@ describe Project do
expect(projects).to contain_exactly(public_project) expect(projects).to contain_exactly(public_project)
end end
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 end
describe '.ids_with_issuables_available_for' do describe '.ids_with_issuables_available_for' do
......
...@@ -691,4 +691,28 @@ describe ProjectPolicy do ...@@ -691,4 +691,28 @@ describe ProjectPolicy do
end end
end 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 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