Commit 9ae10bab authored by charlie ablett's avatar charlie ablett

Merge branch '213566-deploy-token-conan' into 'master'

Deploy token access for the Conan Package Registry

See merge request gitlab-org/gitlab!31114
parents f5e79f65 ab8d4984
---
title: Conan registry is accessible using deploy tokens
merge_request: 31114
author:
type: added
......@@ -108,14 +108,19 @@ conan search Hello* --all --remote=gitlab
## Authenticating to the GitLab Conan Repository
You will need to generate a [personal access token](../../../user/profile/personal_access_tokens.md) with the scope set to `api` for repository authentication.
You will need a personal access token or deploy token.
For repository authentication:
- You can generate a [personal access token](../../../user/profile/personal_access_tokens.md) with the scope set to `api`.
- You can generate a [deploy token](./../../project/deploy_tokens/index.md) with the scope set to `read_package_registry`, `write_package_registry`, or both.
### Adding a Conan user to the GitLab remote
Once you have a personal access token and have [set your Conan remote](#adding-the-gitlab-package-registry-as-a-conan-remote), you can associate the token with the remote so you do not have to explicitly add them to each Conan command you run:
```shell
conan user <gitlab-username> -r gitlab -p <personal_access_token>
conan user <gitlab_username or deploy_token_username> -r gitlab -p <personal_access_token or deploy_token>
```
Note: **Note**
......@@ -130,7 +135,7 @@ Alternatively, you could explicitly include your credentials in any given comman
For example:
```shell
CONAN_LOGIN_USERNAME=<gitlab-username> CONAN_PASSWORD=<personal_access_token> conan upload Hello/0.1@my-group+my-project/beta --all --remote=gitlab
CONAN_LOGIN_USERNAME=<gitlab_username or deploy_token_username> CONAN_PASSWORD=<personal_access_token or deploy_token> conan upload Hello/0.1@my-group+my-project/beta --all --remote=gitlab
```
### Setting a default remote to your project (optional)
......@@ -148,7 +153,7 @@ This functionality is best suited for when you want to consume or install packag
The rest of the example commands in this documentation assume that you have added a Conan user with your credentials to the `gitlab` remote and will not include the explicit credentials or remote option, but be aware that any of the commands could be run without having added a user or default remote:
```shell
`CONAN_LOGIN_USERNAME=<gitlab-username> CONAN_PASSWORD=<personal_access_token> <conan command> --remote=gitlab
`CONAN_LOGIN_USERNAME=<gitlab_username or deploy_token_username> CONAN_PASSWORD=<personal_access_token or deploy_token> <conan command> --remote=gitlab
```
## Uploading a package
......
......@@ -63,11 +63,7 @@ module API
end
route_setting :authentication, job_token_allowed: true
get 'authenticate' do
token = if access_token
::Gitlab::ConanToken.from_personal_access_token(access_token)
else
::Gitlab::ConanToken.from_job(find_job_from_token)
end
unauthorized! unless token
token.to_jwt
end
......
......@@ -94,6 +94,16 @@ module API
end
end
def token
strong_memoize(:token) do
token = nil
token = ::Gitlab::ConanToken.from_personal_access_token(access_token) if access_token
token = ::Gitlab::ConanToken.from_deploy_token(deploy_token_from_request) if deploy_token_from_request
token = ::Gitlab::ConanToken.from_job(find_job_from_token) if find_job_from_token
token
end
end
def download_package_file(file_type)
authorize!(:read_package, project)
......@@ -156,6 +166,10 @@ module API
job.user
end
def deploy_token_from_request
find_deploy_token_from_conan_jwt || find_deploy_token_from_http_basic_auth
end
def find_job_from_token
find_job_from_conan_jwt || find_job_from_http_basic_auth
end
......@@ -173,6 +187,18 @@ module API
PersonalAccessToken.find_by_id_and_user_id(token.access_token_id, token.user_id)
end
def find_deploy_token_from_conan_jwt
token = decode_oauth_token_from_jwt
return unless token
deploy_token = DeployToken.active.find_by_token(token.access_token_id.to_s)
# note: uesr_id is not a user record id, but is the attribute set on ConanToken
return if token.user_id != deploy_token&.username
deploy_token
end
def find_job_from_conan_jwt
token = decode_oauth_token_from_jwt
......
......@@ -36,6 +36,16 @@ module API
::Ci::Build.find_by_token(token)
end
def find_deploy_token_from_http_basic_auth
return unless headers
token = decode_token
return unless token
DeployToken.active.find_by_token(token)
end
def uploaded_package_file(param_name = :file)
uploaded_file = UploadedFile.from_params(params, param_name, ::Packages::PackageFileUploader.workhorse_local_upload_path)
bad_request!('Missing package file!') unless uploaded_file
......
......@@ -20,6 +20,10 @@ module Gitlab
new(access_token_id: job.token, user_id: job.user.id)
end
def from_deploy_token(deploy_token)
new(access_token_id: deploy_token.token, user_id: deploy_token.username)
end
def decode(jwt)
payload = JSONWebToken::HMACToken.decode(jwt, secret).first
......
......@@ -80,6 +80,42 @@ describe API::Helpers::PackagesManagerClientsHelpers do
end
end
describe '#find_deploy_token_from_http_basic_auth' do
let_it_be(:deploy_token) { create(:deploy_token) }
let(:token) { deploy_token.token }
let(:headers) { { Authorization: basic_http_auth(deploy_token.username, token) } }
subject { helper.find_deploy_token_from_http_basic_auth }
before do
allow(helper).to receive(:headers).and_return(headers&.with_indifferent_access)
end
context 'with a valid Authorization header' do
it { is_expected.to eq deploy_token }
end
context 'with an invalid Authorization header' do
where(:headers) do
[
[{ Authorization: 'Invalid' }],
[{}],
[nil]
]
end
with_them do
it { is_expected.to be nil }
end
end
context 'with an invalid token' do
let(:token) { 'Unknown' }
it { is_expected.to be nil }
end
end
describe '#uploaded_package_file' do
let_it_be(:params) { {} }
......
......@@ -47,6 +47,17 @@ describe Gitlab::ConanToken do
end
end
describe '.from_deploy_token' do
it 'sets access token id and user id' do
deploy_token = double(token: '123', username: 'bob')
token = described_class.from_deploy_token(deploy_token)
expect(token.access_token_id).to eq('123')
expect(token.user_id).to eq('bob')
end
end
describe '.decode' do
it 'sets access token id and user id' do
jwt = build_jwt(access_token_id: 123, user_id: 456)
......
......@@ -14,6 +14,8 @@ describe API::ConanPackages do
let(:auth_token) { personal_access_token.token }
let(:job) { create(:ci_build, user: user) }
let(:job_token) { job.token }
let(:deploy_token) { create(:deploy_token, read_package_registry: true, write_package_registry: true) }
let(:project_deploy_token) { create(:project_deploy_token, deploy_token: deploy_token, project: project) }
let(:headers) do
{ 'HTTP_AUTHORIZATION' => ActionController::HttpAuthentication::Basic.encode_credentials('foo', auth_token) }
......@@ -56,6 +58,14 @@ describe API::ConanPackages do
expect(response.headers['X-Conan-Server-Capabilities']).to eq("")
end
it 'responds with 200 OK when valid deploy token is provided' do
jwt = build_jwt_from_deploy_token(deploy_token)
get api('/packages/conan/v1/ping'), headers: build_token_auth_header(jwt.encoded)
expect(response).to have_gitlab_http_status(:ok)
expect(response.headers['X-Conan-Server-Capabilities']).to eq("")
end
it 'responds with 401 Unauthorized when invalid access token ID is provided' do
jwt = build_jwt(double(id: 12345), user_id: personal_access_token.user_id)
get api('/packages/conan/v1/ping'), headers: build_token_auth_header(jwt.encoded)
......@@ -165,6 +175,16 @@ describe API::ConanPackages do
expect(response).to have_gitlab_http_status(:ok)
end
end
context 'with valid deploy token' do
let(:auth_token) { deploy_token.token }
it 'responds with 200' do
subject
expect(response).to have_gitlab_http_status(:ok)
end
end
end
describe 'GET /api/v4/packages/conan/v1/users/check_credentials' do
......@@ -184,6 +204,16 @@ describe API::ConanPackages do
end
end
context 'with deploy token' do
let(:auth_token) { deploy_token.token }
it 'responds with a 200 OK with job token' do
get api('/packages/conan/v1/users/check_credentials'), headers: headers
expect(response).to have_gitlab_http_status(:ok)
end
end
it 'responds with a 401 Unauthorized when an invalid token is used' do
get api('/packages/conan/v1/users/check_credentials'), headers: build_token_auth_header('invalid-token')
......
......@@ -28,6 +28,13 @@ module EE
end
end
def build_jwt_from_deploy_token(deploy_token, secret: jwt_secret)
JSONWebToken::HMACToken.new(secret).tap do |jwt|
jwt['access_token'] = deploy_token.token
jwt['user_id'] = deploy_token.username
end
end
def temp_file(package_tmp)
upload_path = ::Packages::PackageFileUploader.workhorse_local_upload_path
file_path = "#{upload_path}/#{package_tmp}"
......
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