Commit ab8d4984 authored by Steve Abrams's avatar Steve Abrams Committed by charlie ablett

Deploy tokens for Conan package manager

Update the ConanToken model and surrounding auth
flow to allow Conan API endpoints to authenticate
with deploy tokens.
parent e4855eb6
---
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