debian-mirror-gitlab/lib/gitlab/application_rate_limiter.rb

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

245 lines
12 KiB
Ruby
Raw Normal View History

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 },
jobs_index: { threshold: 600, interval: 1.minute }
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