Commit 88f90b0e authored by Sean McGivern's avatar Sean McGivern

Merge branch 'unify-nuget-auth' into 'master'

Unified auth for NuGet package manager

See merge request gitlab-org/gitlab!38627
parents 9a83dbad 8dcfa42a
---
title: Add a new Ruby API for specifying allowed authentication mechanisms
for REST API endpoints
merge_request: 38627
author: Ethan Reesor (@firelizzard)
type: other
---
title: The NuGet endpoints will no longer ignore an invalid username when a personal
access token or deploy token is passed via HTTP Basic authentication
merge_request: 38627
author: Ethan Reesor (@firelizzard)
type: security
...@@ -69,10 +69,15 @@ module API ...@@ -69,10 +69,15 @@ module API
def find_user_from_sources def find_user_from_sources
strong_memoize(:find_user_from_sources) do strong_memoize(:find_user_from_sources) do
deploy_token_from_request || if try(:namespace_inheritable, :authentication)
find_user_from_bearer_token || user_from_namespace_inheritable ||
find_user_from_job_token || user_from_warden
user_from_warden else
deploy_token_from_request ||
find_user_from_bearer_token ||
find_user_from_job_token ||
user_from_warden
end
end end
end end
......
...@@ -56,9 +56,6 @@ module API ...@@ -56,9 +56,6 @@ module API
desc 'The NuGet Service Index' do desc 'The NuGet Service Index' do
detail 'This feature was introduced in GitLab 12.6' detail 'This feature was introduced in GitLab 12.6'
end end
route_setting :authentication, deploy_token_allowed: true, job_token_allowed: :basic_auth, basic_auth_personal_access_token: true
get 'index', format: :json do get 'index', format: :json do
authorize_read_package!(project_or_group) authorize_read_package!(project_or_group)
track_package_event('cli_metadata', :nuget, category: 'API::NugetPackages') track_package_event('cli_metadata', :nuget, category: 'API::NugetPackages')
...@@ -79,9 +76,6 @@ module API ...@@ -79,9 +76,6 @@ module API
desc 'The NuGet Metadata Service - Package name level' do desc 'The NuGet Metadata Service - Package name level' do
detail 'This feature was introduced in GitLab 12.8' detail 'This feature was introduced in GitLab 12.8'
end end
route_setting :authentication, deploy_token_allowed: true, job_token_allowed: :basic_auth, basic_auth_personal_access_token: true
get 'index', format: :json do get 'index', format: :json do
present ::Packages::Nuget::PackagesMetadataPresenter.new(find_packages(params[:package_name])), present ::Packages::Nuget::PackagesMetadataPresenter.new(find_packages(params[:package_name])),
with: ::API::Entities::Nuget::PackagesMetadata with: ::API::Entities::Nuget::PackagesMetadata
...@@ -93,9 +87,6 @@ module API ...@@ -93,9 +87,6 @@ module API
params do params do
requires :package_version, type: String, desc: 'The NuGet package version', regexp: API::NO_SLASH_URL_PART_REGEX requires :package_version, type: String, desc: 'The NuGet package version', regexp: API::NO_SLASH_URL_PART_REGEX
end end
route_setting :authentication, deploy_token_allowed: true, job_token_allowed: :basic_auth, basic_auth_personal_access_token: true
get '*package_version', format: :json do get '*package_version', format: :json do
present ::Packages::Nuget::PackageMetadataPresenter.new(find_package(params[:package_name], params[:package_version])), present ::Packages::Nuget::PackageMetadataPresenter.new(find_package(params[:package_name], params[:package_version])),
with: ::API::Entities::Nuget::PackageMetadata with: ::API::Entities::Nuget::PackageMetadata
...@@ -117,9 +108,6 @@ module API ...@@ -117,9 +108,6 @@ module API
desc 'The NuGet Search Service' do desc 'The NuGet Search Service' do
detail 'This feature was introduced in GitLab 12.8' detail 'This feature was introduced in GitLab 12.8'
end end
route_setting :authentication, deploy_token_allowed: true, job_token_allowed: :basic_auth, basic_auth_personal_access_token: true
get format: :json do get format: :json do
search_options = { search_options = {
include_prerelease_versions: params[:prerelease], include_prerelease_versions: params[:prerelease],
......
# frozen_string_literal: true
module API
module Helpers
module Authentication
extend ActiveSupport::Concern
class_methods do
def authenticate_with(&block)
strategies = ::Gitlab::APIAuthentication::Builder.new.build(&block)
namespace_inheritable :authentication, strategies
end
end
included do
helpers ::Gitlab::Utils::StrongMemoize
helpers do
def token_from_namespace_inheritable
strong_memoize(:token_from_namespace_inheritable) do
strategies = namespace_inheritable(:authentication)
next unless strategies&.any?
# Extract credentials from the request
found = strategies.to_h { |location, _| [location, ::Gitlab::APIAuthentication::TokenLocator.new(location).extract(current_request)] }
found.filter! { |location, raw| raw }
next unless found.any?
# Specifying multiple credentials is an error
# https://gitlab.com/gitlab-org/gitlab/-/merge_requests/38627#note_475984136
bad_request!('Found more than one set of credentials') if found.size > 1
location, raw = found.first
find_token_from_raw_credentials(strategies[location], raw)
end
rescue ::Gitlab::Auth::UnauthorizedError
# TODO: this should be rescued and converted by the exception handling middleware
# https://gitlab.com/gitlab-org/gitlab/-/merge_requests/38627#note_475174516
unauthorized!
end
def access_token_from_namespace_inheritable
token = token_from_namespace_inheritable
token if token.is_a? PersonalAccessToken
end
def user_from_namespace_inheritable
token = token_from_namespace_inheritable
return token if token.is_a? DeployToken
token&.user
end
private
def find_token_from_raw_credentials(token_types, raw)
token_types.each do |token_type|
# Resolve a token from the raw credentials
token = ::Gitlab::APIAuthentication::TokenResolver.new(token_type).resolve(raw)
return token if token
end
# If a request provides credentials via an allowed transport, the
# credentials must be valid. If we reach this point, the credentials
# must not be valid credentials of an allowed type.
raise ::Gitlab::Auth::UnauthorizedError
end
end
end
end
end
end
...@@ -9,13 +9,19 @@ ...@@ -9,13 +9,19 @@
# This is the group level API. # This is the group level API.
module API module API
class NugetGroupPackages < ::API::Base class NugetGroupPackages < ::API::Base
helpers ::API::Helpers::PackagesManagerClientsHelpers helpers ::API::Helpers::PackagesHelpers
helpers ::API::Helpers::Packages::BasicAuthHelpers helpers ::API::Helpers::Packages::BasicAuthHelpers
include ::API::Helpers::Authentication
feature_category :package_registry feature_category :package_registry
default_format :json default_format :json
authenticate_with do |accept|
accept.token_types(:personal_access_token, :deploy_token, :job_token)
.sent_through(:http_basic_auth)
end
rescue_from ArgumentError do |e| rescue_from ArgumentError do |e|
render_api_error!(e.message, 400) render_api_error!(e.message, 400)
end end
...@@ -38,8 +44,6 @@ module API ...@@ -38,8 +44,6 @@ module API
requires :id, type: String, desc: 'The ID of a group', regexp: ::API::Concerns::Packages::NugetEndpoints::POSITIVE_INTEGER_REGEX requires :id, type: String, desc: 'The ID of a group', regexp: ::API::Concerns::Packages::NugetEndpoints::POSITIVE_INTEGER_REGEX
end end
route_setting :authentication, deploy_token_allowed: true, job_token_allowed: :basic_auth, basic_auth_personal_access_token: true
resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
namespace ':id/packages/nuget' do namespace ':id/packages/nuget' do
after_validation do after_validation do
......
...@@ -9,8 +9,9 @@ ...@@ -9,8 +9,9 @@
# This is the project level API. # This is the project level API.
module API module API
class NugetProjectPackages < ::API::Base class NugetProjectPackages < ::API::Base
helpers ::API::Helpers::PackagesManagerClientsHelpers helpers ::API::Helpers::PackagesHelpers
helpers ::API::Helpers::Packages::BasicAuthHelpers helpers ::API::Helpers::Packages::BasicAuthHelpers
include ::API::Helpers::Authentication
feature_category :package_registry feature_category :package_registry
...@@ -18,6 +19,11 @@ module API ...@@ -18,6 +19,11 @@ module API
default_format :json default_format :json
authenticate_with do |accept|
accept.token_types(:personal_access_token, :deploy_token, :job_token)
.sent_through(:http_basic_auth)
end
rescue_from ArgumentError do |e| rescue_from ArgumentError do |e|
render_api_error!(e.message, 400) render_api_error!(e.message, 400)
end end
...@@ -35,9 +41,6 @@ module API ...@@ -35,9 +41,6 @@ module API
params do params do
requires :id, type: String, desc: 'The ID of a project', regexp: ::API::Concerns::Packages::NugetEndpoints::POSITIVE_INTEGER_REGEX requires :id, type: String, desc: 'The ID of a project', regexp: ::API::Concerns::Packages::NugetEndpoints::POSITIVE_INTEGER_REGEX
end end
route_setting :authentication, deploy_token_allowed: true, job_token_allowed: :basic_auth, basic_auth_personal_access_token: true
resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
namespace ':id/packages/nuget' do namespace ':id/packages/nuget' do
include ::API::Concerns::Packages::NugetEndpoints include ::API::Concerns::Packages::NugetEndpoints
...@@ -50,9 +53,6 @@ module API ...@@ -50,9 +53,6 @@ module API
params do params do
requires :package, type: ::API::Validations::Types::WorkhorseFile, desc: 'The package file to be published (generated by Multipart middleware)' requires :package, type: ::API::Validations::Types::WorkhorseFile, desc: 'The package file to be published (generated by Multipart middleware)'
end end
route_setting :authentication, deploy_token_allowed: true, job_token_allowed: :basic_auth, basic_auth_personal_access_token: true
put do put do
authorize_upload!(project_or_group) authorize_upload!(project_or_group)
bad_request!('File is too large') if project_or_group.actual_limits.exceeded?(:nuget_max_file_size, params[:package].size) bad_request!('File is too large') if project_or_group.actual_limits.exceeded?(:nuget_max_file_size, params[:package].size)
...@@ -78,9 +78,6 @@ module API ...@@ -78,9 +78,6 @@ module API
forbidden! forbidden!
end end
route_setting :authentication, deploy_token_allowed: true, job_token_allowed: :basic_auth, basic_auth_personal_access_token: true
put 'authorize' do put 'authorize' do
authorize_workhorse!( authorize_workhorse!(
subject: project_or_group, subject: project_or_group,
...@@ -101,9 +98,6 @@ module API ...@@ -101,9 +98,6 @@ module API
desc 'The NuGet Content Service - index request' do desc 'The NuGet Content Service - index request' do
detail 'This feature was introduced in GitLab 12.8' detail 'This feature was introduced in GitLab 12.8'
end end
route_setting :authentication, deploy_token_allowed: true, job_token_allowed: :basic_auth, basic_auth_personal_access_token: true
get 'index', format: :json do get 'index', format: :json do
present ::Packages::Nuget::PackagesVersionsPresenter.new(find_packages(params[:package_name])), present ::Packages::Nuget::PackagesVersionsPresenter.new(find_packages(params[:package_name])),
with: ::API::Entities::Nuget::PackagesVersions with: ::API::Entities::Nuget::PackagesVersions
...@@ -116,9 +110,6 @@ module API ...@@ -116,9 +110,6 @@ module API
requires :package_version, type: String, desc: 'The NuGet package version', regexp: API::NO_SLASH_URL_PART_REGEX requires :package_version, type: String, desc: 'The NuGet package version', regexp: API::NO_SLASH_URL_PART_REGEX
requires :package_filename, type: String, desc: 'The NuGet package filename', regexp: API::NO_SLASH_URL_PART_REGEX requires :package_filename, type: String, desc: 'The NuGet package filename', regexp: API::NO_SLASH_URL_PART_REGEX
end end
route_setting :authentication, deploy_token_allowed: true, job_token_allowed: :basic_auth, basic_auth_personal_access_token: true
get '*package_version/*package_filename', format: :nupkg do get '*package_version/*package_filename', format: :nupkg do
filename = "#{params[:package_filename]}.#{params[:format]}" filename = "#{params[:package_filename]}.#{params[:format]}"
package_file = ::Packages::PackageFileFinder.new(find_package(params[:package_name], params[:package_version]), filename, with_file_name_like: true) package_file = ::Packages::PackageFileFinder.new(find_package(params[:package_name], params[:package_version]), filename, with_file_name_like: true)
......
# frozen_string_literal: true
# Authentication Strategies Builder
#
# AuthBuilder and its child classes, TokenType and SentThrough, support
# declaring allowed authentication strategies with patterns like
# `accept.token_type(:job_token).sent_through(:http_basic)`.
module Gitlab
module APIAuthentication
class Builder
def build
strategies = Hash.new([])
yield ::Gitlab::APIAuthentication::TokenTypeBuilder.new(strategies)
strategies
end
end
end
end
# frozen_string_literal: true
# See Gitlab::APIAuthentication::Builder
module Gitlab
module APIAuthentication
class SentThroughBuilder
def initialize(strategies, resolvers)
@strategies = strategies
@resolvers = resolvers
end
def sent_through(*locators)
locators.each do |locator|
@strategies[locator] |= @resolvers
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module APIAuthentication
class TokenLocator
UsernameAndPassword = Struct.new(:username, :password)
include ActiveModel::Validations
include ActionController::HttpAuthentication::Basic
attr_reader :location
validates :location, inclusion: { in: %i[http_basic_auth] }
def initialize(location)
@location = location
validate!
end
def extract(request)
case @location
when :http_basic_auth
extract_from_http_basic_auth request
end
end
private
def extract_from_http_basic_auth(request)
username, password = user_name_and_password(request)
return unless username.present? && password.present?
UsernameAndPassword.new(username, password)
end
end
end
end
# frozen_string_literal: true
module Gitlab
module APIAuthentication
class TokenResolver
include ActiveModel::Validations
attr_reader :token_type
validates :token_type, inclusion: { in: %i[personal_access_token job_token deploy_token] }
def initialize(token_type)
@token_type = token_type
validate!
end
# Existing behavior is known to be inconsistent across authentication
# methods with regards to whether to silently ignore present but invalid
# credentials or to raise an error/respond with 401.
#
# If a token can be located from the provided credentials, but the token
# or credentials are in some way invalid, this implementation opts to
# raise an error.
#
# For example, if the raw credentials include a username and password, and
# a token is resolved from the password, but the username does not match
# the token, an error will be raised.
#
# See https://gitlab.com/gitlab-org/gitlab/-/issues/246569
def resolve(raw)
case @token_type
when :personal_access_token
resolve_personal_access_token raw
when :job_token
resolve_job_token raw
when :deploy_token
resolve_deploy_token raw
end
end
private
def resolve_personal_access_token(raw)
# Check if the password is a personal access token
pat = ::PersonalAccessToken.find_by_token(raw.password)
return unless pat
# Ensure that the username matches the token. This check is a subtle
# departure from the existing behavior of #find_personal_access_token_from_http_basic_auth.
# https://gitlab.com/gitlab-org/gitlab/-/merge_requests/38627#note_435907856
raise ::Gitlab::Auth::UnauthorizedError unless pat.user.username == raw.username
pat
end
def resolve_job_token(raw)
# Only look for a job if the username is correct
return if ::Gitlab::Auth::CI_JOB_USER != raw.username
job = ::Ci::AuthJobFinder.new(token: raw.password).execute
# Actively reject credentials with the username `gitlab-ci-token` if
# the password is not a valid job token. This replicates existing
# behavior of #find_user_from_job_token.
raise ::Gitlab::Auth::UnauthorizedError unless job
job
end
def resolve_deploy_token(raw)
# Check if the password is a deploy token
token = ::DeployToken.active.find_by_token(raw.password)
return unless token
# Ensure that the username matches the token. This check is a subtle
# departure from the existing behavior of #deploy_token_from_request.
# https://gitlab.com/gitlab-org/gitlab/-/merge_requests/38627#note_474826205
raise ::Gitlab::Auth::UnauthorizedError unless token.username == raw.username
token
end
end
end
end
# frozen_string_literal: true
# See Gitlab::Auth::AuthBuilder
module Gitlab
module APIAuthentication
class TokenTypeBuilder
def initialize(strategies)
@strategies = strategies
end
def token_types(*resolvers)
::Gitlab::APIAuthentication::SentThroughBuilder.new(@strategies, resolvers)
end
alias_method :token_type, :token_types
end
end
end
...@@ -194,11 +194,15 @@ module Gitlab ...@@ -194,11 +194,15 @@ module Gitlab
def access_token def access_token
strong_memoize(:access_token) do strong_memoize(:access_token) do
# The token can be a PAT or an OAuth (doorkeeper) token if try(:namespace_inheritable, :authentication)
# It is also possible that a PAT is encapsulated in a `Bearer` OAuth token access_token_from_namespace_inheritable
# (e.g. NPM client registry auth), this case will be properly handled else
# by find_personal_access_token # The token can be a PAT or an OAuth (doorkeeper) token
find_oauth_access_token || find_personal_access_token # It is also possible that a PAT is encapsulated in a `Bearer` OAuth token
# (e.g. NPM client registry auth), this case will be properly handled
# by find_personal_access_token
find_oauth_access_token || find_personal_access_token
end
end end
end end
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe API::Helpers::Authentication do
let_it_be(:user) { create(:user) }
let_it_be(:project, reload: true) { create(:project, :public) }
let_it_be(:personal_access_token) { create(:personal_access_token, user: user) }
let_it_be(:deploy_token) { create(:deploy_token, read_package_registry: true, write_package_registry: true) }
describe 'class methods' do
subject { Class.new.include(described_class::ClassMethods).new }
describe '.authenticate_with' do
it 'sets namespace_inheritable :authentication to correctly when body is empty' do
expect(subject).to receive(:namespace_inheritable).with(:authentication, {})
subject.authenticate_with { |allow| }
end
it 'sets namespace_inheritable :authentication to correctly when body is not empty' do
expect(subject).to receive(:namespace_inheritable).with(:authentication, { basic: [:pat, :job], oauth: [:pat, :job] })
subject.authenticate_with { |allow| allow.token_type(:pat, :job).sent_through(:basic, :oauth) }
end
end
end
describe 'helper methods' do
let(:object) do
cls = Class.new
class << cls
def helpers(*modules, &block)
modules.each { |m| include m }
include Module.new.tap { |m| m.class_eval(&block) } if block_given?
end
end
cls.define_method(:unauthorized!) { raise '401' }
cls.define_method(:bad_request!) { |m| raise "400 - #{m}" }
# Include the helper class methods, as instance methods
cls.include described_class::ClassMethods
# Include the methods under test
cls.include described_class
cls.new
end
describe '#token_from_namespace_inheritable' do
let(:object) do
o = super()
o.instance_eval do
# It doesn't matter what this returns as long as the method is defined
def current_request
nil
end
# Spoof Grape's namespace inheritable system
def namespace_inheritable(key, value = nil)
return unless key == :authentication
if value
@authentication = value
else
@authentication
end
end
end
o
end
let(:authentication) do
object.authenticate_with { |allow| allow.token_types(*resolvers).sent_through(*locators) }
end
subject { object.token_from_namespace_inheritable }
before do
# Skip validation of token transports and types to simplify testing
allow(Gitlab::APIAuthentication::TokenLocator).to receive(:new) { |type| type }
allow(Gitlab::APIAuthentication::TokenResolver).to receive(:new) { |type| type }
authentication
end
shared_examples 'stops early' do |response_method|
it "calls ##{response_method}" do
errcls = Class.new(StandardError)
expect(object).to receive(response_method).and_raise(errcls)
expect { subject }.to raise_error(errcls)
end
end
shared_examples 'an anonymous request' do
it 'returns nil' do
expect(subject).to be(nil)
end
end
shared_examples 'an authenticated request' do
it 'returns the token' do
expect(subject).to be(token)
end
end
shared_examples 'an unauthorized request' do
it_behaves_like 'stops early', :unauthorized!
end
context 'with no allowed authentication strategies' do
let(:authentication) { nil }
it_behaves_like 'an anonymous request'
end
context 'with no located credentials' do
let(:locators) { [double(extract: nil)] }
let(:resolvers) { [] }
it_behaves_like 'an anonymous request'
end
context 'with one set of located credentials' do
let(:locators) { [double(extract: true)] }
context 'when the credentials contain a valid token' do
let(:token) { double }
let(:resolvers) { [double(resolve: token)] }
it_behaves_like 'an authenticated request'
end
context 'when the credentials do not contain a valid token' do
let(:resolvers) { [double(resolve: nil)] }
it_behaves_like 'an unauthorized request'
end
end
context 'with multiple located credentials' do
let(:locators) { [double(extract: true), double(extract: true)] }
let(:resolvers) { [] }
it_behaves_like 'stops early', :bad_request!
end
context 'when a resolver raises UnauthorizedError' do
let(:locators) { [double(extract: true)] }
let(:resolvers) do
r = double
expect(r).to receive(:resolve).and_raise(Gitlab::Auth::UnauthorizedError)
r
end
it_behaves_like 'an unauthorized request'
end
end
describe '#access_token_from_namespace_inheritable' do
subject { object.access_token_from_namespace_inheritable }
it 'returns #token_from_namespace_inheritable if it is a personal access token' do
expect(object).to receive(:token_from_namespace_inheritable).and_return(personal_access_token)
expect(subject).to be(personal_access_token)
end
it 'returns nil if #token_from_namespace_inheritable is not a personal access token' do
token = double
expect(object).to receive(:token_from_namespace_inheritable).and_return(token)
expect(subject).to be(nil)
end
end
describe '#user_from_namespace_inheritable' do
subject { object.user_from_namespace_inheritable }
it 'returns #token_from_namespace_inheritable if it is a deploy token' do
expect(object).to receive(:token_from_namespace_inheritable).and_return(deploy_token)
expect(subject).to be(deploy_token)
end
it 'returns #token_from_namespace_inheritable.user if the token is not a deploy token' do
user = double
token = double(user: user)
expect(object).to receive(:token_from_namespace_inheritable).and_return(token)
expect(subject).to be(user)
end
it 'falls back to #find_user_from_warden if #token_from_namespace_inheritable.user is nil' do
token = double(user: nil)
expect(object).to receive(:token_from_namespace_inheritable).and_return(token)
subject
end
it 'falls back to #find_user_from_warden if #token_from_namespace_inheritable is nil' do
expect(object).to receive(:token_from_namespace_inheritable).and_return(nil)
subject
end
end
end
end
# frozen_string_literal: true
require 'fast_spec_helper'
RSpec.describe Gitlab::APIAuthentication::Builder do
describe '#build' do
shared_examples 'builds the correct result' do |token_type:, sent_through:, builds:|
context "with #{token_type.size} token type(s) and #{sent_through.size} sent through(s)" do
it 'works when passed together' do
strategies = described_class.new.build { |allow| allow.token_types(*token_type).sent_through(*sent_through) }
expect(strategies).to eq(builds)
end
it 'works when token types are passed separately' do
strategies = described_class.new.build { |allow| token_type.each { |t| allow.token_types(t).sent_through(*sent_through) } }
expect(strategies).to eq(builds)
end
it 'works when sent throughs are passed separately' do
strategies = described_class.new.build { |allow| sent_through.each { |s| allow.token_types(*token_type).sent_through(s) } }
expect(strategies).to eq(builds)
end
it 'works when token types and sent throughs are passed separately' do
strategies = described_class.new.build { |allow| token_type.each { |t| sent_through.each { |s| allow.token_types(t).sent_through(s) } } }
expect(strategies).to eq(builds)
end
end
end
it_behaves_like 'builds the correct result',
token_type: [:pat],
sent_through: [:basic],
builds: { basic: [:pat] }
it_behaves_like 'builds the correct result',
token_type: [:pat],
sent_through: [:basic, :oauth],
builds: { basic: [:pat], oauth: [:pat] }
it_behaves_like 'builds the correct result',
token_type: [:pat, :job],
sent_through: [:basic],
builds: { basic: [:pat, :job] }
it_behaves_like 'builds the correct result',
token_type: [:pat, :job],
sent_through: [:basic, :oauth],
builds: { basic: [:pat, :job], oauth: [:pat, :job] }
context 'with a complex auth strategy' do
it 'builds the correct result' do
strategies = described_class.new.build do |allow|
allow.token_types(:pat, :job, :deploy).sent_through(:http_basic, :oauth)
allow.token_types(:pat).sent_through(:http_private, :query_private)
allow.token_types(:oauth2).sent_through(:http_bearer, :query_access)
end
expect(strategies).to eq({
http_basic: [:pat, :job, :deploy],
oauth: [:pat, :job, :deploy],
http_private: [:pat],
query_private: [:pat],
http_bearer: [:oauth2],
query_access: [:oauth2]
})
end
end
end
end
# frozen_string_literal: true
require 'fast_spec_helper'
RSpec.describe Gitlab::APIAuthentication::SentThroughBuilder do
describe '#sent_through' do
let(:resolvers) { Array.new(3) { double } }
let(:locators) { Array.new(3) { double } }
it 'adds a strategy for each of locators x resolvers' do
strategies = locators.to_h { |l| [l, []] }
described_class.new(strategies, resolvers).sent_through(*locators)
expect(strategies).to eq(locators.to_h { |l| [l, resolvers] })
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::APIAuthentication::TokenLocator do
let_it_be(:user) { create(:user) }
let_it_be(:project, reload: true) { create(:project, :public) }
let_it_be(:personal_access_token) { create(:personal_access_token, user: user) }
let_it_be(:ci_job) { create(:ci_build, project: project, user: user, status: :running) }
let_it_be(:ci_job_done) { create(:ci_build, project: project, user: user, status: :success) }
let_it_be(:deploy_token) { create(:deploy_token, read_package_registry: true, write_package_registry: true) }
describe '.new' do
context 'with a valid type' do
it 'creates a new instance' do
expect(described_class.new(:http_basic_auth)).to be_a(described_class)
end
end
context 'with an invalid type' do
it 'raises ActiveModel::ValidationError' do
expect { described_class.new(:not_a_real_locator) }.to raise_error(ActiveModel::ValidationError)
end
end
end
describe '#extract' do
let(:locator) { described_class.new(type) }
subject { locator.extract(request) }
context 'with :http_basic_auth' do
let(:type) { :http_basic_auth }
context 'without credentials' do
let(:request) { double(authorization: nil) }
it 'returns nil' do
expect(subject).to be(nil)
end
end
context 'with credentials' do
let(:username) { 'foo' }
let(:password) { 'bar' }
let(:request) { double(authorization: "Basic #{::Base64.strict_encode64("#{username}:#{password}")}") }
it 'returns the credentials' do
expect(subject.username).to eq(username)
expect(subject.password).to eq(password)
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::APIAuthentication::TokenResolver do
let_it_be(:user) { create(:user) }
let_it_be(:project, reload: true) { create(:project, :public) }
let_it_be(:personal_access_token) { create(:personal_access_token, user: user) }
let_it_be(:ci_job) { create(:ci_build, project: project, user: user, status: :running) }
let_it_be(:ci_job_done) { create(:ci_build, project: project, user: user, status: :success) }
let_it_be(:deploy_token) { create(:deploy_token, read_package_registry: true, write_package_registry: true) }
shared_examples 'an authorized request' do
it 'returns the correct token' do
expect(subject).to eq(token)
end
end
shared_examples 'an unauthorized request' do
it 'raises an error' do
expect { subject }.to raise_error(Gitlab::Auth::UnauthorizedError)
end
end
shared_examples 'an anoymous request' do
it 'returns nil' do
expect(subject).to eq(nil)
end
end
describe '.new' do
context 'with a valid type' do
it 'creates a new instance' do
expect(described_class.new(:personal_access_token)).to be_a(described_class)
end
end
context 'with an invalid type' do
it 'raises a validation error' do
expect { described_class.new(:not_a_real_locator) }.to raise_error(ActiveModel::ValidationError)
end
end
end
describe '#resolve' do
let(:resolver) { described_class.new(type) }
subject { resolver.resolve(raw) }
context 'with :personal_access_token' do
let(:type) { :personal_access_token }
let(:token) { personal_access_token }
context 'with valid credentials' do
let(:raw) { username_and_password(user.username, token.token) }
it_behaves_like 'an authorized request'
end
context 'with an invalid username' do
let(:raw) { username_and_password("not-my-#{user.username}", token.token) }
it_behaves_like 'an unauthorized request'
end
end
context 'with :job_token' do
let(:type) { :job_token }
let(:token) { ci_job }
context 'with valid credentials' do
let(:raw) { username_and_password(Gitlab::Auth::CI_JOB_USER, token.token) }
it_behaves_like 'an authorized request'
end
context 'when the job is not running' do
let(:raw) { username_and_password(Gitlab::Auth::CI_JOB_USER, ci_job_done.token) }
it_behaves_like 'an unauthorized request'
end
context 'with the wrong username' do
let(:raw) { username_and_password("not-#{Gitlab::Auth::CI_JOB_USER}", nil) }
it_behaves_like 'an anoymous request'
end
context 'with an invalid job token' do
let(:raw) { username_and_password(Gitlab::Auth::CI_JOB_USER, "not a valid CI job token") }
it_behaves_like 'an unauthorized request'
end
end
context 'with :deploy_token' do
let(:type) { :deploy_token }
let(:token) { deploy_token }
context 'with a valid deploy token' do
let(:raw) { username_and_password(token.username, token.token) }
it_behaves_like 'an authorized request'
end
context 'with an invalid username' do
let(:raw) { username_and_password("not-my-#{token.username}", token.token) }
it_behaves_like 'an unauthorized request'
end
end
end
def username_and_password(username, password)
::Gitlab::APIAuthentication::TokenLocator::UsernameAndPassword.new(username, password)
end
end
# frozen_string_literal: true
require 'fast_spec_helper'
RSpec.describe Gitlab::APIAuthentication::TokenTypeBuilder do
describe '#token_types' do
it 'passes strategies and resolvers to SentThroughBuilder' do
strategies = double
resolvers = Array.new(3) { double }
retval = double
expect(Gitlab::APIAuthentication::SentThroughBuilder).to receive(:new).with(strategies, resolvers).and_return(retval)
expect(described_class.new(strategies).token_types(*resolvers)).to be(retval)
end
end
end
...@@ -49,12 +49,12 @@ RSpec.describe API::NugetProjectPackages do ...@@ -49,12 +49,12 @@ RSpec.describe API::NugetProjectPackages do
where(:visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do where(:visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do
'PUBLIC' | :developer | true | true | 'process nuget download versions request' | :success 'PUBLIC' | :developer | true | true | 'process nuget download versions request' | :success
'PUBLIC' | :guest | true | true | 'process nuget download versions request' | :success 'PUBLIC' | :guest | true | true | 'process nuget download versions request' | :success
'PUBLIC' | :developer | true | false | 'process nuget download versions request' | :success 'PUBLIC' | :developer | true | false | 'rejects nuget packages access' | :unauthorized
'PUBLIC' | :guest | true | false | 'process nuget download versions request' | :success 'PUBLIC' | :guest | true | false | 'rejects nuget packages access' | :unauthorized
'PUBLIC' | :developer | false | true | 'process nuget download versions request' | :success 'PUBLIC' | :developer | false | true | 'process nuget download versions request' | :success
'PUBLIC' | :guest | false | true | 'process nuget download versions request' | :success 'PUBLIC' | :guest | false | true | 'process nuget download versions request' | :success
'PUBLIC' | :developer | false | false | 'process nuget download versions request' | :success 'PUBLIC' | :developer | false | false | 'rejects nuget packages access' | :unauthorized
'PUBLIC' | :guest | false | false | 'process nuget download versions request' | :success 'PUBLIC' | :guest | false | false | 'rejects nuget packages access' | :unauthorized
'PUBLIC' | :anonymous | false | true | 'process nuget download versions request' | :success 'PUBLIC' | :anonymous | false | true | 'process nuget download versions request' | :success
'PRIVATE' | :developer | true | true | 'process nuget download versions request' | :success 'PRIVATE' | :developer | true | true | 'process nuget download versions request' | :success
'PRIVATE' | :guest | true | true | 'rejects nuget packages access' | :forbidden 'PRIVATE' | :guest | true | true | 'rejects nuget packages access' | :forbidden
...@@ -100,12 +100,12 @@ RSpec.describe API::NugetProjectPackages do ...@@ -100,12 +100,12 @@ RSpec.describe API::NugetProjectPackages do
where(:visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do where(:visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do
'PUBLIC' | :developer | true | true | 'process nuget download content request' | :success 'PUBLIC' | :developer | true | true | 'process nuget download content request' | :success
'PUBLIC' | :guest | true | true | 'process nuget download content request' | :success 'PUBLIC' | :guest | true | true | 'process nuget download content request' | :success
'PUBLIC' | :developer | true | false | 'process nuget download content request' | :success 'PUBLIC' | :developer | true | false | 'rejects nuget packages access' | :unauthorized
'PUBLIC' | :guest | true | false | 'process nuget download content request' | :success 'PUBLIC' | :guest | true | false | 'rejects nuget packages access' | :unauthorized
'PUBLIC' | :developer | false | true | 'process nuget download content request' | :success 'PUBLIC' | :developer | false | true | 'process nuget download content request' | :success
'PUBLIC' | :guest | false | true | 'process nuget download content request' | :success 'PUBLIC' | :guest | false | true | 'process nuget download content request' | :success
'PUBLIC' | :developer | false | false | 'process nuget download content request' | :success 'PUBLIC' | :developer | false | false | 'rejects nuget packages access' | :unauthorized
'PUBLIC' | :guest | false | false | 'process nuget download content request' | :success 'PUBLIC' | :guest | false | false | 'rejects nuget packages access' | :unauthorized
'PUBLIC' | :anonymous | false | true | 'process nuget download content request' | :success 'PUBLIC' | :anonymous | false | true | 'process nuget download content request' | :success
'PRIVATE' | :developer | true | true | 'process nuget download content request' | :success 'PRIVATE' | :developer | true | true | 'process nuget download content request' | :success
'PRIVATE' | :guest | true | true | 'rejects nuget packages access' | :forbidden 'PRIVATE' | :guest | true | true | 'rejects nuget packages access' | :forbidden
......
...@@ -10,12 +10,12 @@ RSpec.shared_examples 'handling nuget service requests' do |anonymous_requests_e ...@@ -10,12 +10,12 @@ RSpec.shared_examples 'handling nuget service requests' do |anonymous_requests_e
where(:visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do where(:visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do
'PUBLIC' | :developer | true | true | 'process nuget service index request' | :success 'PUBLIC' | :developer | true | true | 'process nuget service index request' | :success
'PUBLIC' | :guest | true | true | 'process nuget service index request' | :success 'PUBLIC' | :guest | true | true | 'process nuget service index request' | :success
'PUBLIC' | :developer | true | false | anonymous_requests_example_name | anonymous_requests_status 'PUBLIC' | :developer | true | false | 'rejects nuget packages access' | :unauthorized
'PUBLIC' | :guest | true | false | anonymous_requests_example_name | anonymous_requests_status 'PUBLIC' | :guest | true | false | 'rejects nuget packages access' | :unauthorized
'PUBLIC' | :developer | false | true | 'process nuget service index request' | :success 'PUBLIC' | :developer | false | true | 'process nuget service index request' | :success
'PUBLIC' | :guest | false | true | 'process nuget service index request' | :success 'PUBLIC' | :guest | false | true | 'process nuget service index request' | :success
'PUBLIC' | :developer | false | false | anonymous_requests_example_name | anonymous_requests_status 'PUBLIC' | :developer | false | false | 'rejects nuget packages access' | :unauthorized
'PUBLIC' | :guest | false | false | anonymous_requests_example_name | anonymous_requests_status 'PUBLIC' | :guest | false | false | 'rejects nuget packages access' | :unauthorized
'PUBLIC' | :anonymous | false | true | anonymous_requests_example_name | anonymous_requests_status 'PUBLIC' | :anonymous | false | true | anonymous_requests_example_name | anonymous_requests_status
'PRIVATE' | :developer | true | true | 'process nuget service index request' | :success 'PRIVATE' | :developer | true | true | 'process nuget service index request' | :success
'PRIVATE' | :guest | true | true | 'rejects nuget packages access' | :forbidden 'PRIVATE' | :guest | true | true | 'rejects nuget packages access' | :forbidden
...@@ -109,12 +109,12 @@ RSpec.shared_examples 'handling nuget metadata requests with package name' do |a ...@@ -109,12 +109,12 @@ RSpec.shared_examples 'handling nuget metadata requests with package name' do |a
where(:visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do where(:visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do
'PUBLIC' | :developer | true | true | 'process nuget metadata request at package name level' | :success 'PUBLIC' | :developer | true | true | 'process nuget metadata request at package name level' | :success
'PUBLIC' | :guest | true | true | 'process nuget metadata request at package name level' | :success 'PUBLIC' | :guest | true | true | 'process nuget metadata request at package name level' | :success
'PUBLIC' | :developer | true | false | anonymous_requests_example_name | anonymous_requests_status 'PUBLIC' | :developer | true | false | 'rejects nuget packages access' | :unauthorized
'PUBLIC' | :guest | true | false | anonymous_requests_example_name | anonymous_requests_status 'PUBLIC' | :guest | true | false | 'rejects nuget packages access' | :unauthorized
'PUBLIC' | :developer | false | true | 'process nuget metadata request at package name level' | :success 'PUBLIC' | :developer | false | true | 'process nuget metadata request at package name level' | :success
'PUBLIC' | :guest | false | true | 'process nuget metadata request at package name level' | :success 'PUBLIC' | :guest | false | true | 'process nuget metadata request at package name level' | :success
'PUBLIC' | :developer | false | false | anonymous_requests_example_name | anonymous_requests_status 'PUBLIC' | :developer | false | false | 'rejects nuget packages access' | :unauthorized
'PUBLIC' | :guest | false | false | anonymous_requests_example_name | anonymous_requests_status 'PUBLIC' | :guest | false | false | 'rejects nuget packages access' | :unauthorized
'PUBLIC' | :anonymous | false | true | anonymous_requests_example_name | anonymous_requests_status 'PUBLIC' | :anonymous | false | true | anonymous_requests_example_name | anonymous_requests_status
'PRIVATE' | :developer | true | true | 'process nuget metadata request at package name level' | :success 'PRIVATE' | :developer | true | true | 'process nuget metadata request at package name level' | :success
'PRIVATE' | :guest | true | true | 'rejects nuget packages access' | :forbidden 'PRIVATE' | :guest | true | true | 'rejects nuget packages access' | :forbidden
...@@ -171,12 +171,12 @@ RSpec.shared_examples 'handling nuget metadata requests with package name and pa ...@@ -171,12 +171,12 @@ RSpec.shared_examples 'handling nuget metadata requests with package name and pa
where(:visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do where(:visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do
'PUBLIC' | :developer | true | true | 'process nuget metadata request at package name and package version level' | :success 'PUBLIC' | :developer | true | true | 'process nuget metadata request at package name and package version level' | :success
'PUBLIC' | :guest | true | true | 'process nuget metadata request at package name and package version level' | :success 'PUBLIC' | :guest | true | true | 'process nuget metadata request at package name and package version level' | :success
'PUBLIC' | :developer | true | false | anonymous_requests_example_name | anonymous_requests_status 'PUBLIC' | :developer | true | false | 'rejects nuget packages access' | :unauthorized
'PUBLIC' | :guest | true | false | anonymous_requests_example_name | anonymous_requests_status 'PUBLIC' | :guest | true | false | 'rejects nuget packages access' | :unauthorized
'PUBLIC' | :developer | false | true | 'process nuget metadata request at package name and package version level' | :success 'PUBLIC' | :developer | false | true | 'process nuget metadata request at package name and package version level' | :success
'PUBLIC' | :guest | false | true | 'process nuget metadata request at package name and package version level' | :success 'PUBLIC' | :guest | false | true | 'process nuget metadata request at package name and package version level' | :success
'PUBLIC' | :developer | false | false | anonymous_requests_example_name | anonymous_requests_status 'PUBLIC' | :developer | false | false | 'rejects nuget packages access' | :unauthorized
'PUBLIC' | :guest | false | false | anonymous_requests_example_name | anonymous_requests_status 'PUBLIC' | :guest | false | false | 'rejects nuget packages access' | :unauthorized
'PUBLIC' | :anonymous | false | true | anonymous_requests_example_name | anonymous_requests_status 'PUBLIC' | :anonymous | false | true | anonymous_requests_example_name | anonymous_requests_status
'PRIVATE' | :developer | true | true | 'process nuget metadata request at package name and package version level' | :success 'PRIVATE' | :developer | true | true | 'process nuget metadata request at package name and package version level' | :success
'PRIVATE' | :guest | true | true | 'rejects nuget packages access' | :forbidden 'PRIVATE' | :guest | true | true | 'rejects nuget packages access' | :forbidden
...@@ -235,12 +235,12 @@ RSpec.shared_examples 'handling nuget search requests' do |anonymous_requests_ex ...@@ -235,12 +235,12 @@ RSpec.shared_examples 'handling nuget search requests' do |anonymous_requests_ex
where(:visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do where(:visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do
'PUBLIC' | :developer | true | true | 'process nuget search request' | :success 'PUBLIC' | :developer | true | true | 'process nuget search request' | :success
'PUBLIC' | :guest | true | true | 'process nuget search request' | :success 'PUBLIC' | :guest | true | true | 'process nuget search request' | :success
'PUBLIC' | :developer | true | false | anonymous_requests_example_name | anonymous_requests_status 'PUBLIC' | :developer | true | false | 'rejects nuget packages access' | :unauthorized
'PUBLIC' | :guest | true | false | anonymous_requests_example_name | anonymous_requests_status 'PUBLIC' | :guest | true | false | 'rejects nuget packages access' | :unauthorized
'PUBLIC' | :developer | false | true | 'process nuget search request' | :success 'PUBLIC' | :developer | false | true | 'process nuget search request' | :success
'PUBLIC' | :guest | false | true | 'process nuget search request' | :success 'PUBLIC' | :guest | false | true | 'process nuget search request' | :success
'PUBLIC' | :developer | false | false | anonymous_requests_example_name | anonymous_requests_status 'PUBLIC' | :developer | false | false | 'rejects nuget packages access' | :unauthorized
'PUBLIC' | :guest | false | false | anonymous_requests_example_name | anonymous_requests_status 'PUBLIC' | :guest | false | false | 'rejects nuget packages access' | :unauthorized
'PUBLIC' | :anonymous | false | true | anonymous_requests_example_name | anonymous_requests_status 'PUBLIC' | :anonymous | false | true | anonymous_requests_example_name | anonymous_requests_status
'PRIVATE' | :developer | true | true | 'process nuget search request' | :success 'PRIVATE' | :developer | true | true | 'process nuget search request' | :success
'PRIVATE' | :guest | true | true | 'rejects nuget packages access' | :forbidden 'PRIVATE' | :guest | true | true | 'rejects nuget packages access' | :forbidden
......
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