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

186 lines
7.4 KiB
Ruby
Raw Normal View History

2020-01-01 13:55:28 +05:30
# frozen_string_literal: true
module Gitlab
# This class implements a simple rate limiter that can be used to throttle
# certain actions. Unlike Rack Attack and Rack::Throttle, which operate at
# the middleware level, this can be used at the controller or API level.
#
# @example
# if Gitlab::ApplicationRateLimiter.throttled?(:project_export, scope: [@project, @current_user])
# flash[:alert] = 'error!'
# redirect_to(edit_project_path(@project), status: :too_many_requests)
# end
class ApplicationRateLimiter
2021-12-11 22:18:48 +05:30
InvalidKeyError = Class.new(StandardError)
2021-11-18 22:05:49 +05:30
def initialize(key, **options)
@key = key
@options = options
end
def throttled?
self.class.throttled?(key, **options)
end
def threshold_value
options[:threshold] || self.class.threshold(key)
end
def interval_value
self.class.interval(key)
end
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.
def rate_limits
{
2020-07-28 23:09:34 +05:30
issues_create: { threshold: -> { application_settings.issues_create_limit }, interval: 1.minute },
2021-03-11 19:13:27 +05:30
notes_create: { threshold: -> { application_settings.notes_create_limit }, interval: 1.minute },
2020-07-28 23:09:34 +05:30
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 },
2020-07-28 23:09:34 +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 },
2020-09-03 11:15:55 +05:30
project_testing_hook: { threshold: 5, interval: 1.minute },
2020-07-28 23:09:34 +05:30
play_pipeline_schedule: { threshold: 1, interval: 1.minute },
show_raw_controller: { 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 },
2020-09-03 11:15:55 +05:30
group_import: { threshold: -> { application_settings.group_import_limit }, interval: 1.minute },
2020-10-04 03:57:07 +05:30
group_testing_hook: { threshold: 5, interval: 1.minute },
profile_add_new_email: { threshold: 5, interval: 1.minute },
2021-06-08 01:23:25 +05:30
web_hook_calls: { interval: 1.minute },
2021-01-29 00:20:46 +05:30
profile_resend_email_confirmation: { threshold: 5, interval: 1.minute },
2021-02-22 17:27:13 +05:30
update_environment_canary_ingress: { threshold: 1, interval: 1.minute },
auto_rollback_deployment: { threshold: 1, interval: 3.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`
# @option scope [Array<ActiveRecord>] Array of ActiveRecord models to scope throttling to a specific request (e.g. per user per project)
# @option threshold [Integer] Optional threshold value to override default one registered in `.rate_limits`
2021-04-17 20:07:23 +05:30
# @option 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.
2020-01-01 13:55:28 +05:30
#
# @return [Boolean] Whether or not a request should be throttled
2021-03-11 19:13:27 +05:30
def throttled?(key, **options)
2021-12-11 22:18:48 +05:30
raise InvalidKeyError unless rate_limits[key]
2020-01-01 13:55:28 +05:30
2021-03-11 19:13:27 +05:30
return if scoped_user_in_allowlist?(options)
2020-01-01 13:55:28 +05:30
2021-03-11 19:13:27 +05:30
threshold_value = options[:threshold] || threshold(key)
2020-01-01 13:55:28 +05:30
threshold_value > 0 &&
2021-12-11 22:18:48 +05:30
increment(key, options[:scope]) > threshold_value
2020-01-01 13:55:28 +05:30
end
2021-12-11 22:18:48 +05:30
# Increments a cache key that is based on the current time and interval.
# So that when time passes to the next interval, the key changes and the count starts again from 0.
#
# Based on https://github.com/rack/rack-attack/blob/886ba3a18d13c6484cd511a4dc9b76c0d14e5e96/lib/rack/attack/cache.rb#L63-L68
2020-01-01 13:55:28 +05:30
#
# @param key [Symbol] Key attribute registered in `.rate_limits`
# @option scope [Array<ActiveRecord>] Array of ActiveRecord models to scope throttling to a specific request (e.g. per user per project)
#
# @return [Integer] incremented value
2021-12-11 22:18:48 +05:30
def increment(key, scope)
interval_value = interval(key)
period_key, time_elapsed_in_period = Time.now.to_i.divmod(interval_value)
cache_key = "#{action_key(key, scope)}:#{period_key}"
# 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
2020-01-01 13:55:28 +05:30
2021-11-18 22:05:49 +05:30
::Gitlab::Redis::RateLimiting.with do |redis|
2021-12-11 22:18:48 +05:30
redis.pipelined do
redis.incr(cache_key)
redis.expire(cache_key, expiry)
end.first
2020-01-01 13:55:28 +05:30
end
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 = {
message: 'Application_Rate_Limiter_Request',
env: type,
remote_ip: request.ip,
request_method: request.request_method,
path: request.fullpath
}
if current_user
request_information.merge!({
user_id: current_user.id,
username: current_user.username
})
end
logger.error(request_information)
end
private
def threshold(key)
value = rate_limit_value_by_key(key, :threshold)
return value.call if value.is_a?(Proc)
value.to_i
end
def interval(key)
rate_limit_value_by_key(key, :interval).to_i
end
def rate_limit_value_by_key(key, setting)
action = rate_limits[key]
action[setting] if action
end
def action_key(key, scope)
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(":")
"application_rate_limiter:#{serialized}"
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
def scoped_user_in_allowlist?(options)
return unless options[:users_allowlist].present?
scoped_user = [options[:scope]].flatten.find { |s| s.is_a?(User) }
return unless scoped_user
scoped_user.username.downcase.in?(options[:users_allowlist])
end
2020-01-01 13:55:28 +05:30
end
2021-11-18 22:05:49 +05:30
private
attr_reader :key, :options
2020-01-01 13:55:28 +05:30
end
end