Commit ddbc9d7b authored by Bob Van Landuyt's avatar Bob Van Landuyt

Merge branch '12568-conan-api-auth' into 'master'

Implement authentication for Conan Packages API

See merge request gitlab-org/gitlab-ee!14864
parents 8028d7d4 a55d2e21
# frozen_string_literal: true
module API
class ConanPackages < Grape::API
helpers ::API::Helpers::PackagesHelpers
before do
not_found! unless Feature.enabled?(:conan_package_registry)
require_packages_enabled!
# Personal access token will be extracted from Bearer or Basic authorization
# in the overriden find_personal_access_token helper
authenticate!
end
helpers ::API::Helpers::PackagesHelpers
namespace 'packages/conan/v1/users/' do
format :txt
before do
require_packages_enabled!
require_conan_authentication!
desc 'Authenticate user' 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
end
namespace 'packages/conan/v1/' do
desc 'Ping the Conan API' do
detail 'This feature was introduced in GitLab 12.2'
end
get 'packages/conan/v1/ping' do
nil
get 'ping' do
header 'X-Conan-Server-Capabilities', [].join(',')
end
end
helpers do
def find_personal_access_token
personal_access_token = find_personal_access_token_from_conan_jwt ||
find_personal_access_token_from_conan_http_basic_auth
personal_access_token || unauthorized!
end
# We need to override this one because it
# looks into Bearer authorization header
def find_oauth_access_token
end
def find_personal_access_token_from_conan_jwt
jwt = Doorkeeper::OAuth::Token.from_bearer_authorization(current_request)
return unless jwt
token = ::Gitlab::ConanToken.decode(jwt)
return unless token&.personal_access_token_id && token&.user_id
PersonalAccessToken.find_by_id_and_user_id(token.personal_access_token_id, token.user_id)
end
def find_personal_access_token_from_conan_http_basic_auth
encoded_credentials = headers['Authorization'].to_s.split('Basic ', 2).second
token = Base64.decode64(encoded_credentials || '').split(':', 2).second
return unless token
PersonalAccessToken.find_by_token(token)
end
end
end
end
......@@ -23,12 +23,6 @@ module API
def authorize_destroy_package!
authorize!(:destroy_package, user_project)
end
def require_conan_authentication!
# TODO: implement Conan server authentication
# To be implemented in https://gitlab.com/gitlab-org/gitlab-ee/issues/12568
unauthorized!
end
end
end
end
# frozen_string_literal: true
module Gitlab
class ConanToken
HMAC_KEY = 'gitlab-conan-packages'.freeze
attr_reader :personal_access_token_id, :user_id
class << self
def from_personal_access_token(personal_access_token)
new(personal_access_token_id: personal_access_token.id, user_id: personal_access_token.user_id)
end
def decode(jwt)
payload = JSONWebToken::HMACToken.decode(jwt, secret).first
new(personal_access_token_id: payload['pat'], user_id: payload['u'])
rescue JWT::DecodeError
end
def secret
OpenSSL::HMAC.hexdigest(
OpenSSL::Digest::SHA256.new,
::Settings.attr_encrypted_db_key_base,
HMAC_KEY
)
end
end
def initialize(personal_access_token_id:, user_id:)
@personal_access_token_id = personal_access_token_id
@user_id = user_id
end
def to_jwt
hmac_token.encoded
end
private
def hmac_token
JSONWebToken::HMACToken.new(self.class.secret).tap do |token|
token['pat'] = personal_access_token_id
token['u'] = user_id
token.expire_time = token.issued_at + 1.hour
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::ConanToken do
let(:base_secret) { SecureRandom.base64(64) }
let(:jwt_secret) do
OpenSSL::HMAC.hexdigest(
OpenSSL::Digest::SHA256.new,
base_secret,
described_class::HMAC_KEY
)
end
before do
allow(Settings).to receive(:attr_encrypted_db_key_base).and_return(base_secret)
end
def build_jwt(personal_access_token_id:, user_id:)
JSONWebToken::HMACToken.new(jwt_secret).tap do |jwt|
jwt['pat'] = personal_access_token_id
jwt['u'] = user_id || user_id
jwt.expire_time = jwt.issued_at + 1.hour
end
end
describe '.from_personal_access_token' do
it 'sets personal access token id and user id' do
personal_access_token = double(id: 123, user_id: 456)
token = described_class.from_personal_access_token(personal_access_token)
expect(token.personal_access_token_id).to eq(123)
expect(token.user_id).to eq(456)
end
end
describe '.decode' do
it 'sets personal access token id and user id' do
jwt = build_jwt(personal_access_token_id: 123, user_id: 456)
token = described_class.decode(jwt.encoded)
expect(token.personal_access_token_id).to eq(123)
expect(token.user_id).to eq(456)
end
it 'returns nil for invalid JWT' do
expect(described_class.decode('invalid-jwt')).to be_nil
end
end
describe '#to_jwt' do
it 'returns the encoded JWT' do
allow(SecureRandom).to receive(:uuid).and_return('u-u-i-d')
Timecop.freeze do
jwt = build_jwt(personal_access_token_id: 123, user_id: 456)
token = described_class.new(personal_access_token_id: 123, user_id: 456)
expect(token.to_jwt).to eq(jwt.encoded)
end
end
end
end
......@@ -2,45 +2,125 @@
require 'spec_helper'
describe API::ConanPackages do
set(:guest) { create(:user) }
let(:api_user) { guest }
let(:base_secret) { SecureRandom.base64(64) }
let(:jwt_secret) do
OpenSSL::HMAC.hexdigest(
OpenSSL::Digest::SHA256.new,
base_secret,
Gitlab::ConanToken::HMAC_KEY
)
end
before do
stub_licensed_features(packages: true)
allow(Settings).to receive(:attr_encrypted_db_key_base).and_return(base_secret)
end
describe 'GET /api/v4/packages/conan/v1/ping' do
let(:url) { '/packages/conan/v1/ping' }
subject { get api(url, api_user) }
def build_jwt(personal_access_token, secret: jwt_secret, user_id: nil)
JSONWebToken::HMACToken.new(secret).tap do |jwt|
jwt['pat'] = personal_access_token.id
jwt['u'] = user_id || personal_access_token.user_id
end
end
describe 'GET /api/v4/packages/conan/v1/ping' do
context 'feature flag disabled' do
before do
stub_feature_flags(conan_package_registry: false)
end
it 'returns not found' do
subject
it 'responds with 404 Not Found' do
get api('/packages/conan/v1/ping')
expect(response).to have_gitlab_http_status(404)
end
end
context 'feature flag enabled' do
it 'rejects with no authorization' do
subject
it 'responds with 401 Unauthorized when no token provided' do
get api('/packages/conan/v1/ping')
expect(response).to have_gitlab_http_status(401)
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
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
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
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
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
expect(response).to have_gitlab_http_status(401)
end
context 'packages feature disabled' do
it 'fails' do
it 'responds with 404 Not Found' do
stub_packages_setting(enabled: false)
subject
get api('/packages/conan/v1/ping')
expect(response).to have_gitlab_http_status(404)
end
end
end
end
describe 'GET /api/v4/packages/conan/v1/users/authenticate' do
it 'responds with 401 Unauthorized when invalid token is provided' do
headers = { 'HTTP_AUTHORIZATION' => ActionController::HttpAuthentication::Basic.encode_credentials('foo', 'wrong-token') }
get api('/packages/conan/v1/users/authenticate'), headers: headers
expect(response).to have_gitlab_http_status(401)
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)
payload = JSONWebToken::HMACToken.decode(response.body, jwt_secret).first
expect(payload['pat']).to eq(personal_access_token.id)
expect(payload['u']).to eq(personal_access_token.user_id)
duration = payload['exp'] - payload['iat']
expect(duration).to eq(1.hour)
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