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 # frozen_string_literal: true
module API module API
class ConanPackages < Grape::API class ConanPackages < Grape::API
helpers ::API::Helpers::PackagesHelpers
before do before do
not_found! unless Feature.enabled?(:conan_package_registry) 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 end
helpers ::API::Helpers::PackagesHelpers namespace 'packages/conan/v1/users/' do
format :txt
before do desc 'Authenticate user' do
require_packages_enabled! detail 'This feature was introduced in GitLab 12.2'
require_conan_authentication! end
get 'authenticate' do
token = ::Gitlab::ConanToken.from_personal_access_token(access_token)
token.to_jwt
end
end end
namespace 'packages/conan/v1/' do
desc 'Ping the Conan API' do desc 'Ping the Conan API' do
detail 'This feature was introduced in GitLab 12.2' detail 'This feature was introduced in GitLab 12.2'
end end
get 'packages/conan/v1/ping' do get 'ping' do
nil 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 end
end end
...@@ -23,12 +23,6 @@ module API ...@@ -23,12 +23,6 @@ module API
def authorize_destroy_package! def authorize_destroy_package!
authorize!(:destroy_package, user_project) authorize!(:destroy_package, user_project)
end 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 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 @@ ...@@ -2,45 +2,125 @@
require 'spec_helper' require 'spec_helper'
describe API::ConanPackages do describe API::ConanPackages do
set(:guest) { create(:user) } let(:base_secret) { SecureRandom.base64(64) }
let(:api_user) { guest }
let(:jwt_secret) do
OpenSSL::HMAC.hexdigest(
OpenSSL::Digest::SHA256.new,
base_secret,
Gitlab::ConanToken::HMAC_KEY
)
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)
end end
describe 'GET /api/v4/packages/conan/v1/ping' do def build_jwt(personal_access_token, secret: jwt_secret, user_id: nil)
let(:url) { '/packages/conan/v1/ping' } JSONWebToken::HMACToken.new(secret).tap do |jwt|
jwt['pat'] = personal_access_token.id
subject { get api(url, api_user) } 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 context 'feature flag disabled' do
before do before do
stub_feature_flags(conan_package_registry: false) stub_feature_flags(conan_package_registry: false)
end end
it 'returns not found' do it 'responds with 404 Not Found' do
subject get api('/packages/conan/v1/ping')
expect(response).to have_gitlab_http_status(404) expect(response).to have_gitlab_http_status(404)
end end
end end
context 'feature flag enabled' do context 'feature flag enabled' do
it 'rejects with no authorization' do it 'responds with 401 Unauthorized when no token provided' do
subject 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) expect(response).to have_gitlab_http_status(401)
end end
context 'packages feature disabled' do context 'packages feature disabled' do
it 'fails' do it 'responds with 404 Not Found' do
stub_packages_setting(enabled: false) stub_packages_setting(enabled: false)
subject get api('/packages/conan/v1/ping')
expect(response).to have_gitlab_http_status(404) expect(response).to have_gitlab_http_status(404)
end end
end end
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 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