summaryrefslogtreecommitdiff
path: root/lib/gitlab/action_rate_limiter.rb
blob: fdb06d005482c498b6fdaa0e2d655a902dd02894 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
# 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 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

    # 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
    def increment(key)
      value = 0

      Gitlab::Redis::Cache.with do |redis|
        cache_key = action_key(key)
        value = redis.incr(cache_key)
        redis.expire(cache_key, expiry_time) if value == 1
      end

      value
    end

    # Increments the given key and returns true if the action should
    # be throttled.
    #
    # 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.
    def throttled?(key, threshold_value)
      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)
    end

    private

    def action_key(key)
      serialized = key.map do |obj|
        if obj.is_a?(String)
          "#{obj}"
        else
          "#{obj.class.model_name.to_s.underscore}:#{obj.id}"
        end
      end.join(":")

      "action_rate_limiter:#{action}:#{serialized}"
    end
  end
end