Commit 62918264 authored by GitLab Release Tools Bot's avatar GitLab Release Tools Bot

Merge branch 'security-342481-deploy-token-support-rack-attack-14-10' into '14-10-stable-ee'

Allow rate limiting of deploy tokens

See merge request gitlab-org/security/gitlab!2395
parents 8f00846e 8de55091
...@@ -13,6 +13,10 @@ module Gitlab ...@@ -13,6 +13,10 @@ module Gitlab
@request = request @request = request
end end
def find_authenticated_requester(request_formats)
user(request_formats) || deploy_token_from_request
end
def user(request_formats) def user(request_formats)
request_formats.each do |format| request_formats.each do |format|
user = find_sessionless_user(format) user = find_sessionless_user(format)
...@@ -84,7 +88,8 @@ module Gitlab ...@@ -84,7 +88,8 @@ 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? || git_request? basic_auth_personal_access_token: api_request? || git_request?,
deploy_token_allowed: api_request? || git_request?
} }
end end
......
...@@ -73,12 +73,16 @@ module Gitlab ...@@ -73,12 +73,16 @@ module Gitlab
matched: req.env['rack.attack.matched'] matched: req.env['rack.attack.matched']
} }
if THROTTLES_WITH_USER_INFORMATION.include? req.env['rack.attack.matched'].to_sym discriminator = req.env['rack.attack.match_discriminator'].to_s
user_id = req.env['rack.attack.match_discriminator'] discriminator_id = discriminator.split(':').last
user = User.find_by(id: user_id) # rubocop:disable CodeReuse/ActiveRecord
rack_attack_info[:user_id] = user_id if discriminator.starts_with?('user:')
user = User.find_by(id: discriminator_id) # rubocop:disable CodeReuse/ActiveRecord
rack_attack_info[:user_id] = discriminator_id.to_i
rack_attack_info['meta.user'] = user.username unless user.nil? rack_attack_info['meta.user'] = user.username unless user.nil?
elsif discriminator.starts_with?('deploy_token:')
rack_attack_info[:deploy_token_id] = discriminator_id.to_i
end end
Gitlab::InstrumentationHelper.add_instrumentation_data(rack_attack_info) Gitlab::InstrumentationHelper.add_instrumentation_data(rack_attack_info)
......
...@@ -95,7 +95,7 @@ module Gitlab ...@@ -95,7 +95,7 @@ module Gitlab
authenticated_options = Gitlab::Throttle.options(throttle, authenticated: true) authenticated_options = Gitlab::Throttle.options(throttle, authenticated: true)
throttle_or_track(rack_attack, "throttle_authenticated_#{throttle}", authenticated_options) do |req| throttle_or_track(rack_attack, "throttle_authenticated_#{throttle}", authenticated_options) do |req|
if req.throttle?(throttle, authenticated: true) if req.throttle?(throttle, authenticated: true)
req.throttled_user_id([:api]) req.throttled_identifer([:api])
end end
end end
end end
...@@ -117,7 +117,7 @@ module Gitlab ...@@ -117,7 +117,7 @@ module Gitlab
throttle_or_track(rack_attack, 'throttle_authenticated_web', Gitlab::Throttle.authenticated_web_options) do |req| throttle_or_track(rack_attack, 'throttle_authenticated_web', Gitlab::Throttle.authenticated_web_options) do |req|
if req.throttle_authenticated_web? if req.throttle_authenticated_web?
req.throttled_user_id([:api, :rss, :ics]) req.throttled_identifer([:api, :rss, :ics])
end end
end end
...@@ -129,19 +129,19 @@ module Gitlab ...@@ -129,19 +129,19 @@ module Gitlab
throttle_or_track(rack_attack, 'throttle_authenticated_protected_paths_api', Gitlab::Throttle.protected_paths_options) do |req| throttle_or_track(rack_attack, 'throttle_authenticated_protected_paths_api', Gitlab::Throttle.protected_paths_options) do |req|
if req.throttle_authenticated_protected_paths_api? if req.throttle_authenticated_protected_paths_api?
req.throttled_user_id([:api]) req.throttled_identifer([:api])
end end
end end
throttle_or_track(rack_attack, 'throttle_authenticated_protected_paths_web', Gitlab::Throttle.protected_paths_options) do |req| throttle_or_track(rack_attack, 'throttle_authenticated_protected_paths_web', Gitlab::Throttle.protected_paths_options) do |req|
if req.throttle_authenticated_protected_paths_web? if req.throttle_authenticated_protected_paths_web?
req.throttled_user_id([:api, :rss, :ics]) req.throttled_identifer([:api, :rss, :ics])
end end
end end
throttle_or_track(rack_attack, 'throttle_authenticated_git_lfs', Gitlab::Throttle.throttle_authenticated_git_lfs_options) do |req| throttle_or_track(rack_attack, 'throttle_authenticated_git_lfs', Gitlab::Throttle.throttle_authenticated_git_lfs_options) do |req|
if req.throttle_authenticated_git_lfs? if req.throttle_authenticated_git_lfs?
req.throttled_user_id([:api]) req.throttled_identifer([:api])
end end
end end
......
...@@ -9,18 +9,22 @@ module Gitlab ...@@ -9,18 +9,22 @@ module Gitlab
GROUP_PATH_REGEX = %r{^/api/v\d+/groups/[^/]+/?$}.freeze GROUP_PATH_REGEX = %r{^/api/v\d+/groups/[^/]+/?$}.freeze
def unauthenticated? def unauthenticated?
!(authenticated_user_id([:api, :rss, :ics]) || authenticated_runner_id) !(authenticated_identifier([:api, :rss, :ics]) || authenticated_runner_id)
end end
def throttled_user_id(request_formats) def throttled_identifer(request_formats)
user_id = authenticated_user_id(request_formats) identifier = authenticated_identifier(request_formats)
return unless identifier
if Gitlab::RackAttack.user_allowlist.include?(user_id) identifier_type = identifier[:identifier_type]
identifier_id = identifier[:identifier_id]
if identifier_type == :user && Gitlab::RackAttack.user_allowlist.include?(identifier_id)
Gitlab::Instrumentation::Throttle.safelist = 'throttle_user_allowlist' Gitlab::Instrumentation::Throttle.safelist = 'throttle_user_allowlist'
return return
end end
user_id "#{identifier_type}:#{identifier_id}"
end end
def authenticated_runner_id def authenticated_runner_id
...@@ -169,8 +173,18 @@ module Gitlab ...@@ -169,8 +173,18 @@ module Gitlab
private private
def authenticated_user_id(request_formats) def authenticated_identifier(request_formats)
request_authenticator.user(request_formats)&.id requester = request_authenticator.find_authenticated_requester(request_formats)
return unless requester
identifier_type = if requester.is_a?(DeployToken)
:deploy_token
else
:user
end
{ identifier_type: identifier_type, identifier_id: requester.id }
end end
def request_authenticator def request_authenticator
......
...@@ -76,6 +76,38 @@ RSpec.describe Gitlab::Auth::RequestAuthenticator do ...@@ -76,6 +76,38 @@ RSpec.describe Gitlab::Auth::RequestAuthenticator do
end end
end end
describe '#find_authenticated_requester' do
let_it_be(:api_user) { create(:user) }
let_it_be(:deploy_token_user) { create(:user) }
it 'returns the deploy token if it exists' do
allow_next_instance_of(described_class) do |authenticator|
expect(authenticator).to receive(:deploy_token_from_request).and_return(deploy_token_user)
allow(authenticator).to receive(:user).and_return(nil)
end
expect(subject.find_authenticated_requester([:api])).to eq deploy_token_user
end
it 'returns the user id if it exists' do
allow_next_instance_of(described_class) do |authenticator|
allow(authenticator).to receive(:deploy_token_from_request).and_return(deploy_token_user)
expect(authenticator).to receive(:user).and_return(api_user)
end
expect(subject.find_authenticated_requester([:api])).to eq api_user
end
it 'rerturns nil if no match is found' do
allow_next_instance_of(described_class) do |authenticator|
expect(authenticator).to receive(:deploy_token_from_request).and_return(nil)
expect(authenticator).to receive(:user).and_return(nil)
end
expect(subject.find_authenticated_requester([:api])).to eq nil
end
end
describe '#find_sessionless_user' do describe '#find_sessionless_user' do
let_it_be(:dependency_proxy_user) { build(:user) } let_it_be(:dependency_proxy_user) { build(:user) }
let_it_be(:access_token_user) { build(:user) } let_it_be(:access_token_user) { build(:user) }
...@@ -380,10 +412,10 @@ RSpec.describe Gitlab::Auth::RequestAuthenticator do ...@@ -380,10 +412,10 @@ RSpec.describe Gitlab::Auth::RequestAuthenticator do
describe '#route_authentication_setting' do describe '#route_authentication_setting' do
using RSpec::Parameterized::TableSyntax using RSpec::Parameterized::TableSyntax
where(:script_name, :expected_job_token_allowed, :expected_basic_auth_personal_access_token) do where(:script_name, :expected_job_token_allowed, :expected_basic_auth_personal_access_token, :expected_deploy_token_allowed) do
'/api/endpoint' | true | true '/api/endpoint' | true | true | true
'/namespace/project.git' | false | true '/namespace/project.git' | false | true | true
'/web/endpoint' | false | false '/web/endpoint' | false | false | false
end end
with_them do with_them do
...@@ -394,7 +426,8 @@ RSpec.describe Gitlab::Auth::RequestAuthenticator do ...@@ -394,7 +426,8 @@ RSpec.describe Gitlab::Auth::RequestAuthenticator do
it 'returns correct settings' do it 'returns correct settings' do
expect(subject.send(:route_authentication_setting)).to eql({ expect(subject.send(:route_authentication_setting)).to eql({
job_token_allowed: expected_job_token_allowed, job_token_allowed: expected_job_token_allowed,
basic_auth_personal_access_token: expected_basic_auth_personal_access_token basic_auth_personal_access_token: expected_basic_auth_personal_access_token,
deploy_token_allowed: expected_deploy_token_allowed
}) })
end end
end end
......
...@@ -91,72 +91,110 @@ RSpec.describe Gitlab::Metrics::Subscribers::RackAttack, :request_store do ...@@ -91,72 +91,110 @@ RSpec.describe Gitlab::Metrics::Subscribers::RackAttack, :request_store do
end end
end end
context 'when matched throttle requires user information' do context 'matching user or deploy token authenticated information' do
context 'when user not found' do context 'when matching for user' do
let(:event) do context 'when user not found' do
ActiveSupport::Notifications::Event.new( let(:event) do
event_name, Time.current, Time.current + 2.seconds, '1', request: double( ActiveSupport::Notifications::Event.new(
:request, event_name, Time.current, Time.current + 2.seconds, '1', request: double(
ip: '1.2.3.4', :request,
request_method: 'GET', ip: '1.2.3.4',
fullpath: '/api/v4/internal/authorized_keys', request_method: 'GET',
env: { fullpath: '/api/v4/internal/authorized_keys',
'rack.attack.match_type' => match_type, env: {
'rack.attack.matched' => 'throttle_authenticated_api', 'rack.attack.match_type' => match_type,
'rack.attack.match_discriminator' => 'not_exist_user_id' 'rack.attack.matched' => 'throttle_authenticated_api',
} 'rack.attack.match_discriminator' => "user:#{non_existing_record_id}"
}
)
) )
) end
it 'logs request information and user id' do
expect(Gitlab::AuthLogger).to receive(:error).with(
include(
message: 'Rack_Attack',
env: match_type,
remote_ip: '1.2.3.4',
request_method: 'GET',
path: '/api/v4/internal/authorized_keys',
matched: 'throttle_authenticated_api',
user_id: non_existing_record_id
)
)
subscriber.send(match_type, event)
end
end end
it 'logs request information and user id' do context 'when user found' do
expect(Gitlab::AuthLogger).to receive(:error).with( let(:user) { create(:user) }
include( let(:event) do
message: 'Rack_Attack', ActiveSupport::Notifications::Event.new(
env: match_type, event_name, Time.current, Time.current + 2.seconds, '1', request: double(
remote_ip: '1.2.3.4', :request,
request_method: 'GET', ip: '1.2.3.4',
path: '/api/v4/internal/authorized_keys', request_method: 'GET',
matched: 'throttle_authenticated_api', fullpath: '/api/v4/internal/authorized_keys',
user_id: 'not_exist_user_id' env: {
'rack.attack.match_type' => match_type,
'rack.attack.matched' => 'throttle_authenticated_api',
'rack.attack.match_discriminator' => "user:#{user.id}"
}
)
) )
) end
subscriber.send(match_type, event)
it 'logs request information and user meta' do
expect(Gitlab::AuthLogger).to receive(:error).with(
include(
message: 'Rack_Attack',
env: match_type,
remote_ip: '1.2.3.4',
request_method: 'GET',
path: '/api/v4/internal/authorized_keys',
matched: 'throttle_authenticated_api',
user_id: user.id,
'meta.user' => user.username
)
)
subscriber.send(match_type, event)
end
end end
end end
context 'when user found' do context 'when matching for deploy token' do
let(:user) { create(:user) } context 'when deploy token found' do
let(:event) do let(:deploy_token) { create(:deploy_token) }
ActiveSupport::Notifications::Event.new( let(:event) do
event_name, Time.current, Time.current + 2.seconds, '1', request: double( ActiveSupport::Notifications::Event.new(
:request, event_name, Time.current, Time.current + 2.seconds, '1', request: double(
ip: '1.2.3.4', :request,
request_method: 'GET', ip: '1.2.3.4',
fullpath: '/api/v4/internal/authorized_keys', request_method: 'GET',
env: { fullpath: '/api/v4/internal/authorized_keys',
'rack.attack.match_type' => match_type, env: {
'rack.attack.matched' => 'throttle_authenticated_api', 'rack.attack.match_type' => match_type,
'rack.attack.match_discriminator' => user.id 'rack.attack.matched' => 'throttle_authenticated_api',
} 'rack.attack.match_discriminator' => "deploy_token:#{deploy_token.id}"
}
)
) )
) end
end
it 'logs request information and user meta' do
it 'logs request information and user meta' do expect(Gitlab::AuthLogger).to receive(:error).with(
expect(Gitlab::AuthLogger).to receive(:error).with( include(
include( message: 'Rack_Attack',
message: 'Rack_Attack', env: match_type,
env: match_type, remote_ip: '1.2.3.4',
remote_ip: '1.2.3.4', request_method: 'GET',
request_method: 'GET', path: '/api/v4/internal/authorized_keys',
path: '/api/v4/internal/authorized_keys', matched: 'throttle_authenticated_api',
matched: 'throttle_authenticated_api', deploy_token_id: deploy_token.id
user_id: user.id, )
'meta.user' => user.username
) )
) subscriber.send(match_type, event)
subscriber.send(match_type, event) end
end end
end end
end end
......
This diff is collapsed.
...@@ -10,11 +10,11 @@ module RackAttackSpecHelpers ...@@ -10,11 +10,11 @@ module RackAttackSpecHelpers
end end
def private_token_headers(user) def private_token_headers(user)
{ 'HTTP_PRIVATE_TOKEN' => user.private_token } { Gitlab::Auth::AuthFinders::PRIVATE_TOKEN_HEADER => user.private_token }
end end
def personal_access_token_headers(personal_access_token) def personal_access_token_headers(personal_access_token)
{ 'HTTP_PRIVATE_TOKEN' => personal_access_token.token } { Gitlab::Auth::AuthFinders::PRIVATE_TOKEN_HEADER => personal_access_token.token }
end end
def oauth_token_headers(oauth_access_token) def oauth_token_headers(oauth_access_token)
...@@ -26,6 +26,10 @@ module RackAttackSpecHelpers ...@@ -26,6 +26,10 @@ module RackAttackSpecHelpers
{ 'AUTHORIZATION' => "Basic #{encoded_login}" } { 'AUTHORIZATION' => "Basic #{encoded_login}" }
end end
def deploy_token_headers(deploy_token)
basic_auth_headers(deploy_token, deploy_token)
end
def expect_rejection(name = nil, &block) def expect_rejection(name = nil, &block)
yield yield
......
...@@ -8,7 +8,50 @@ ...@@ -8,7 +8,50 @@
# * requests_per_period # * requests_per_period
# * period_in_seconds # * period_in_seconds
# * period # * period
RSpec.shared_examples 'rate-limited token-authenticated requests' do RSpec.shared_examples 'rate-limited user based token-authenticated requests' do
context 'when the throttle is enabled' do
before do
settings_to_set[:"#{throttle_setting_prefix}_enabled"] = true
stub_application_setting(settings_to_set)
end
it 'does not reject requests if the user is in the allowlist' do
stub_env('GITLAB_THROTTLE_USER_ALLOWLIST', user.id.to_s)
Gitlab::RackAttack.configure_user_allowlist
expect(Gitlab::Instrumentation::Throttle).to receive(:safelist=).with('throttle_user_allowlist').at_least(:once)
(requests_per_period + 1).times do
make_request(request_args)
expect(response).not_to have_gitlab_http_status(:too_many_requests)
end
stub_env('GITLAB_THROTTLE_USER_ALLOWLIST', nil)
Gitlab::RackAttack.configure_user_allowlist
end
end
include_examples 'rate-limited token requests' do
let(:log_data) do
{
user_id: user.id,
'meta.user' => user.username
}
end
end
end
RSpec.shared_examples 'rate-limited deploy-token-authenticated requests' do
include_examples 'rate-limited token requests' do
let(:log_data) do
{
deploy_token_id: deploy_token.id
}
end
end
end
RSpec.shared_examples 'rate-limited token requests' do
let(:throttle_types) do let(:throttle_types) do
{ {
"throttle_protected_paths" => "throttle_authenticated_protected_paths_api", "throttle_protected_paths" => "throttle_authenticated_protected_paths_api",
...@@ -51,18 +94,6 @@ RSpec.shared_examples 'rate-limited token-authenticated requests' do ...@@ -51,18 +94,6 @@ RSpec.shared_examples 'rate-limited token-authenticated requests' do
expect_rejection { make_request(request_args) } expect_rejection { make_request(request_args) }
end end
it 'does not reject requests if the user is in the allowlist' do
stub_env('GITLAB_THROTTLE_USER_ALLOWLIST', user.id.to_s)
Gitlab::RackAttack.configure_user_allowlist
expect(Gitlab::Instrumentation::Throttle).to receive(:safelist=).with('throttle_user_allowlist').at_least(:once)
(requests_per_period + 1).times do
make_request(request_args)
expect(response).not_to have_gitlab_http_status(:too_many_requests)
end
end
it 'allows requests after throttling and then waiting for the next period' do it 'allows requests after throttling and then waiting for the next period' do
requests_per_period.times do requests_per_period.times do
make_request(request_args) make_request(request_args)
...@@ -81,7 +112,7 @@ RSpec.shared_examples 'rate-limited token-authenticated requests' do ...@@ -81,7 +112,7 @@ RSpec.shared_examples 'rate-limited token-authenticated requests' do
end end
end end
it 'counts requests from different users separately, even from the same IP' do it 'counts requests from different requesters separately, even from the same IP' do
requests_per_period.times do requests_per_period.times do
make_request(request_args) make_request(request_args)
expect(response).not_to have_gitlab_http_status(:too_many_requests) expect(response).not_to have_gitlab_http_status(:too_many_requests)
...@@ -92,7 +123,7 @@ RSpec.shared_examples 'rate-limited token-authenticated requests' do ...@@ -92,7 +123,7 @@ RSpec.shared_examples 'rate-limited token-authenticated requests' do
expect(response).not_to have_gitlab_http_status(:too_many_requests) expect(response).not_to have_gitlab_http_status(:too_many_requests)
end end
it 'counts all requests from the same user, even via different IPs' do it 'counts all requests from the same requesters, even via different IPs' do
requests_per_period.times do requests_per_period.times do
make_request(request_args) make_request(request_args)
expect(response).not_to have_gitlab_http_status(:too_many_requests) expect(response).not_to have_gitlab_http_status(:too_many_requests)
...@@ -122,10 +153,8 @@ RSpec.shared_examples 'rate-limited token-authenticated requests' do ...@@ -122,10 +153,8 @@ RSpec.shared_examples 'rate-limited token-authenticated requests' do
remote_ip: '127.0.0.1', remote_ip: '127.0.0.1',
request_method: request_method, request_method: request_method,
path: request_args.first, path: request_args.first,
user_id: user.id,
'meta.user' => user.username,
matched: throttle_types[throttle_setting_prefix] matched: throttle_types[throttle_setting_prefix]
}) }.merge(log_data))
expect(Gitlab::AuthLogger).to receive(:error).with(arguments).once expect(Gitlab::AuthLogger).to receive(:error).with(arguments).once
......
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