Commit eb396f71 authored by Francisco Javier López's avatar Francisco Javier López Committed by Douwe Maan

CE Port: Add request rate limits

parent b4045045
......@@ -11,8 +11,7 @@ class ApplicationController < ActionController::Base
include EnforcesTwoFactorAuthentication
include WithPerformanceBar
before_action :authenticate_user_from_personal_access_token!
before_action :authenticate_user_from_rss_token!
before_action :authenticate_sessionless_user!
before_action :authenticate_user!
before_action :validate_user_service_ticket!
before_action :check_password_expiration
......@@ -100,27 +99,11 @@ class ApplicationController < ActionController::Base
return try(:authenticated_user)
end
def authenticate_user_from_personal_access_token!
token = params[:private_token].presence || request.headers['PRIVATE-TOKEN'].presence
# This filter handles personal access tokens, and atom requests with rss tokens
def authenticate_sessionless_user!
user = Gitlab::Auth::RequestAuthenticator.new(request).find_sessionless_user
return unless token.present?
user = User.find_by_personal_access_token(token)
sessionless_sign_in(user)
end
# This filter handles authentication for atom request with an rss_token
def authenticate_user_from_rss_token!
return unless request.format.atom?
token = params[:rss_token].presence
return unless token.present?
user = User.find_by_rss_token(token)
sessionless_sign_in(user)
sessionless_sign_in(user) if user
end
def verify_namespace_plan_check_enabled
......
......@@ -57,7 +57,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
if current_user
log_audit_event(current_user, with: :saml)
# Update SAML identity if data has changed.
identity = current_user.identities.find_by(extern_uid: oauth['uid'], provider: :saml)
identity = current_user.identities.with_extern_uid(:saml, oauth['uid']).take
if identity.nil?
current_user.identities.create(extern_uid: oauth['uid'], provider: :saml)
redirect_to profile_account_path, notice: 'Authentication method updated'
......@@ -112,7 +112,9 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
def handle_omniauth
if current_user
# Add new authentication method
current_user.identities.find_or_create_by(extern_uid: oauth['uid'], provider: oauth['provider'])
current_user.identities
.with_extern_uid(oauth['provider'], oauth['uid'])
.first_or_create(extern_uid: oauth['uid'])
log_audit_event(current_user, with: oauth['provider'])
redirect_to profile_account_path, notice: 'Authentication method updated'
else
......
......@@ -232,6 +232,15 @@ module ApplicationSettingsHelper
:sign_in_text,
:signup_enabled,
:terminal_max_session_time,
:throttle_unauthenticated_enabled,
:throttle_unauthenticated_requests_per_period,
:throttle_unauthenticated_period_in_seconds,
:throttle_authenticated_web_enabled,
:throttle_authenticated_web_requests_per_period,
:throttle_authenticated_web_period_in_seconds,
:throttle_authenticated_api_enabled,
:throttle_authenticated_api_requests_per_period,
:throttle_authenticated_api_period_in_seconds,
:two_factor_grace_period,
:unique_ips_limit_enabled,
:unique_ips_limit_per_user,
......
......@@ -308,6 +308,15 @@ class ApplicationSetting < ActiveRecord::Base
sign_in_text: nil,
signup_enabled: Settings.gitlab['signup_enabled'],
terminal_max_session_time: 0,
throttle_unauthenticated_enabled: false,
throttle_unauthenticated_requests_per_period: 3600,
throttle_unauthenticated_period_in_seconds: 3600,
throttle_authenticated_web_enabled: false,
throttle_authenticated_web_requests_per_period: 7200,
throttle_authenticated_web_period_in_seconds: 3600,
throttle_authenticated_api_enabled: false,
throttle_authenticated_api_requests_per_period: 7200,
throttle_authenticated_api_period_in_seconds: 3600,
two_factor_grace_period: 48,
user_default_external: false,
polling_interval_multiplier: 1,
......
......@@ -835,5 +835,56 @@
The amount of seconds after which a request to get a secondary node
status will time out.
%fieldset
%legend User and IP Rate Limits
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
= f.label :throttle_unauthenticated_enabled do
= f.check_box :throttle_unauthenticated_enabled
Enable unauthenticated request rate limit
%span.help-block
Helps reduce request volume (e.g. from crawlers or abusive bots)
.form-group
= f.label :throttle_unauthenticated_requests_per_period, 'Max requests per period per IP', class: 'control-label col-sm-2'
.col-sm-10
= f.number_field :throttle_unauthenticated_requests_per_period, class: 'form-control'
.form-group
= f.label :throttle_unauthenticated_period_in_seconds, 'Rate limit period in seconds', class: 'control-label col-sm-2'
.col-sm-10
= f.number_field :throttle_unauthenticated_period_in_seconds, class: 'form-control'
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
= f.label :throttle_authenticated_api_enabled do
= f.check_box :throttle_authenticated_api_enabled
Enable authenticated API request rate limit
%span.help-block
Helps reduce request volume (e.g. from crawlers or abusive bots)
.form-group
= f.label :throttle_authenticated_api_requests_per_period, 'Max requests per period per user', class: 'control-label col-sm-2'
.col-sm-10
= f.number_field :throttle_authenticated_api_requests_per_period, class: 'form-control'
.form-group
= f.label :throttle_authenticated_api_period_in_seconds, 'Rate limit period in seconds', class: 'control-label col-sm-2'
.col-sm-10
= f.number_field :throttle_authenticated_api_period_in_seconds, class: 'form-control'
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
= f.label :throttle_authenticated_web_enabled do
= f.check_box :throttle_authenticated_web_enabled
Enable authenticated web request rate limit
%span.help-block
Helps reduce request volume (e.g. from crawlers or abusive bots)
.form-group
= f.label :throttle_authenticated_web_requests_per_period, 'Max requests per period per user', class: 'control-label col-sm-2'
.col-sm-10
= f.number_field :throttle_authenticated_web_requests_per_period, class: 'form-control'
.form-group
= f.label :throttle_authenticated_web_period_in_seconds, 'Rate limit period in seconds', class: 'control-label col-sm-2'
.col-sm-10
= f.number_field :throttle_authenticated_web_period_in_seconds, class: 'form-control'
.form-actions
= f.submit 'Save', class: 'btn btn-save'
---
title: Add anonymous rate limit per IP, and authenticated (web or API) rate limits
per user
merge_request: 14708
author:
type: added
......@@ -130,7 +130,7 @@ module Gitlab
config.action_view.sanitized_allowed_protocols = %w(smb)
config.middleware.insert_before Warden::Manager, Rack::Attack
config.middleware.insert_after Warden::Manager, Rack::Attack
# Allow access to GitLab API from other domains
config.middleware.insert_before Warden::Manager, Rack::Cors do
......
module Gitlab::Throttle
def self.settings
Gitlab::CurrentSettings.current_application_settings
end
def self.unauthenticated_options
limit_proc = proc { |req| settings.throttle_unauthenticated_requests_per_period }
period_proc = proc { |req| settings.throttle_unauthenticated_period_in_seconds.seconds }
{ limit: limit_proc, period: period_proc }
end
def self.authenticated_api_options
limit_proc = proc { |req| settings.throttle_authenticated_api_requests_per_period }
period_proc = proc { |req| settings.throttle_authenticated_api_period_in_seconds.seconds }
{ limit: limit_proc, period: period_proc }
end
def self.authenticated_web_options
limit_proc = proc { |req| settings.throttle_authenticated_web_requests_per_period }
period_proc = proc { |req| settings.throttle_authenticated_web_period_in_seconds.seconds }
{ limit: limit_proc, period: period_proc }
end
end
class Rack::Attack
throttle('throttle_unauthenticated', Gitlab::Throttle.unauthenticated_options) do |req|
Gitlab::Throttle.settings.throttle_unauthenticated_enabled &&
req.unauthenticated? &&
req.ip
end
throttle('throttle_authenticated_api', Gitlab::Throttle.authenticated_api_options) do |req|
Gitlab::Throttle.settings.throttle_authenticated_api_enabled &&
req.api_request? &&
req.authenticated_user_id
end
throttle('throttle_authenticated_web', Gitlab::Throttle.authenticated_web_options) do |req|
Gitlab::Throttle.settings.throttle_authenticated_web_enabled &&
req.web_request? &&
req.authenticated_user_id
end
class Request
def unauthenticated?
!authenticated_user_id
end
def authenticated_user_id
Gitlab::Auth::RequestAuthenticator.new(self).user&.id
end
def api_request?
path.start_with?('/api')
end
def web_request?
!api_request?
end
end
end
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class AddGlobalRateLimitsToApplicationSettings < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_column_with_default :application_settings, :throttle_unauthenticated_enabled, :boolean, default: false, allow_null: false
add_column_with_default :application_settings, :throttle_unauthenticated_requests_per_period, :integer, default: 3600, allow_null: false
add_column_with_default :application_settings, :throttle_unauthenticated_period_in_seconds, :integer, default: 3600, allow_null: false
add_column_with_default :application_settings, :throttle_authenticated_api_enabled, :boolean, default: false, allow_null: false
add_column_with_default :application_settings, :throttle_authenticated_api_requests_per_period, :integer, default: 7200, allow_null: false
add_column_with_default :application_settings, :throttle_authenticated_api_period_in_seconds, :integer, default: 3600, allow_null: false
add_column_with_default :application_settings, :throttle_authenticated_web_enabled, :boolean, default: false, allow_null: false
add_column_with_default :application_settings, :throttle_authenticated_web_requests_per_period, :integer, default: 7200, allow_null: false
add_column_with_default :application_settings, :throttle_authenticated_web_period_in_seconds, :integer, default: 3600, allow_null: false
end
def down
remove_column :application_settings, :throttle_authenticated_web_period_in_seconds
remove_column :application_settings, :throttle_authenticated_web_requests_per_period
remove_column :application_settings, :throttle_authenticated_web_enabled
remove_column :application_settings, :throttle_authenticated_api_period_in_seconds
remove_column :application_settings, :throttle_authenticated_api_requests_per_period
remove_column :application_settings, :throttle_authenticated_api_enabled
remove_column :application_settings, :throttle_unauthenticated_period_in_seconds
remove_column :application_settings, :throttle_unauthenticated_requests_per_period
remove_column :application_settings, :throttle_unauthenticated_enabled
end
end
......@@ -164,6 +164,15 @@ ActiveRecord::Schema.define(version: 20171107144726) do
t.boolean "remote_mirror_available", default: true, null: false
t.integer "circuitbreaker_access_retries", default: 3
t.integer "circuitbreaker_backoff_threshold", default: 80
t.boolean "throttle_unauthenticated_enabled", default: false, null: false
t.integer "throttle_unauthenticated_requests_per_period", default: 3600, null: false
t.integer "throttle_unauthenticated_period_in_seconds", default: 3600, null: false
t.boolean "throttle_authenticated_api_enabled", default: false, null: false
t.integer "throttle_authenticated_api_requests_per_period", default: 7200, null: false
t.integer "throttle_authenticated_api_period_in_seconds", default: 3600, null: false
t.boolean "throttle_authenticated_web_enabled", default: false, null: false
t.integer "throttle_authenticated_web_requests_per_period", default: 7200, null: false
t.integer "throttle_authenticated_web_period_in_seconds", default: 3600, null: false
end
create_table "approvals", force: :cascade do |t|
......
......@@ -6,11 +6,6 @@ module API
module APIGuard
extend ActiveSupport::Concern
PRIVATE_TOKEN_HEADER = "HTTP_PRIVATE_TOKEN".freeze
PRIVATE_TOKEN_PARAM = :private_token
JOB_TOKEN_HEADER = "HTTP_JOB_TOKEN".freeze
JOB_TOKEN_PARAM = :job_token
included do |base|
# OAuth2 Resource Server Authentication
use Rack::OAuth2::Server::Resource::Bearer, 'The API' do |request|
......@@ -44,6 +39,8 @@ module API
# Helper Methods for Grape Endpoint
module HelperMethods
include Gitlab::Auth::UserAuthFinders
def find_current_user!
user = find_user_from_access_token || find_user_from_job_token || find_user_from_warden
return unless user
......@@ -53,96 +50,8 @@ module API
user
end
def access_token
return @access_token if defined?(@access_token)
@access_token = find_oauth_access_token || find_personal_access_token
end
def validate_access_token!(scopes: [])
return unless access_token
case AccessTokenValidationService.new(access_token, request: request).validate(scopes: scopes)
when AccessTokenValidationService::INSUFFICIENT_SCOPE
raise InsufficientScopeError.new(scopes)
when AccessTokenValidationService::EXPIRED
raise ExpiredError
when AccessTokenValidationService::REVOKED
raise RevokedError
end
end
private
def find_user_from_access_token
return unless access_token
validate_access_token!
access_token.user || raise(UnauthorizedError)
end
def find_user_from_job_token
return unless route_authentication_setting[:job_token_allowed]
token = (params[JOB_TOKEN_PARAM] || env[JOB_TOKEN_HEADER]).to_s
return unless token.present?
job = Ci::Build.find_by(token: token)
raise UnauthorizedError unless job
@job_token_authentication = true
job.user
end
# Check the Rails session for valid authentication details
def find_user_from_warden
warden.try(:authenticate) if verified_request?
end
def warden
env['warden']
end
# Check if the request is GET/HEAD, or if CSRF token is valid.
def verified_request?
Gitlab::RequestForgeryProtection.verified?(env)
end
def route_authentication_setting
return {} unless respond_to?(:route_setting)
route_setting(:authentication) || {}
end
def find_oauth_access_token
token = Doorkeeper::OAuth::Token.from_request(doorkeeper_request, *Doorkeeper.configuration.access_token_methods)
return unless token
# Expiration, revocation and scopes are verified in `find_user_by_access_token`
access_token = OauthAccessToken.by_token(token)
raise UnauthorizedError unless access_token
access_token.revoke_previous_refresh_token!
access_token
end
def find_personal_access_token
token = (params[PRIVATE_TOKEN_PARAM] || env[PRIVATE_TOKEN_HEADER]).to_s
return unless token.present?
# Expiration, revocation and scopes are verified in `find_user_by_access_token`
access_token = PersonalAccessToken.find_by(token: token)
raise UnauthorizedError unless access_token
access_token
end
def doorkeeper_request
@doorkeeper_request ||= ActionDispatch::Request.new(env)
end
# An array of scopes that were registered (using `allow_access_with_scope`)
# for the current endpoint class. It also returns scopes registered on
# `API::API`, since these are meant to apply to all API routes.
......@@ -165,8 +74,11 @@ module API
private
def install_error_responders(base)
error_classes = [MissingTokenError, TokenNotFoundError,
ExpiredError, RevokedError, InsufficientScopeError]
error_classes = [Gitlab::Auth::MissingTokenError,
Gitlab::Auth::TokenNotFoundError,
Gitlab::Auth::ExpiredError,
Gitlab::Auth::RevokedError,
Gitlab::Auth::InsufficientScopeError]
base.__send__(:rescue_from, *error_classes, oauth2_bearer_token_error_handler) # rubocop:disable GitlabSecurity/PublicSend
end
......@@ -175,25 +87,25 @@ module API
proc do |e|
response =
case e
when MissingTokenError
when Gitlab::Auth::MissingTokenError
Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new
when TokenNotFoundError
when Gitlab::Auth::TokenNotFoundError
Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new(
:invalid_token,
"Bad Access Token.")
when ExpiredError
when Gitlab::Auth::ExpiredError
Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new(
:invalid_token,
"Token is expired. You can either do re-authorization or token refresh.")
when RevokedError
when Gitlab::Auth::RevokedError
Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new(
:invalid_token,
"Token was revoked. You have to re-authorize from the user.")
when InsufficientScopeError
when Gitlab::Auth::InsufficientScopeError
# FIXME: ForbiddenError (inherited from Bearer::Forbidden of Rack::Oauth2)
# does not include WWW-Authenticate header, which breaks the standard.
Rack::OAuth2::Server::Resource::Bearer::Forbidden.new(
......@@ -206,22 +118,5 @@ module API
end
end
end
#
# Exceptions
#
MissingTokenError = Class.new(StandardError)
TokenNotFoundError = Class.new(StandardError)
ExpiredError = Class.new(StandardError)
RevokedError = Class.new(StandardError)
UnauthorizedError = Class.new(StandardError)
class InsufficientScopeError < StandardError
attr_reader :scopes
def initialize(scopes)
@scopes = scopes.map { |s| s.try(:name) || s }
end
end
end
end
......@@ -431,7 +431,7 @@ module API
begin
@initial_current_user = Gitlab::Auth::UniqueIpsLimiter.limit_user! { find_current_user! }
rescue APIGuard::UnauthorizedError
rescue Gitlab::Auth::UnauthorizedError
unauthorized!
end
end
......
# Use for authentication only, in particular for Rack::Attack.
# Does not perform authorization of scopes, etc.
module Gitlab
module Auth
class RequestAuthenticator
include UserAuthFinders
attr_reader :request
def initialize(request)
@request = request
end
def user
find_sessionless_user || find_user_from_warden
end
def find_sessionless_user
find_user_from_access_token || find_user_from_rss_token
rescue Gitlab::Auth::AuthenticationError
nil
end
end
end
end
module Gitlab
module Auth
#
# Exceptions
#
AuthenticationError = Class.new(StandardError)
MissingTokenError = Class.new(AuthenticationError)
TokenNotFoundError = Class.new(AuthenticationError)
ExpiredError = Class.new(AuthenticationError)
RevokedError = Class.new(AuthenticationError)
UnauthorizedError = Class.new(AuthenticationError)
class InsufficientScopeError < AuthenticationError
attr_reader :scopes
def initialize(scopes)
@scopes = scopes.map { |s| s.try(:name) || s }
end
end
module UserAuthFinders
PRIVATE_TOKEN_HEADER = 'HTTP_PRIVATE_TOKEN'.freeze
PRIVATE_TOKEN_PARAM = :private_token
JOB_TOKEN_HEADER = "HTTP_JOB_TOKEN".freeze
JOB_TOKEN_PARAM = :job_token
# Check the Rails session for valid authentication details
def find_user_from_warden
current_request.env['warden']&.authenticate if verified_request?
end
def find_user_from_rss_token
return unless current_request.path.ends_with?('.atom') || current_request.format.atom?
token = current_request.params[:rss_token].presence
return unless token
User.find_by_rss_token(token) || raise(UnauthorizedError)
end
def find_user_from_access_token
return unless access_token
validate_access_token!
access_token.user || raise(UnauthorizedError)
end
def find_user_from_job_token
return unless route_authentication_setting[:job_token_allowed]
token = (params[JOB_TOKEN_PARAM] || env[JOB_TOKEN_HEADER]).to_s
return unless token.present?
job = ::Ci::Build.find_by(token: token)
raise UnauthorizedError unless job
@job_token_authentication = true
job.user
end
def validate_access_token!(scopes: [])
return unless access_token
case AccessTokenValidationService.new(access_token, request: request).validate(scopes: scopes)
when AccessTokenValidationService::INSUFFICIENT_SCOPE
raise InsufficientScopeError.new(scopes)
when AccessTokenValidationService::EXPIRED
raise ExpiredError
when AccessTokenValidationService::REVOKED
raise RevokedError
end
end
private
def route_authentication_setting
return {} unless respond_to?(:route_setting)
route_setting(:authentication) || {}
end
def access_token
return @access_token if defined?(@access_token)
@access_token = find_oauth_access_token || find_personal_access_token
end
def find_personal_access_token
token =
current_request.params[PRIVATE_TOKEN_PARAM].presence ||
current_request.env[PRIVATE_TOKEN_HEADER].presence
return unless token
# Expiration, revocation and scopes are verified in `validate_access_token!`
PersonalAccessToken.find_by(token: token) || raise(UnauthorizedError)
end
def find_oauth_access_token
token = Doorkeeper::OAuth::Token.from_request(current_request, *Doorkeeper.configuration.access_token_methods)
return unless token
# Expiration, revocation and scopes are verified in `validate_access_token!`
oauth_token = OauthAccessToken.by_token(token)
raise UnauthorizedError unless oauth_token
oauth_token.revoke_previous_refresh_token!
oauth_token
end
# Check if the request is GET/HEAD, or if CSRF token is valid.
def verified_request?
Gitlab::RequestForgeryProtection.verified?(current_request.env)
end
def ensure_action_dispatch_request(request)
return request if request.is_a?(ActionDispatch::Request)
ActionDispatch::Request.new(request.env)
end
def current_request
@current_request ||= ensure_action_dispatch_request(request)
end
end
end
end
......@@ -13,6 +13,7 @@ describe EE::API::Helpers do
}
end
let(:header) { }
let(:request) { Grape::Request.new(env)}
before do
allow(Gitlab::Database::LoadBalancing).to receive(:enable?).and_return(true)
......
require 'spec_helper'
describe Gitlab::Auth::RequestAuthenticator do
let(:env) do
{
'rack.input' => '',
'REQUEST_METHOD' => 'GET'
}
end
let(:request) { ActionDispatch::Request.new(env) }
subject { described_class.new(request) }
describe '#user' do
let!(:sessionless_user) { build(:user) }
let!(:session_user) { build(:user) }
it 'returns sessionless user first' do
allow_any_instance_of(described_class).to receive(:find_sessionless_user).and_return(sessionless_user)
allow_any_instance_of(described_class).to receive(:find_user_from_warden).and_return(session_user)
expect(subject.user).to eq sessionless_user
end
it 'returns session user if no sessionless user found' do
allow_any_instance_of(described_class).to receive(:find_user_from_warden).and_return(session_user)
expect(subject.user).to eq session_user
end
it 'returns nil if no user found' do
expect(subject.user).to be_blank
end
it 'bubbles up exceptions' do
allow_any_instance_of(described_class).to receive(:find_user_from_warden).and_raise(Gitlab::Auth::UnauthorizedError)
end
end
describe '#find_sessionless_user' do
let!(:access_token_user) { build(:user) }
let!(:rss_token_user) { build(:user) }
it 'returns access_token user first' do
allow_any_instance_of(described_class).to receive(:find_user_from_access_token).and_return(access_token_user)
allow_any_instance_of(described_class).to receive(:find_user_from_rss_token).and_return(rss_token_user)
expect(subject.find_sessionless_user).to eq access_token_user
end
it 'returns rss_token user if no access_token user found' do
allow_any_instance_of(described_class).to receive(:find_user_from_rss_token).and_return(rss_token_user)
expect(subject.find_sessionless_user).to eq rss_token_user
end
it 'returns nil if no user found' do
expect(subject.find_sessionless_user).to be_blank
end
it 'rescue Gitlab::Auth::AuthenticationError exceptions' do
allow_any_instance_of(described_class).to receive(:find_user_from_access_token).and_raise(Gitlab::Auth::UnauthorizedError)
expect(subject.find_sessionless_user).to be_blank
end
end
end
require 'spec_helper'
describe Gitlab::Auth::UserAuthFinders do
include described_class
let(:user) { create(:user) }
let(:env) do
{
'rack.input' => ''
}
end
let(:request) { Rack::Request.new(env)}
let(:params) { request.params }
def set_param(key, value)
request.update_param(key, value)
end
describe '#find_user_from_warden' do
context 'with CSRF token' do
before do
allow(Gitlab::RequestForgeryProtection).to receive(:verified?).and_return(true)
end
context 'with invalid credentials' do
it 'returns nil' do
expect(find_user_from_warden).to be_nil
end
end
context 'with valid credentials' do
it 'returns the user' do
env['warden'] = double("warden", authenticate: user)
expect(find_user_from_warden).to eq user
end
end
end
context 'without CSRF token' do
it 'returns nil' do
allow(Gitlab::RequestForgeryProtection).to receive(:verified?).and_return(false)
env['warden'] = double("warden", authenticate: user)
expect(find_user_from_warden).to be_nil
end
end
end
describe '#find_user_from_rss_token' do
context 'when the request format is atom' do
before do
env['HTTP_ACCEPT'] = 'application/atom+xml'
end
it 'returns user if valid rss_token' do
set_param(:rss_token, user.rss_token)
expect(find_user_from_rss_token).to eq user
end
it 'returns nil if rss_token is blank' do
expect(find_user_from_rss_token).to be_nil
end
it 'returns exception if invalid rss_token' do
set_param(:rss_token, 'invalid_token')
expect { find_user_from_rss_token }.to raise_error(Gitlab::Auth::UnauthorizedError)
end
end
context 'when the request format is not atom' do
it 'returns nil' do
set_param(:rss_token, user.rss_token)
expect(find_user_from_rss_token).to be_nil
end
end
end
describe '#find_user_from_access_token' do
let(:personal_access_token) { create(:personal_access_token, user: user) }
it 'returns nil if no access_token present' do
expect(find_personal_access_token).to be_nil
end
context 'when validate_access_token! returns valid' do
it 'returns user' do
env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token
expect(find_user_from_access_token).to eq user
end
it 'returns exception if token has no user' do
env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token
allow_any_instance_of(PersonalAccessToken).to receive(:user).and_return(nil)
expect { find_user_from_access_token }.to raise_error(Gitlab::Auth::UnauthorizedError)
end
end
end
describe '#find_user_from_job_token' do
let(:job) { create(:ci_build, user: user) }
shared_examples 'find user from job token' do
context 'when route is allowed to be authenticated' do
let(:route_authentication_setting) { { job_token_allowed: true } }
it "returns an Unauthorized exception for an invalid token" do
set_token('invalid token')
expect { find_user_from_job_token }.to raise_error(Gitlab::Auth::UnauthorizedError)
end
it "return user if token is valid" do
set_token(job.token)
expect(find_user_from_job_token).to eq(user)
end
end
context 'when route is not allowed to be authenticated' do
let(:route_authentication_setting) { { job_token_allowed: false } }
it "sets current_user to nil" do
set_token(job.token)
allow_any_instance_of(Gitlab::UserAccess).to receive(:allowed?).and_return(true)
expect(find_user_from_job_token).to be_nil
end
end
end
context 'when the job token is in the headers' do
def set_token(token)
env[Gitlab::Auth::UserAuthFinders::JOB_TOKEN_HEADER] = token
end
it_behaves_like 'find user from job token'
end
context 'when the job token is in the params' do
def set_token(token)
set_param(Gitlab::Auth::UserAuthFinders::JOB_TOKEN_PARAM, token)
end
it_behaves_like 'find user from job token'
end
end
describe '#find_personal_access_token' do
let(:personal_access_token) { create(:personal_access_token, user: user) }
context 'passed as header' do
it 'returns token if valid personal_access_token' do
env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token
expect(find_personal_access_token).to eq personal_access_token
end
end
context 'passed as param' do
it 'returns token if valid personal_access_token' do
set_param(Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_PARAM, personal_access_token.token)
expect(find_personal_access_token).to eq personal_access_token
end
end
it 'returns nil if no personal_access_token' do
expect(find_personal_access_token).to be_nil
end
it 'returns exception if invalid personal_access_token' do
env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = 'invalid_token'
expect { find_personal_access_token }.to raise_error(Gitlab::Auth::UnauthorizedError)
end
end
describe '#find_oauth_access_token' do
let(:application) { Doorkeeper::Application.create!(name: 'MyApp', redirect_uri: 'https://app.com', owner: user) }
let(:token) { Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id, scopes: 'api') }
context 'passed as header' do
it 'returns token if valid oauth_access_token' do
env['HTTP_AUTHORIZATION'] = "Bearer #{token.token}"
expect(find_oauth_access_token.token).to eq token.token
end
end
context 'passed as param' do
it 'returns user if valid oauth_access_token' do
set_param(:access_token, token.token)
expect(find_oauth_access_token.token).to eq token.token
end
end
it 'returns nil if no oauth_access_token' do
expect(find_oauth_access_token).to be_nil
end
it 'returns exception if invalid oauth_access_token' do
env['HTTP_AUTHORIZATION'] = "Bearer invalid_token"
expect { find_oauth_access_token }.to raise_error(Gitlab::Auth::UnauthorizedError)
end
end
describe '#validate_access_token!' do
let(:personal_access_token) { create(:personal_access_token, user: user) }
it 'returns nil if no access_token present' do
expect(validate_access_token!).to be_nil
end
context 'token is not valid' do
before do
allow_any_instance_of(described_class).to receive(:access_token).and_return(personal_access_token)
end
it 'returns Gitlab::Auth::ExpiredError if token expired' do
personal_access_token.expires_at = 1.day.ago
expect { validate_access_token! }.to raise_error(Gitlab::Auth::ExpiredError)
end
it 'returns Gitlab::Auth::RevokedError if token revoked' do
personal_access_token.revoke!
expect { validate_access_token! }.to raise_error(Gitlab::Auth::RevokedError)
end
it 'returns Gitlab::Auth::InsufficientScopeError if invalid token scope' do
expect { validate_access_token!(scopes: [:sudo]) }.to raise_error(Gitlab::Auth::InsufficientScopeError)
end
end
end
end
......@@ -11,7 +11,6 @@ describe API::Helpers do
let(:admin) { create(:admin) }
let(:key) { create(:key, user: user) }
let(:params) { {} }
let(:csrf_token) { SecureRandom.base64(ActionController::RequestForgeryProtection::AUTHENTICITY_TOKEN_LENGTH) }
let(:env) do
{
......@@ -19,11 +18,14 @@ describe API::Helpers do
'rack.session' => {
_csrf_token: csrf_token
},
'REQUEST_METHOD' => 'GET'
'REQUEST_METHOD' => 'GET',
'CONTENT_TYPE' => 'text/plain;charset=utf-8'
}
end
let(:header) { }
let(:route_authentication_setting) { {} }
let(:request) { Grape::Request.new(env)}
let(:params) { request.params }
before do
allow_any_instance_of(self.class).to receive(:options).and_return({})
......@@ -40,6 +42,10 @@ describe API::Helpers do
raise Exception.new("#{status} - #{message}")
end
def set_param(key, value)
request.update_param(key, value)
end
describe ".current_user" do
subject { current_user }
......@@ -135,13 +141,13 @@ describe API::Helpers do
let(:personal_access_token) { create(:personal_access_token, user: user) }
it "returns a 401 response for an invalid token" do
env[API::APIGuard::PRIVATE_TOKEN_HEADER] = 'invalid token'
env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = 'invalid token'
expect { current_user }.to raise_error /401/
end
it "returns a 403 response for a user without access" do
env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token
env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token
allow_any_instance_of(Gitlab::UserAccess).to receive(:allowed?).and_return(false)
expect { current_user }.to raise_error /403/
......@@ -149,35 +155,35 @@ describe API::Helpers do
it 'returns a 403 response for a user who is blocked' do
user.block!
env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token
env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token
expect { current_user }.to raise_error /403/
end
it "sets current_user" do
env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token
env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token
expect(current_user).to eq(user)
end
it "does not allow tokens without the appropriate scope" do
personal_access_token = create(:personal_access_token, user: user, scopes: ['read_user'])
env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token
env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token
expect { current_user }.to raise_error API::APIGuard::InsufficientScopeError
expect { current_user }.to raise_error Gitlab::Auth::InsufficientScopeError
end
it 'does not allow revoked tokens' do
personal_access_token.revoke!
env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token
env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token
expect { current_user }.to raise_error API::APIGuard::RevokedError
expect { current_user }.to raise_error Gitlab::Auth::RevokedError
end
it 'does not allow expired tokens' do
personal_access_token.update_attributes!(expires_at: 1.day.ago)
env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token
env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token
expect { current_user }.to raise_error API::APIGuard::ExpiredError
expect { current_user }.to raise_error Gitlab::Auth::ExpiredError
end
end
......@@ -188,13 +194,13 @@ describe API::Helpers do
let(:route_authentication_setting) { { job_token_allowed: true } }
it "returns a 401 response for an invalid token" do
env[API::APIGuard::JOB_TOKEN_HEADER] = 'invalid token'
env[Gitlab::Auth::UserAuthFinders::JOB_TOKEN_HEADER] = 'invalid token'
expect { current_user }.to raise_error /401/
end
it "returns a 403 response for a user without access" do
env[API::APIGuard::JOB_TOKEN_HEADER] = job.token
env[Gitlab::Auth::UserAuthFinders::JOB_TOKEN_HEADER] = job.token
allow_any_instance_of(Gitlab::UserAccess).to receive(:allowed?).and_return(false)
expect { current_user }.to raise_error /403/
......@@ -202,13 +208,13 @@ describe API::Helpers do
it 'returns a 403 response for a user who is blocked' do
user.block!
env[API::APIGuard::JOB_TOKEN_HEADER] = job.token
env[Gitlab::Auth::UserAuthFinders::JOB_TOKEN_HEADER] = job.token
expect { current_user }.to raise_error /403/
end
it "sets current_user" do
env[API::APIGuard::JOB_TOKEN_HEADER] = job.token
env[Gitlab::Auth::UserAuthFinders::JOB_TOKEN_HEADER] = job.token
expect(current_user).to eq(user)
end
......@@ -218,7 +224,7 @@ describe API::Helpers do
let(:route_authentication_setting) { { job_token_allowed: false } }
it "sets current_user to nil" do
env[API::APIGuard::JOB_TOKEN_HEADER] = job.token
env[Gitlab::Auth::UserAuthFinders::JOB_TOKEN_HEADER] = job.token
allow_any_instance_of(Gitlab::UserAccess).to receive(:allowed?).and_return(true)
expect(current_user).to be_nil
......@@ -398,7 +404,7 @@ describe API::Helpers do
context 'when using param' do
context 'when providing username' do
before do
params[API::Helpers::SUDO_PARAM] = user.username
set_param(API::Helpers::SUDO_PARAM, user.username)
end
it_behaves_like 'successful sudo'
......@@ -406,7 +412,7 @@ describe API::Helpers do
context 'when providing user ID' do
before do
params[API::Helpers::SUDO_PARAM] = user.id.to_s
set_param(API::Helpers::SUDO_PARAM, user.id.to_s)
end
it_behaves_like 'successful sudo'
......@@ -416,7 +422,7 @@ describe API::Helpers do
context 'when user does not exist' do
before do
params[API::Helpers::SUDO_PARAM] = 'nonexistent'
set_param(API::Helpers::SUDO_PARAM, 'nonexistent')
end
it 'raises an error' do
......@@ -430,11 +436,11 @@ describe API::Helpers do
token.scopes = %w[api]
token.save!
params[API::Helpers::SUDO_PARAM] = user.id.to_s
set_param(API::Helpers::SUDO_PARAM, user.id.to_s)
end
it 'raises an error' do
expect { current_user }.to raise_error API::APIGuard::InsufficientScopeError
expect { current_user }.to raise_error Gitlab::Auth::InsufficientScopeError
end
end
end
......@@ -444,7 +450,7 @@ describe API::Helpers do
token.user = user
token.save!
params[API::Helpers::SUDO_PARAM] = user.id.to_s
set_param(API::Helpers::SUDO_PARAM, user.id.to_s)
end
it 'raises an error' do
......@@ -468,7 +474,7 @@ describe API::Helpers do
context 'passed as param' do
before do
params[API::APIGuard::PRIVATE_TOKEN_PARAM] = token.token
set_param(Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_PARAM, token.token)
end
it_behaves_like 'sudo'
......@@ -476,7 +482,7 @@ describe API::Helpers do
context 'passed as header' do
before do
env[API::APIGuard::PRIVATE_TOKEN_HEADER] = token.token
env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = token.token
end
it_behaves_like 'sudo'
......
require 'spec_helper'
describe 'Rack Attack global throttles' do
let(:settings) { Gitlab::CurrentSettings.current_application_settings }
# Start with really high limits and override them with low limits to ensure
# the right settings are being exercised
let(:settings_to_set) do
{
throttle_unauthenticated_requests_per_period: 100,
throttle_unauthenticated_period_in_seconds: 1,
throttle_authenticated_api_requests_per_period: 100,
throttle_authenticated_api_period_in_seconds: 1,
throttle_authenticated_web_requests_per_period: 100,
throttle_authenticated_web_period_in_seconds: 1
}
end
let(:requests_per_period) { 1 }
let(:period_in_seconds) { 10000 }
let(:period) { period_in_seconds.seconds }
let(:url_that_does_not_require_authentication) { '/users/sign_in' }
let(:url_that_requires_authentication) { '/dashboard/snippets' }
let(:api_partial_url) { '/todos' }
around do |example|
# Instead of test environment's :null_store so the throttles can increment
Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new
# Make time-dependent tests deterministic
Timecop.freeze { example.run }
Rack::Attack.cache.store = Rails.cache
end
# Requires let variables:
# * throttle_setting_prefix (e.g. "throttle_authenticated_api" or "throttle_authenticated_web")
# * get_args
# * other_user_get_args
shared_examples_for 'rate-limited token-authenticated requests' do
before do
# Set low limits
settings_to_set[:"#{throttle_setting_prefix}_requests_per_period"] = requests_per_period
settings_to_set[:"#{throttle_setting_prefix}_period_in_seconds"] = period_in_seconds
end
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 'rejects requests over the rate limit' do
# At first, allow requests under the rate limit.
requests_per_period.times do
get(*get_args)
expect(response).to have_http_status 200
end
# the last straw
expect_rejection { get(*get_args) }
end
it 'allows requests after throttling and then waiting for the next period' do
requests_per_period.times do
get(*get_args)
expect(response).to have_http_status 200
end
expect_rejection { get(*get_args) }
Timecop.travel(period.from_now) do
requests_per_period.times do
get(*get_args)
expect(response).to have_http_status 200
end
expect_rejection { get(*get_args) }
end
end
it 'counts requests from different users separately, even from the same IP' do
requests_per_period.times do
get(*get_args)
expect(response).to have_http_status 200
end
# would be over the limit if this wasn't a different user
get(*other_user_get_args)
expect(response).to have_http_status 200
end
it 'counts all requests from the same user, even via different IPs' do
requests_per_period.times do
get(*get_args)
expect(response).to have_http_status 200
end
expect_any_instance_of(Rack::Attack::Request).to receive(:ip).and_return('1.2.3.4')
expect_rejection { get(*get_args) }
end
end
context 'when the throttle is disabled' do
before do
settings_to_set[:"#{throttle_setting_prefix}_enabled"] = false
stub_application_setting(settings_to_set)
end
it 'allows requests over the rate limit' do
(1 + requests_per_period).times do
get(*get_args)
expect(response).to have_http_status 200
end
end
end
end
describe 'unauthenticated requests' do
before do
# Set low limits
settings_to_set[:throttle_unauthenticated_requests_per_period] = requests_per_period
settings_to_set[:throttle_unauthenticated_period_in_seconds] = period_in_seconds
end
context 'when the throttle is enabled' do
before do
settings_to_set[:throttle_unauthenticated_enabled] = true
stub_application_setting(settings_to_set)
end
it 'rejects requests over the rate limit' do
# At first, allow requests under the rate limit.
requests_per_period.times do
get url_that_does_not_require_authentication
expect(response).to have_http_status 200
end
# the last straw
expect_rejection { get url_that_does_not_require_authentication }
end
it 'allows requests after throttling and then waiting for the next period' do
requests_per_period.times do
get url_that_does_not_require_authentication
expect(response).to have_http_status 200
end
expect_rejection { get url_that_does_not_require_authentication }
Timecop.travel(period.from_now) do
requests_per_period.times do
get url_that_does_not_require_authentication
expect(response).to have_http_status 200
end
expect_rejection { get url_that_does_not_require_authentication }
end
end
it 'counts requests from different IPs separately' do
requests_per_period.times do
get url_that_does_not_require_authentication
expect(response).to have_http_status 200
end
expect_any_instance_of(Rack::Attack::Request).to receive(:ip).and_return('1.2.3.4')
# would be over limit for the same IP
get url_that_does_not_require_authentication
expect(response).to have_http_status 200
end
end
context 'when the throttle is disabled' do
before do
settings_to_set[:throttle_unauthenticated_enabled] = false
stub_application_setting(settings_to_set)
end
it 'allows requests over the rate limit' do
(1 + requests_per_period).times do
get url_that_does_not_require_authentication
expect(response).to have_http_status 200
end
end
end
end
describe 'API requests authenticated with personal access token', :api do
let(:user) { create(:user) }
let(:token) { create(:personal_access_token, user: user) }
let(:other_user) { create(:user) }
let(:other_user_token) { create(:personal_access_token, user: other_user) }
let(:throttle_setting_prefix) { 'throttle_authenticated_api' }
context 'with the token in the query string' do
let(:get_args) { [api(api_partial_url, personal_access_token: token)] }
let(:other_user_get_args) { [api(api_partial_url, personal_access_token: other_user_token)] }
it_behaves_like 'rate-limited token-authenticated requests'
end
context 'with the token in the headers' do
let(:get_args) { api_get_args_with_token_headers(api_partial_url, personal_access_token_headers(token)) }
let(:other_user_get_args) { api_get_args_with_token_headers(api_partial_url, personal_access_token_headers(other_user_token)) }
it_behaves_like 'rate-limited token-authenticated requests'
end
end
describe 'API requests authenticated with OAuth token', :api do
let(:user) { create(:user) }
let(:application) { Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user) }
let(:token) { Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id, scopes: "api") }
let(:other_user) { create(:user) }
let(:other_user_application) { Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: other_user) }
let(:other_user_token) { Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: other_user.id, scopes: "api") }
let(:throttle_setting_prefix) { 'throttle_authenticated_api' }
context 'with the token in the query string' do
let(:get_args) { [api(api_partial_url, oauth_access_token: token)] }
let(:other_user_get_args) { [api(api_partial_url, oauth_access_token: other_user_token)] }
it_behaves_like 'rate-limited token-authenticated requests'
end
context 'with the token in the headers' do
let(:get_args) { api_get_args_with_token_headers(api_partial_url, oauth_token_headers(token)) }
let(:other_user_get_args) { api_get_args_with_token_headers(api_partial_url, oauth_token_headers(other_user_token)) }
it_behaves_like 'rate-limited token-authenticated requests'
end
end
describe '"web" (non-API) requests authenticated with RSS token' do
let(:user) { create(:user) }
let(:other_user) { create(:user) }
let(:throttle_setting_prefix) { 'throttle_authenticated_web' }
context 'with the token in the query string' do
let(:get_args) { [rss_url(user), nil] }
let(:other_user_get_args) { [rss_url(other_user), nil] }
it_behaves_like 'rate-limited token-authenticated requests'
end
end
describe 'web requests authenticated with regular login' do
let(:user) { create(:user) }
before do
login_as(user)
# Set low limits
settings_to_set[:throttle_authenticated_web_requests_per_period] = requests_per_period
settings_to_set[:throttle_authenticated_web_period_in_seconds] = period_in_seconds
end
context 'when the throttle is enabled' do
before do
settings_to_set[:throttle_authenticated_web_enabled] = true
stub_application_setting(settings_to_set)
end
it 'rejects requests over the rate limit' do
# At first, allow requests under the rate limit.
requests_per_period.times do
get url_that_requires_authentication
expect(response).to have_http_status 200
end
# the last straw
expect_rejection { get url_that_requires_authentication }
end
it 'allows requests after throttling and then waiting for the next period' do
requests_per_period.times do
get url_that_requires_authentication
expect(response).to have_http_status 200
end
expect_rejection { get url_that_requires_authentication }
Timecop.travel(period.from_now) do
requests_per_period.times do
get url_that_requires_authentication
expect(response).to have_http_status 200
end
expect_rejection { get url_that_requires_authentication }
end
end
it 'counts requests from different users separately, even from the same IP' do
requests_per_period.times do
get url_that_requires_authentication
expect(response).to have_http_status 200
end
# would be over the limit if this wasn't a different user
login_as(create(:user))
get url_that_requires_authentication
expect(response).to have_http_status 200
end
it 'counts all requests from the same user, even via different IPs' do
requests_per_period.times do
get url_that_requires_authentication
expect(response).to have_http_status 200
end
expect_any_instance_of(Rack::Attack::Request).to receive(:ip).and_return('1.2.3.4')
expect_rejection { get url_that_requires_authentication }
end
end
context 'when the throttle is disabled' do
before do
settings_to_set[:throttle_authenticated_web_enabled] = false
stub_application_setting(settings_to_set)
end
it 'allows requests over the rate limit' do
(1 + requests_per_period).times do
get url_that_requires_authentication
expect(response).to have_http_status 200
end
end
end
end
def api_get_args_with_token_headers(partial_url, token_headers)
["/api/#{API::API.version}#{partial_url}", nil, token_headers]
end
def rss_url(user)
"/dashboard/projects.atom?rss_token=#{user.rss_token}"
end
def private_token_headers(user)
{ 'HTTP_PRIVATE_TOKEN' => user.private_token }
end
def personal_access_token_headers(personal_access_token)
{ 'HTTP_PRIVATE_TOKEN' => personal_access_token.token }
end
def oauth_token_headers(oauth_access_token)
{ 'AUTHORIZATION' => "Bearer #{oauth_access_token.token}" }
end
def expect_rejection(&block)
yield
expect(response).to have_http_status(429)
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