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 ...@@ -16,13 +16,21 @@ module API
namespace 'packages/conan/v1/users/' do namespace 'packages/conan/v1/users/' do
format :txt format :txt
desc 'Authenticate user' do desc 'Authenticate user against conan CLI' do
detail 'This feature was introduced in GitLab 12.2' detail 'This feature was introduced in GitLab 12.2'
end end
get 'authenticate' do get 'authenticate' do
token = ::Gitlab::ConanToken.from_personal_access_token(access_token) token = ::Gitlab::ConanToken.from_personal_access_token(access_token)
token.to_jwt token.to_jwt
end 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 end
namespace 'packages/conan/v1/' do namespace 'packages/conan/v1/' do
...@@ -34,7 +42,91 @@ module API ...@@ -34,7 +42,91 @@ module API
end end
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 helpers do
def base_file_url
"#{::Settings.gitlab.base_url}/api/v4/packages/conan/v1/files"
end
def find_personal_access_token def find_personal_access_token
personal_access_token = find_personal_access_token_from_conan_jwt || personal_access_token = find_personal_access_token_from_conan_jwt ||
find_personal_access_token_from_conan_http_basic_auth find_personal_access_token_from_conan_http_basic_auth
...@@ -64,6 +156,10 @@ module API ...@@ -64,6 +156,10 @@ module API
PersonalAccessToken.find_by_token(token) PersonalAccessToken.find_by_token(token)
end end
def valid_url_recipe?(recipe_url)
recipe_url =~ %r{\A(([\w](\.|\+|-)?)*(\/?)){4}\z}
end
end end
end end
end end
...@@ -12,6 +12,11 @@ describe API::ConanPackages do ...@@ -12,6 +12,11 @@ describe API::ConanPackages do
) )
end 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 before do
stub_licensed_features(packages: true) stub_licensed_features(packages: true)
allow(Settings).to receive(:attr_encrypted_db_key_base).and_return(base_secret) allow(Settings).to receive(:attr_encrypted_db_key_base).and_return(base_secret)
...@@ -24,6 +29,10 @@ describe API::ConanPackages do ...@@ -24,6 +29,10 @@ describe API::ConanPackages do
end end
end end
def build_auth_headers(token)
{ 'HTTP_AUTHORIZATION' => "Bearer #{token}" }
end
describe 'GET /api/v4/packages/conan/v1/ping' do describe 'GET /api/v4/packages/conan/v1/ping' do
context 'feature flag disabled' do context 'feature flag disabled' do
before do before do
...@@ -45,46 +54,36 @@ describe API::ConanPackages do ...@@ -45,46 +54,36 @@ describe API::ConanPackages do
end end
it 'responds with 200 OK when valid token is provided' do it 'responds with 200 OK when valid token is provided' do
personal_access_token = create(:personal_access_token)
jwt = build_jwt(personal_access_token) jwt = build_jwt(personal_access_token)
headers = { 'HTTP_AUTHORIZATION' => "Bearer #{jwt.encoded}" } get api('/packages/conan/v1/ping'), headers: build_auth_headers(jwt.encoded)
get api('/packages/conan/v1/ping'), headers: headers
expect(response).to have_gitlab_http_status(200) expect(response).to have_gitlab_http_status(200)
expect(response.headers['X-Conan-Server-Capabilities']).to eq("") expect(response.headers['X-Conan-Server-Capabilities']).to eq("")
end end
it 'responds with 401 Unauthorized when invalid access token ID is provided' do 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) 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: build_auth_headers(jwt.encoded)
get api('/packages/conan/v1/ping'), headers: headers
expect(response).to have_gitlab_http_status(401) expect(response).to have_gitlab_http_status(401)
end end
it 'responds with 401 Unauthorized when invalid user is provided' do 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) jwt = build_jwt(personal_access_token, user_id: 12345)
headers = { 'HTTP_AUTHORIZATION' => "Bearer #{jwt.encoded}" } get api('/packages/conan/v1/ping'), headers: build_auth_headers(jwt.encoded)
get api('/packages/conan/v1/ping'), headers: headers
expect(response).to have_gitlab_http_status(401) expect(response).to have_gitlab_http_status(401)
end end
it 'responds with 401 Unauthorized when the provided JWT is signed with different secret' do 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)) jwt = build_jwt(personal_access_token, secret: SecureRandom.base64(32))
headers = { 'HTTP_AUTHORIZATION' => "Bearer #{jwt.encoded}" } get api('/packages/conan/v1/ping'), headers: build_auth_headers(jwt.encoded)
get api('/packages/conan/v1/ping'), headers: headers
expect(response).to have_gitlab_http_status(401) expect(response).to have_gitlab_http_status(401)
end end
it 'responds with 401 Unauthorized when invalid JWT is provided' do it 'responds with 401 Unauthorized when invalid JWT is provided' do
headers = { 'HTTP_AUTHORIZATION' => "Bearer invalid-jwt" } get api('/packages/conan/v1/ping'), headers: build_auth_headers('invalid-jwt')
get api('/packages/conan/v1/ping'), headers: headers
expect(response).to have_gitlab_http_status(401) expect(response).to have_gitlab_http_status(401)
end end
...@@ -109,8 +108,6 @@ describe API::ConanPackages do ...@@ -109,8 +108,6 @@ describe API::ConanPackages do
end end
it 'responds with 200 OK and JWT when valid access token is provided' do 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 get api('/packages/conan/v1/users/authenticate'), headers: headers
expect(response).to have_gitlab_http_status(200) expect(response).to have_gitlab_http_status(200)
...@@ -123,4 +120,129 @@ describe API::ConanPackages do ...@@ -123,4 +120,129 @@ describe API::ConanPackages do
expect(duration).to eq(1.hour) expect(duration).to eq(1.hour)
end end
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 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