2018-10-22 03:00:50 -04:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
2017-12-09 04:01:42 -05:00
|
|
|
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 level.
|
|
|
|
class ActionRateLimiter
|
|
|
|
TIME_TO_EXPIRE = 60 # 1 min
|
|
|
|
|
|
|
|
attr_accessor :action, :expiry_time
|
|
|
|
|
|
|
|
def initialize(action:, expiry_time: TIME_TO_EXPIRE)
|
|
|
|
@action = action
|
|
|
|
@expiry_time = expiry_time
|
|
|
|
end
|
|
|
|
|
2017-12-12 18:46:05 -05:00
|
|
|
# Increments the given cache key and increments the value by 1 with the
|
|
|
|
# given expiration time. Returns the incremented value.
|
|
|
|
#
|
|
|
|
# key - An array of ActiveRecord instances
|
2017-12-09 04:01:42 -05:00
|
|
|
def increment(key)
|
|
|
|
value = 0
|
|
|
|
|
|
|
|
Gitlab::Redis::Cache.with do |redis|
|
2017-12-12 18:46:05 -05:00
|
|
|
cache_key = action_key(key)
|
2017-12-09 04:01:42 -05:00
|
|
|
value = redis.incr(cache_key)
|
|
|
|
redis.expire(cache_key, expiry_time) if value == 1
|
|
|
|
end
|
|
|
|
|
2017-12-09 18:45:01 -05:00
|
|
|
value
|
2017-12-09 04:01:42 -05:00
|
|
|
end
|
|
|
|
|
2017-12-12 18:46:05 -05:00
|
|
|
# Increments the given key and returns true if the action should
|
|
|
|
# be throttled.
|
|
|
|
#
|
2019-07-24 15:49:31 -04:00
|
|
|
# key - An array of ActiveRecord instances or strings
|
|
|
|
# threshold_value - The maximum number of times this action should occur in the given time interval. If number is zero is considered disabled.
|
2017-12-09 04:01:42 -05:00
|
|
|
def throttled?(key, threshold_value)
|
2019-07-24 15:49:31 -04:00
|
|
|
threshold_value > 0 &&
|
|
|
|
self.increment(key) > threshold_value
|
|
|
|
end
|
|
|
|
|
|
|
|
# Logs request into auth.log
|
|
|
|
#
|
|
|
|
# request - Web request to be logged
|
|
|
|
# type - A symbol key that represents the request.
|
|
|
|
# current_user - Current user of the request, it can be nil.
|
|
|
|
def log_request(request, type, current_user)
|
|
|
|
request_information = {
|
|
|
|
message: 'Action_Rate_Limiter_Request',
|
|
|
|
env: type,
|
|
|
|
ip: request.ip,
|
|
|
|
request_method: request.request_method,
|
|
|
|
fullpath: request.fullpath
|
|
|
|
}
|
|
|
|
|
|
|
|
if current_user
|
|
|
|
request_information.merge!({
|
|
|
|
user_id: current_user.id,
|
|
|
|
username: current_user.username
|
|
|
|
})
|
|
|
|
end
|
|
|
|
|
|
|
|
Gitlab::AuthLogger.error(request_information)
|
2017-12-09 04:01:42 -05:00
|
|
|
end
|
2017-12-12 18:46:05 -05:00
|
|
|
|
|
|
|
private
|
|
|
|
|
|
|
|
def action_key(key)
|
2019-07-24 15:49:31 -04:00
|
|
|
serialized = key.map do |obj|
|
|
|
|
if obj.is_a?(String)
|
|
|
|
"#{obj}"
|
|
|
|
else
|
|
|
|
"#{obj.class.model_name.to_s.underscore}:#{obj.id}"
|
|
|
|
end
|
|
|
|
end.join(":")
|
|
|
|
|
2017-12-12 18:46:05 -05:00
|
|
|
"action_rate_limiter:#{action}:#{serialized}"
|
|
|
|
end
|
2017-12-09 04:01:42 -05:00
|
|
|
end
|
|
|
|
end
|