Commit dcf8011a authored by Jonas Wälter's avatar Jonas Wälter Committed by Markus Koller

Extend basic authentication detection for rate limiting

Detects the following forms of basic authentication in addition:
* <username>:<password>
* <username>:<personal_access_token>
* <username>:<lfs_token>

Changelog: fixed
parent 6dcfb73a
...@@ -89,6 +89,32 @@ module Gitlab ...@@ -89,6 +89,32 @@ module Gitlab
job.user job.user
end end
def find_user_from_basic_auth_password
return unless has_basic_credentials?(current_request)
login, password = user_name_and_password(current_request)
return if ::Gitlab::Auth::CI_JOB_USER == login
Gitlab::Auth.find_with_user_password(login, password)
end
def find_user_from_lfs_token
return unless has_basic_credentials?(current_request)
login, token = user_name_and_password(current_request)
user = User.by_login(login)
user if user && Gitlab::LfsToken.new(user).token_valid?(token)
end
def find_user_from_personal_access_token
return unless access_token
validate_access_token!
access_token&.user || raise(UnauthorizedError)
end
# We allow Private Access Tokens with `api` scope to be used by web # We allow Private Access Tokens with `api` scope to be used by web
# requests on RSS feeds or ICS files for backwards compatibility. # requests on RSS feeds or ICS files for backwards compatibility.
# It is also used by GraphQL/API requests. # It is also used by GraphQL/API requests.
...@@ -308,6 +334,10 @@ module Gitlab ...@@ -308,6 +334,10 @@ module Gitlab
current_request.path.starts_with?(Gitlab::Utils.append_path(Gitlab.config.gitlab.relative_url_root, '/api/')) current_request.path.starts_with?(Gitlab::Utils.append_path(Gitlab.config.gitlab.relative_url_root, '/api/'))
end end
def git_request?
Gitlab::PathRegex.repository_git_route_regex.match?(current_request.path)
end
def archive_request? def archive_request?
current_request.path.include?('/-/archive/') current_request.path.include?('/-/archive/')
end end
......
...@@ -34,7 +34,10 @@ module Gitlab ...@@ -34,7 +34,10 @@ module Gitlab
find_user_from_feed_token(request_format) || find_user_from_feed_token(request_format) ||
find_user_from_static_object_token(request_format) || find_user_from_static_object_token(request_format) ||
find_user_from_basic_auth_job || find_user_from_basic_auth_job ||
find_user_from_job_token find_user_from_job_token ||
find_user_from_lfs_token ||
find_user_from_personal_access_token ||
find_user_from_basic_auth_password
rescue Gitlab::Auth::AuthenticationError rescue Gitlab::Auth::AuthenticationError
nil nil
end end
...@@ -58,7 +61,7 @@ module Gitlab ...@@ -58,7 +61,7 @@ module Gitlab
def route_authentication_setting def route_authentication_setting
@route_authentication_setting ||= { @route_authentication_setting ||= {
job_token_allowed: api_request?, job_token_allowed: api_request?,
basic_auth_personal_access_token: api_request? basic_auth_personal_access_token: api_request? || git_request?
} }
end end
end end
......
...@@ -708,6 +708,122 @@ RSpec.describe Gitlab::Auth::AuthFinders do ...@@ -708,6 +708,122 @@ RSpec.describe Gitlab::Auth::AuthFinders do
end end
end end
describe '#find_user_from_basic_auth_password' do
subject { find_user_from_basic_auth_password }
context 'when the request does not have AUTHORIZATION header' do
it { is_expected.to be_nil }
end
it 'returns nil without user and password' do
set_basic_auth_header(nil, nil)
is_expected.to be_nil
end
it 'returns nil without password' do
set_basic_auth_header('some-user', nil)
is_expected.to be_nil
end
it 'returns nil without user' do
set_basic_auth_header(nil, 'password')
is_expected.to be_nil
end
it 'returns nil with CI username' do
set_basic_auth_header(::Gitlab::Auth::CI_JOB_USER, 'password')
is_expected.to be_nil
end
it 'returns nil with wrong password' do
set_basic_auth_header(user.username, 'wrong-password')
is_expected.to be_nil
end
it 'returns user with correct credentials' do
set_basic_auth_header(user.username, user.password)
is_expected.to eq(user)
end
end
describe '#find_user_from_lfs_token' do
subject { find_user_from_lfs_token }
context 'when the request does not have AUTHORIZATION header' do
it { is_expected.to be_nil }
end
it 'returns nil without user and token' do
set_basic_auth_header(nil, nil)
is_expected.to be_nil
end
it 'returns nil without token' do
set_basic_auth_header('some-user', nil)
is_expected.to be_nil
end
it 'returns nil without user' do
set_basic_auth_header(nil, 'token')
is_expected.to be_nil
end
it 'returns nil with wrong token' do
set_basic_auth_header(user.username, 'wrong-token')
is_expected.to be_nil
end
it 'returns user with correct user and correct token' do
lfs_token = Gitlab::LfsToken.new(user).token
set_basic_auth_header(user.username, lfs_token)
is_expected.to eq(user)
end
it 'returns nil with wrong user and correct token' do
lfs_token = Gitlab::LfsToken.new(user).token
other_user = create(:user)
set_basic_auth_header(other_user.username, lfs_token)
is_expected.to be_nil
end
end
describe '#find_user_from_personal_access_token' do
subject { find_user_from_personal_access_token }
it 'returns nil without access token' do
allow_any_instance_of(described_class).to receive(:access_token).and_return(nil)
is_expected.to be_nil
end
it 'returns user with correct access token' do
personal_access_token = create(:personal_access_token, user: user)
allow_any_instance_of(described_class).to receive(:access_token).and_return(personal_access_token)
is_expected.to eq(user)
end
it 'returns exception if access token has no user' do
personal_access_token = create(:personal_access_token, user: user)
allow_any_instance_of(described_class).to receive(:access_token).and_return(personal_access_token)
allow_any_instance_of(PersonalAccessToken).to receive(:user).and_return(nil)
expect { subject }.to raise_error(Gitlab::Auth::UnauthorizedError)
end
end
describe '#validate_access_token!' do describe '#validate_access_token!' do
subject { validate_access_token! } subject { validate_access_token! }
......
...@@ -45,6 +45,9 @@ RSpec.describe Gitlab::Auth::RequestAuthenticator do ...@@ -45,6 +45,9 @@ RSpec.describe Gitlab::Auth::RequestAuthenticator do
let!(:feed_token_user) { build(:user) } let!(:feed_token_user) { build(:user) }
let!(:static_object_token_user) { build(:user) } let!(:static_object_token_user) { build(:user) }
let!(:job_token_user) { build(:user) } let!(:job_token_user) { build(:user) }
let!(:lfs_token_user) { build(:user) }
let!(:basic_auth_access_token_user) { build(:user) }
let!(:basic_auth_password_user) { build(:user) }
it 'returns access_token user first' do it 'returns access_token user first' do
allow_any_instance_of(described_class).to receive(:find_user_from_web_access_token) allow_any_instance_of(described_class).to receive(:find_user_from_web_access_token)
...@@ -78,6 +81,30 @@ RSpec.describe Gitlab::Auth::RequestAuthenticator do ...@@ -78,6 +81,30 @@ RSpec.describe Gitlab::Auth::RequestAuthenticator do
expect(subject.find_sessionless_user(:api)).to eq job_token_user expect(subject.find_sessionless_user(:api)).to eq job_token_user
end end
it 'returns lfs_token user if no job_token user found' do
allow_any_instance_of(described_class)
.to receive(:find_user_from_lfs_token)
.and_return(lfs_token_user)
expect(subject.find_sessionless_user(:api)).to eq lfs_token_user
end
it 'returns basic_auth_access_token user if no lfs_token user found' do
allow_any_instance_of(described_class)
.to receive(:find_user_from_personal_access_token)
.and_return(basic_auth_access_token_user)
expect(subject.find_sessionless_user(:api)).to eq basic_auth_access_token_user
end
it 'returns basic_auth_access_password user if no basic_auth_access_token user found' do
allow_any_instance_of(described_class)
.to receive(:find_user_from_basic_auth_password)
.and_return(basic_auth_password_user)
expect(subject.find_sessionless_user(:api)).to eq basic_auth_password_user
end
it 'returns nil if no user found' do it 'returns nil if no user found' do
expect(subject.find_sessionless_user(:api)).to be_blank expect(subject.find_sessionless_user(:api)).to be_blank
end end
...@@ -194,4 +221,27 @@ RSpec.describe Gitlab::Auth::RequestAuthenticator do ...@@ -194,4 +221,27 @@ RSpec.describe Gitlab::Auth::RequestAuthenticator do
expect(subject.runner).to be_blank expect(subject.runner).to be_blank
end end
end end
describe '#route_authentication_setting' do
using RSpec::Parameterized::TableSyntax
where(:script_name, :expected_job_token_allowed, :expected_basic_auth_personal_access_token) do
'/api/endpoint' | true | true
'/namespace/project.git' | false | true
'/web/endpoint' | false | false
end
with_them do
before do
env['SCRIPT_NAME'] = script_name
end
it 'returns correct settings' do
expect(subject.send(:route_authentication_setting)).to eql({
job_token_allowed: expected_job_token_allowed,
basic_auth_personal_access_token: expected_basic_auth_personal_access_token
})
end
end
end
end end
...@@ -677,4 +677,118 @@ RSpec.describe 'Rack Attack global throttles', :use_clean_rails_memory_store_cac ...@@ -677,4 +677,118 @@ RSpec.describe 'Rack Attack global throttles', :use_clean_rails_memory_store_cac
it_behaves_like 'reject requests over the rate limit' it_behaves_like 'reject requests over the rate limit'
end end
end end
describe 'Gitlab::RackAttack::Request#unauthenticated?' do
let_it_be(:url) { "/api/v4/projects" }
let_it_be(:user) { create(:user) }
def expect_unauthenticated_request
expect_next_instance_of(Rack::Attack::Request) do |instance|
expect(instance.unauthenticated?).to be true
end
end
def expect_authenticated_request
expect_next_instance_of(Rack::Attack::Request) do |instance|
expect(instance.unauthenticated?).to be false
end
end
before do
settings_to_set[:throttle_unauthenticated_enabled] = true
stub_application_setting(settings_to_set)
end
context 'without authentication' do
it 'request is unauthenticated' do
expect_unauthenticated_request
get url
end
end
context 'authenticated by a runner token' do
let_it_be(:runner) { create(:ci_runner) }
it 'request is authenticated' do
expect_authenticated_request
get url, params: { token: runner.token }
end
end
context 'authenticated with personal access token' do
let_it_be(:personal_access_token) { create(:personal_access_token, user: user) }
it 'request is authenticated by token in query string' do
expect_authenticated_request
get url, params: { private_token: personal_access_token.token }
end
it 'request is authenticated by token in the headers' do
expect_authenticated_request
get url, headers: personal_access_token_headers(personal_access_token)
end
it 'request is authenticated by token in the OAuth headers' do
expect_authenticated_request
get url, headers: oauth_token_headers(personal_access_token)
end
it 'request is authenticated by token in basic auth' do
expect_authenticated_request
get url, headers: basic_auth_headers(user, personal_access_token)
end
end
context 'authenticated with OAuth token' do
let(:application) { Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user) }
let(:oauth_token) { Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id, scopes: "api") }
it 'request is authenticated by token in query string' do
expect_authenticated_request
get url, params: { access_token: oauth_token.token }
end
it 'request is authenticated by token in the headers' do
expect_authenticated_request
get url, headers: oauth_token_headers(oauth_token)
end
end
context 'authenticated with lfs token' do
it 'request is authenticated by token in basic auth' do
lfs_token = Gitlab::LfsToken.new(user)
encoded_login = ["#{user.username}:#{lfs_token.token}"].pack('m0')
expect_authenticated_request
get url, headers: { 'AUTHORIZATION' => "Basic #{encoded_login}" }
end
end
context 'authenticated with regular login' do
it 'request is authenticated after login' do
login_as(user)
expect_authenticated_request
get url
end
it 'request is authenticated by credentials in basic auth' do
encoded_login = ["#{user.username}:#{user.password}"].pack('m0')
expect_authenticated_request
get url, headers: { 'AUTHORIZATION' => "Basic #{encoded_login}" }
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