2020-01-01 13:55:28 +05:30
# frozen_string_literal: true
module Gitlab
2022-04-04 11:22:00 +05:30
# This module implements a simple rate limiter that can be used to throttle
2020-01-01 13:55:28 +05:30
# certain actions. Unlike Rack Attack and Rack::Throttle, which operate at
# the middleware level, this can be used at the controller or API level.
2022-01-26 12:08:38 +05:30
# See CheckRateLimit concern for usage.
2022-04-04 11:22:00 +05:30
module ApplicationRateLimiter
2021-12-11 22:18:48 +05:30
InvalidKeyError = Class . new ( StandardError )
2020-01-01 13:55:28 +05:30
class << self
# Application rate limits
#
# Threshold value can be either an Integer or a Proc
# in order to not evaluate it's value every time this method is called
# and only do that when it's needed.
2022-04-04 11:22:00 +05:30
def rate_limits # rubocop:disable Metrics/AbcSize
2020-01-01 13:55:28 +05:30
{
2022-10-11 01:57:18 +05:30
issues_create : { threshold : - > { application_settings . issues_create_limit } , interval : 1 . minute } ,
notes_create : { threshold : - > { application_settings . notes_create_limit } , interval : 1 . minute } ,
project_export : { threshold : - > { application_settings . project_export_limit } , interval : 1 . minute } ,
project_download_export : { threshold : - > { application_settings . project_download_export_limit } , interval : 1 . minute } ,
2020-04-08 14:13:33 +05:30
project_repositories_archive : { threshold : 5 , interval : 1 . minute } ,
2022-10-11 01:57:18 +05:30
project_generate_new_export : { threshold : - > { application_settings . project_export_limit } , interval : 1 . minute } ,
project_import : { threshold : - > { application_settings . project_import_limit } , interval : 1 . minute } ,
project_testing_hook : { threshold : 5 , interval : 1 . minute } ,
play_pipeline_schedule : { threshold : 1 , interval : 1 . minute } ,
raw_blob : { threshold : - > { application_settings . raw_blob_request_limit } , interval : 1 . minute } ,
group_export : { threshold : - > { application_settings . group_export_limit } , interval : 1 . minute } ,
group_download_export : { threshold : - > { application_settings . group_download_export_limit } , interval : 1 . minute } ,
group_import : { threshold : - > { application_settings . group_import_limit } , interval : 1 . minute } ,
group_testing_hook : { threshold : 5 , interval : 1 . minute } ,
profile_add_new_email : { threshold : 5 , interval : 1 . minute } ,
web_hook_calls : { interval : 1 . minute } ,
web_hook_calls_mid : { interval : 1 . minute } ,
web_hook_calls_low : { interval : 1 . minute } ,
users_get_by_id : { threshold : - > { application_settings . users_get_by_id_limit } , interval : 10 . minutes } ,
username_exists : { threshold : 20 , interval : 1 . minute } ,
user_sign_up : { threshold : 20 , interval : 1 . minute } ,
user_sign_in : { threshold : 5 , interval : 10 . minutes } ,
profile_resend_email_confirmation : { threshold : 5 , interval : 1 . minute } ,
profile_update_username : { threshold : 10 , interval : 1 . minute } ,
update_environment_canary_ingress : { threshold : 1 , interval : 1 . minute } ,
auto_rollback_deployment : { threshold : 1 , interval : 3 . minutes } ,
search_rate_limit : { threshold : - > { application_settings . search_rate_limit } , interval : 1 . minute } ,
search_rate_limit_unauthenticated : { threshold : - > { application_settings . search_rate_limit_unauthenticated } , interval : 1 . minute } ,
gitlab_shell_operation : { threshold : 600 , interval : 1 . minute } ,
pipelines_create : { threshold : - > { application_settings . pipeline_limit_per_project_user_sha } , interval : 1 . minute } ,
temporary_email_failure : { threshold : 300 , interval : 1 . day } ,
permanent_email_failure : { threshold : 5 , interval : 1 . day } ,
project_testing_integration : { threshold : 5 , interval : 1 . minute } ,
email_verification : { threshold : 10 , interval : 10 . minutes } ,
email_verification_code_send : { threshold : 10 , interval : 1 . hour } ,
2023-03-04 22:38:38 +05:30
phone_verification_send_code : { threshold : 10 , interval : 1 . hour } ,
phone_verification_verify_code : { threshold : 10 , interval : 10 . minutes } ,
2022-10-11 01:57:18 +05:30
namespace_exists : { threshold : 20 , interval : 1 . minute } ,
2023-03-04 22:38:38 +05:30
fetch_google_ip_list : { threshold : 10 , interval : 1 . minute } ,
2023-05-27 22:25:52 +05:30
project_fork_sync : { threshold : 10 , interval : 30 . minutes } ,
2023-04-23 21:23:45 +05:30
jobs_index : { threshold : 600 , interval : 1 . minute } ,
2023-05-27 22:25:52 +05:30
bulk_import : { threshold : 6 , interval : 1 . minute } ,
projects_api_rate_limit_unauthenticated : {
threshold : - > { application_settings . projects_api_rate_limit_unauthenticated } , interval : 10 . minutes
}
2020-01-01 13:55:28 +05:30
} . freeze
end
# Increments the given key and returns true if the action should
# be throttled.
#
# @param key [Symbol] Key attribute registered in `.rate_limits`
2022-08-13 15:12:31 +05:30
# @param scope [Array<ActiveRecord>] Array of ActiveRecord models, Strings
# or Symbols to scope throttling to a specific request (e.g. per user
# per project)
# @param resource [ActiveRecord] An ActiveRecord model to count an action
# for (e.g. limit unique project (resource) downloads (action) to five
# per user (scope))
# @param threshold [Integer] Optional threshold value to override default
# one registered in `.rate_limits`
2022-08-27 11:52:29 +05:30
# @param interval [Integer] Optional interval value to override default
# one registered in `.rate_limits`
2022-08-13 15:12:31 +05:30
# @param users_allowlist [Array<String>] Optional list of usernames to
# exclude from the limit. This param will only be functional if Scope
# includes a current user.
# @param peek [Boolean] Optional. When true the key will not be
# incremented but the current throttled state will be returned.
2020-01-01 13:55:28 +05:30
#
# @return [Boolean] Whether or not a request should be throttled
2022-08-27 11:52:29 +05:30
def throttled? ( key , scope : , resource : nil , threshold : nil , interval : nil , users_allowlist : nil , peek : false )
2021-12-11 22:18:48 +05:30
raise InvalidKeyError unless rate_limits [ key ]
2020-01-01 13:55:28 +05:30
2022-08-13 15:12:31 +05:30
strategy = resource . present? ? IncrementPerActionedResource . new ( resource . id ) : IncrementPerAction . new
2022-07-16 23:28:13 +05:30
:: Gitlab :: Instrumentation :: RateLimitingGates . track ( key )
2022-01-26 12:08:38 +05:30
return false if scoped_user_in_allowlist? ( scope , users_allowlist )
2020-01-01 13:55:28 +05:30
2022-01-26 12:08:38 +05:30
threshold_value = threshold || threshold ( key )
2020-01-01 13:55:28 +05:30
2022-01-26 12:08:38 +05:30
return false if threshold_value == 0
2021-12-11 22:18:48 +05:30
2022-08-27 11:52:29 +05:30
interval_value = interval || interval ( key )
2022-08-13 15:12:31 +05:30
return false if interval_value == 0
2022-01-26 12:08:38 +05:30
# `period_key` is based on the current time and interval so when time passes to the next interval
# the key changes and the rate limit count starts again from 0.
# Based on https://github.com/rack/rack-attack/blob/886ba3a18d13c6484cd511a4dc9b76c0d14e5e96/lib/rack/attack/cache.rb#L63-L68
2021-12-11 22:18:48 +05:30
period_key , time_elapsed_in_period = Time . now . to_i . divmod ( interval_value )
2022-01-26 12:08:38 +05:30
cache_key = cache_key ( key , scope , period_key )
2021-12-11 22:18:48 +05:30
2022-01-26 12:08:38 +05:30
value = if peek
2022-08-13 15:12:31 +05:30
strategy . read ( cache_key )
2022-01-26 12:08:38 +05:30
else
2022-08-13 15:12:31 +05:30
# We add a 1 second buffer to avoid timing issues when we're at the end of a period
expiry = interval_value - time_elapsed_in_period + 1
strategy . increment ( cache_key , expiry )
2022-01-26 12:08:38 +05:30
end
2020-01-01 13:55:28 +05:30
2022-01-26 12:08:38 +05:30
value > threshold_value
end
2023-03-17 16:20:25 +05:30
# Similar to #throttled? above but checks for the bypass header in the request and logs the request when it is over the rate limit
#
# @param request [Http::Request] - Web request used to check the header and log
# @param current_user [User] Current user of the request, it can be nil
# @param key [Symbol] Key attribute registered in `.rate_limits`
# @param scope [Array<ActiveRecord>] Array of ActiveRecord models, Strings
# or Symbols to scope throttling to a specific request (e.g. per user
# per project)
# @param resource [ActiveRecord] An ActiveRecord model to count an action
# for (e.g. limit unique project (resource) downloads (action) to five
# per user (scope))
# @param threshold [Integer] Optional threshold value to override default
# one registered in `.rate_limits`
# @param interval [Integer] Optional interval value to override default
# one registered in `.rate_limits`
# @param users_allowlist [Array<String>] Optional list of usernames to
# exclude from the limit. This param will only be functional if Scope
# includes a current user.
# @param peek [Boolean] Optional. When true the key will not be
# incremented but the current throttled state will be returned.
#
# @return [Boolean] Whether or not a request should be throttled
def throttled_request? ( request , current_user , key , scope : , ** options )
if :: Gitlab :: Throttle . bypass_header . present? && request . get_header ( Gitlab :: Throttle . bypass_header ) == '1'
return false
end
throttled? ( key , scope : scope , ** options ) . tap do | throttled |
log_request ( request , " #{ key } _request_limit " . to_sym , current_user ) if throttled
end
end
2022-01-26 12:08:38 +05:30
# Returns the current rate limited state without incrementing the count.
#
# @param key [Symbol] Key attribute registered in `.rate_limits`
# @param scope [Array<ActiveRecord>] Array of ActiveRecord models to scope throttling to a specific request (e.g. per user per project)
# @param threshold [Integer] Optional threshold value to override default one registered in `.rate_limits`
2022-08-27 11:52:29 +05:30
# @param interval [Integer] Optional interval value to override default one registered in `.rate_limits`
2022-01-26 12:08:38 +05:30
# @param users_allowlist [Array<String>] Optional list of usernames to exclude from the limit. This param will only be functional if Scope includes a current user.
#
# @return [Boolean] Whether or not a request is currently throttled
2022-08-27 11:52:29 +05:30
def peek ( key , scope : , threshold : nil , interval : nil , users_allowlist : nil )
throttled? ( key , peek : true , scope : scope , threshold : threshold , interval : interval , users_allowlist : users_allowlist )
2020-01-01 13:55:28 +05:30
end
# Logs request using provided logger
#
# @param request [Http::Request] - Web request to be logged
# @param type [Symbol] A symbol key that represents the request
# @param current_user [User] Current user of the request, it can be nil
# @param logger [Logger] Logger to log request to a specific log file. Defaults to Gitlab::AuthLogger
def log_request ( request , type , current_user , logger = Gitlab :: AuthLogger )
request_information = {
2022-10-11 01:57:18 +05:30
message : 'Application_Rate_Limiter_Request' ,
env : type ,
remote_ip : request . ip ,
2020-01-01 13:55:28 +05:30
request_method : request . request_method ,
2022-10-11 01:57:18 +05:30
path : request . fullpath
2020-01-01 13:55:28 +05:30
}
if current_user
request_information . merge! ( {
2022-10-11 01:57:18 +05:30
user_id : current_user . id ,
2020-01-01 13:55:28 +05:30
username : current_user . username
} )
end
logger . error ( request_information )
end
private
def threshold ( key )
value = rate_limit_value_by_key ( key , :threshold )
2022-08-13 15:12:31 +05:30
rate_limit_value ( value )
2020-01-01 13:55:28 +05:30
end
def interval ( key )
2022-08-13 15:12:31 +05:30
value = rate_limit_value_by_key ( key , :interval )
2020-01-01 13:55:28 +05:30
2022-08-13 15:12:31 +05:30
rate_limit_value ( value )
2020-01-01 13:55:28 +05:30
end
2022-08-13 15:12:31 +05:30
def rate_limit_value ( value )
value = value . call if value . is_a? ( Proc )
2022-01-26 12:08:38 +05:30
2022-08-13 15:12:31 +05:30
value . to_i
2022-01-26 12:08:38 +05:30
end
2022-08-13 15:12:31 +05:30
def rate_limit_value_by_key ( key , setting )
action = rate_limits [ key ]
action [ setting ] if action
2022-01-26 12:08:38 +05:30
end
def cache_key ( key , scope , period_key )
2020-01-01 13:55:28 +05:30
composed_key = [ key , scope ] . flatten . compact
serialized = composed_key . map do | obj |
if obj . is_a? ( String ) || obj . is_a? ( Symbol )
" #{ obj } "
else
" #{ obj . class . model_name . to_s . underscore } : #{ obj . id } "
end
end . join ( " : " )
2022-01-26 12:08:38 +05:30
" application_rate_limiter: #{ serialized } : #{ period_key } "
2020-01-01 13:55:28 +05:30
end
2020-07-28 23:09:34 +05:30
def application_settings
Gitlab :: CurrentSettings . current_application_settings
end
2021-03-11 19:13:27 +05:30
2022-01-26 12:08:38 +05:30
def scoped_user_in_allowlist? ( scope , users_allowlist )
return unless users_allowlist . present?
2021-03-11 19:13:27 +05:30
2022-01-26 12:08:38 +05:30
scoped_user = [ scope ] . flatten . find { | s | s . is_a? ( User ) }
2021-03-11 19:13:27 +05:30
return unless scoped_user
2022-01-26 12:08:38 +05:30
scoped_user . username . downcase . in? ( users_allowlist )
2021-03-11 19:13:27 +05:30
end
2020-01-01 13:55:28 +05:30
end
end
end
2022-07-23 23:45:48 +05:30
Gitlab :: ApplicationRateLimiter . prepend_mod