Commit 2d5571f9 authored by Steve Abrams's avatar Steve Abrams Committed by Grzegorz Bizon

Add Conan check_credentials and upload_urls api endpoints

Endpoint added to match conan registry API used by CLI.
check_credentials is called by the conan CLI to periodically
re-check the user token.

upload_urls is called by the conan CLI to return the urls
to be used to upload package files to the GitLab package registry
parent 7e2969b7
---
title: Add Conan check_credentials API endpoint
merge_request: 16215
author:
type: added
......@@ -16,13 +16,21 @@ module API
namespace 'packages/conan/v1/users/' do
format :txt
desc 'Authenticate user' do
desc 'Authenticate user against conan CLI' do
detail 'This feature was introduced in GitLab 12.2'
end
get 'authenticate' do
token = ::Gitlab::ConanToken.from_personal_access_token(access_token)
token.to_jwt
end
desc 'Check for valid user credentials per conan CLI' do
detail 'This feature was introduced in GitLab 12.3'
end
get 'check_credentials' do
authenticate!
:ok
end
end
namespace 'packages/conan/v1/' do
......@@ -34,7 +42,91 @@ module API
end
end
namespace 'packages/conan/v1/conans/*url_recipe' do
before do
render_api_error!("Invalid recipe", 400) unless valid_url_recipe?(params[:url_recipe])
end
params do
requires :url_recipe, type: String, desc: 'Package recipe'
end
# Get the recipe manifest
# returns the download urls for the existing recipe in the registry
#
# the manifest is a hash of { filename: url }
# where the url is the download url for the file
desc 'Package Digest' do
detail 'This feature was introduced in GitLab 12.3'
end
get 'packages/:package_id/digest' do
render_api_error!("No recipe manifest found", 404)
end
desc 'Recipe Digest' do
detail 'This feature was introduced in GitLab 12.3'
end
get 'digest' do
render_api_error!("No recipe manifest found", 404)
end
# Get the upload urls
#
# request body contains { filename: filesize } where the filename is the
# name of the file the conan client is requesting to upload
#
# returns { filename: url }
# where the url is the upload url for the file that the conan client will use
desc 'Package Upload Urls' do
detail 'This feature was introduced in GitLab 12.3'
end
params do
requires :package_id, type: String, desc: 'Conan package ID'
end
post 'packages/:package_id/upload_urls' do
status 200
{
'conaninfo.txt': "#{base_file_url}/#{params[:url_recipe]}/-/0/package/#{params[:package_id]}/0/conaninfo.txt",
'conanmanifest.txt': "#{base_file_url}/#{params[:url_recipe]}/-/0/package/#{params[:package_id]}/0/conanmanifest.txt",
'conan_package.tgz': "#{base_file_url}/#{params[:url_recipe]}/-/0/package/#{params[:package_id]}/0/conan_package.tgz"
}
end
desc 'Recipe Upload Urls' do
detail 'This feature was introduced in GitLab 12.3'
end
post 'upload_urls' do
status 200
{
'conanfile.py': "#{base_file_url}/#{params[:url_recipe]}/-/0/export/conanfile.py",
'conanmanifest.txt': "#{base_file_url}/#{params[:url_recipe]}/-/0/export/conanmanifest.txt"
}
end
# Get the recipe snapshot
#
# the snapshot is a hash of { filename: md5 hash }
# md5 hash is the has of that file. This hash is used to diff the files existing on the client
# to determine which client files need to be uploaded if no recipe exists the snapshot is empty
desc 'Recipe Snapshot' do
detail 'This feature was introduced in GitLab 12.3'
end
get '/' do
{}
end
desc 'Package Snapshot' do
detail 'This feature was introduced in GitLab 12.3'
end
get 'packages/:package_id' do
{}
end
end
helpers do
def base_file_url
"#{::Settings.gitlab.base_url}/api/v4/packages/conan/v1/files"
end
def find_personal_access_token
personal_access_token = find_personal_access_token_from_conan_jwt ||
find_personal_access_token_from_conan_http_basic_auth
......@@ -64,6 +156,10 @@ module API
PersonalAccessToken.find_by_token(token)
end
def valid_url_recipe?(recipe_url)
recipe_url =~ %r{\A(([\w](\.|\+|-)?)*(\/?)){4}\z}
end
end
end
end
......@@ -12,6 +12,11 @@ describe API::ConanPackages do
)
end
let(:personal_access_token) { create(:personal_access_token) }
let(:headers) do
{ 'HTTP_AUTHORIZATION' => ActionController::HttpAuthentication::Basic.encode_credentials('foo', personal_access_token.token) }
end
before do
stub_licensed_features(packages: true)
allow(Settings).to receive(:attr_encrypted_db_key_base).and_return(base_secret)
......@@ -24,6 +29,10 @@ describe API::ConanPackages do
end
end
def build_auth_headers(token)
{ 'HTTP_AUTHORIZATION' => "Bearer #{token}" }
end
describe 'GET /api/v4/packages/conan/v1/ping' do
context 'feature flag disabled' do
before do
......@@ -45,46 +54,36 @@ describe API::ConanPackages do
end
it 'responds with 200 OK when valid token is provided' do
personal_access_token = create(:personal_access_token)
jwt = build_jwt(personal_access_token)
headers = { 'HTTP_AUTHORIZATION' => "Bearer #{jwt.encoded}" }
get api('/packages/conan/v1/ping'), headers: headers
get api('/packages/conan/v1/ping'), headers: build_auth_headers(jwt.encoded)
expect(response).to have_gitlab_http_status(200)
expect(response.headers['X-Conan-Server-Capabilities']).to eq("")
end
it 'responds with 401 Unauthorized when invalid access token ID is provided' do
personal_access_token = create(:personal_access_token)
jwt = build_jwt(double(id: 12345), user_id: personal_access_token.user_id)
headers = { 'HTTP_AUTHORIZATION' => "Bearer #{jwt.encoded}" }
get api('/packages/conan/v1/ping'), headers: headers
get api('/packages/conan/v1/ping'), headers: build_auth_headers(jwt.encoded)
expect(response).to have_gitlab_http_status(401)
end
it 'responds with 401 Unauthorized when invalid user is provided' do
personal_access_token = create(:personal_access_token)
jwt = build_jwt(personal_access_token, user_id: 12345)
headers = { 'HTTP_AUTHORIZATION' => "Bearer #{jwt.encoded}" }
get api('/packages/conan/v1/ping'), headers: headers
get api('/packages/conan/v1/ping'), headers: build_auth_headers(jwt.encoded)
expect(response).to have_gitlab_http_status(401)
end
it 'responds with 401 Unauthorized when the provided JWT is signed with different secret' do
personal_access_token = create(:personal_access_token)
jwt = build_jwt(personal_access_token, secret: SecureRandom.base64(32))
headers = { 'HTTP_AUTHORIZATION' => "Bearer #{jwt.encoded}" }
get api('/packages/conan/v1/ping'), headers: headers
get api('/packages/conan/v1/ping'), headers: build_auth_headers(jwt.encoded)
expect(response).to have_gitlab_http_status(401)
end
it 'responds with 401 Unauthorized when invalid JWT is provided' do
headers = { 'HTTP_AUTHORIZATION' => "Bearer invalid-jwt" }
get api('/packages/conan/v1/ping'), headers: headers
get api('/packages/conan/v1/ping'), headers: build_auth_headers('invalid-jwt')
expect(response).to have_gitlab_http_status(401)
end
......@@ -109,8 +108,6 @@ describe API::ConanPackages do
end
it 'responds with 200 OK and JWT when valid access token is provided' do
personal_access_token = create(:personal_access_token)
headers = { 'HTTP_AUTHORIZATION' => ActionController::HttpAuthentication::Basic.encode_credentials('foo', personal_access_token.token) }
get api('/packages/conan/v1/users/authenticate'), headers: headers
expect(response).to have_gitlab_http_status(200)
......@@ -123,4 +120,129 @@ describe API::ConanPackages do
expect(duration).to eq(1.hour)
end
end
describe 'GET /api/v4/packages/conan/v1/users/check_credentials' do
it 'responds with a 200 OK' do
get api('/packages/conan/v1/users/check_credentials'), headers: headers
expect(response).to have_gitlab_http_status(200)
end
it 'responds with a 401 Unauthorized when an invalid token is used' do
get api('/packages/conan/v1/users/check_credentials'), headers: build_auth_headers('invalid-token')
expect(response).to have_gitlab_http_status(401)
end
end
shared_examples 'rejected invalid recipe' do
context 'with invalid recipe url' do
let(:recipe) { '../../foo++../..' }
it 'returns 400' do
subject
expect(response).to have_gitlab_http_status(400)
end
end
end
context 'recipe endpoints' do
let(:jwt) { build_jwt(personal_access_token) }
let(:headers) { build_auth_headers(jwt.encoded) }
let(:recipe) { 'my-package-name/1.0/username/channel' }
describe 'GET /api/v4/packages/conan/v1/conans/*recipe' do
subject { get api("/packages/conan/v1/conans/#{recipe}"), headers: headers }
it_behaves_like 'rejected invalid recipe'
it 'responds with an empty response' do
subject
expect(response.body).to be {}
end
end
describe 'GET /api/v4/packages/conan/v1/conans/*recipe/digest' do
subject { get api("/packages/conan/v1/conans/#{recipe}/digest"), headers: headers }
it_behaves_like 'rejected invalid recipe'
it 'responds with a 404' do
subject
expect(response).to have_gitlab_http_status(404)
end
end
describe 'GET /api/v4/packages/conan/v1/conans/*recipe/upload_urls' do
let(:params) do
{ "conanfile.py": 24,
"conanmanifext.txt": 123 }
end
subject { post api("/packages/conan/v1/conans/#{recipe}/upload_urls"), params: params, headers: headers }
it_behaves_like 'rejected invalid recipe'
it 'returns a set of upload urls for the files requested' do
subject
expected_response = {
'conanfile.py': "#{Settings.gitlab.base_url}/api/v4/packages/conan/v1/files/#{recipe}/-/0/export/conanfile.py",
'conanmanifest.txt': "#{Settings.gitlab.base_url}/api/v4/packages/conan/v1/files/#{recipe}/-/0/export/conanmanifest.txt"
}
expect(response.body).to eq expected_response.to_json
end
end
describe 'GET /api/v4/packages/conan/v1/conans/*recipe/packages/:package_id' do
subject { get api("/packages/conan/v1/conans/#{recipe}/packages/123456789"), headers: headers }
it_behaves_like 'rejected invalid recipe'
it 'responds with an empty response' do
subject
expect(response.body).to be {}
end
end
describe 'GET /api/v4/packages/conan/v1/conans/*recipe/packages/:package_id/digest' do
subject { get api("/packages/conan/v1/conans/#{recipe}/packages/123456789/digest"), headers: headers }
it_behaves_like 'rejected invalid recipe'
it 'responds with a 404' do
subject
expect(response).to have_gitlab_http_status(404)
end
end
describe 'GET /api/v4/packages/conan/v1/conans/*recipe/packages/:package_id/upload_urls' do
let(:params) do
{ "conaninfo.txt": 24,
"conanmanifext.txt": 123,
"conan_package.tgz": 523 }
end
context 'valid recipe' do
subject { post api("/packages/conan/v1/conans/#{recipe}/packages/123456789/upload_urls"), params: params, headers: headers }
it_behaves_like 'rejected invalid recipe'
it 'returns a set of upload urls for the files requested' do
expected_response = {
'conaninfo.txt': "#{Settings.gitlab.base_url}/api/v4/packages/conan/v1/files/#{recipe}/-/0/package/123456789/0/conaninfo.txt",
'conanmanifest.txt': "#{Settings.gitlab.base_url}/api/v4/packages/conan/v1/files/#{recipe}/-/0/package/123456789/0/conanmanifest.txt",
'conan_package.tgz': "#{Settings.gitlab.base_url}/api/v4/packages/conan/v1/files/#{recipe}/-/0/package/123456789/0/conan_package.tgz"
}
subject
expect(response.body).to eq expected_response.to_json
end
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