Module: Gitlab::RackAttack

Defined in:
lib/gitlab/rack_attack.rb,
lib/gitlab/rack_attack/request.rb,
lib/gitlab/rack_attack/user_allowlist.rb,
lib/gitlab/rack_attack/instrumented_cache_store.rb

Defined Under Namespace

Modules: Request Classes: InstrumentedCacheStore, UserAllowlist

Class Method Summary collapse

Class Method Details

.configure(rack_attack) ⇒ Object


10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# File 'lib/gitlab/rack_attack.rb', line 10

def self.configure(rack_attack)
  # This adds some methods used by our throttles to the `Rack::Request`
  rack_attack::Request.include(Gitlab::RackAttack::Request)

  # This is Rack::Attack::DEFAULT_THROTTLED_RESPONSE, modified to allow a custom response
  rack_attack.throttled_response = lambda do |env|
    throttled_headers = Gitlab::RackAttack.throttled_response_headers(
      env['rack.attack.matched'], env['rack.attack.match_data']
    )
    [429, { 'Content-Type' => 'text/plain' }.merge(throttled_headers), [Gitlab::Throttle.rate_limiting_response_text]]
  end

  rack_attack.cache.store = Gitlab::RackAttack::InstrumentedCacheStore.new

  # Configure the throttles
  configure_throttles(rack_attack)

  configure_user_allowlist
end

.configure_throttles(rack_attack) ⇒ Object


84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
# File 'lib/gitlab/rack_attack.rb', line 84

def self.configure_throttles(rack_attack)
  # Each of these settings follows the same pattern of specifying separate
  # authenticated and unauthenticated rates via settings
  Gitlab::Throttle::REGULAR_THROTTLES.each do |throttle|
    unauthenticated_options = Gitlab::Throttle.options(throttle, authenticated: false)
    throttle_or_track(rack_attack, "throttle_unauthenticated_#{throttle}", unauthenticated_options) do |req|
      if req.throttle?(throttle, authenticated: false)
        req.ip
      end
    end

    authenticated_options = Gitlab::Throttle.options(throttle, authenticated: true)
    throttle_or_track(rack_attack, "throttle_authenticated_#{throttle}", authenticated_options) do |req|
      if req.throttle?(throttle, authenticated: true)
        req.throttled_identifer([:api])
      end
    end
  end

  throttle_or_track(rack_attack, 'throttle_unauthenticated_web', Gitlab::Throttle.unauthenticated_web_options) do |req|
    if req.throttle_unauthenticated_web?
      req.ip
    end
  end

  # Product analytics feature is in experimental stage.
  # At this point we want to limit amount of events registered
  # per application (aid stands for application id).
  throttle_or_track(rack_attack, 'throttle_product_analytics_collector', limit: 100, period: 60) do |req|
    if req.product_analytics_collector_request?
      req.params['aid']
    end
  end

  throttle_or_track(rack_attack, 'throttle_authenticated_web', Gitlab::Throttle.authenticated_web_options) do |req|
    if req.throttle_authenticated_web?
      req.throttled_identifer([:api, :rss, :ics])
    end
  end

  throttle_or_track(rack_attack, 'throttle_unauthenticated_protected_paths', Gitlab::Throttle.protected_paths_options) do |req|
    if req.throttle_unauthenticated_protected_paths?
      req.ip
    end
  end

  throttle_or_track(rack_attack, 'throttle_authenticated_protected_paths_api', Gitlab::Throttle.protected_paths_options) do |req|
    if req.throttle_authenticated_protected_paths_api?
      req.throttled_identifer([:api])
    end
  end

  throttle_or_track(rack_attack, 'throttle_authenticated_protected_paths_web', Gitlab::Throttle.protected_paths_options) do |req|
    if req.throttle_authenticated_protected_paths_web?
      req.throttled_identifer([:api, :rss, :ics])
    end
  end

  throttle_or_track(rack_attack, 'throttle_authenticated_git_lfs', Gitlab::Throttle.throttle_authenticated_git_lfs_options) do |req|
    if req.throttle_authenticated_git_lfs?
      req.throttled_identifer([:api])
    end
  end

  rack_attack.safelist('throttle_bypass_header') do |req|
    Gitlab::Throttle.bypass_header.present? &&
      req.get_header(Gitlab::Throttle.bypass_header) == '1'
  end
end

.configure_user_allowlistObject


79
80
81
82
# File 'lib/gitlab/rack_attack.rb', line 79

def self.configure_user_allowlist
  @user_allowlist = nil
  user_allowlist
end

.throttle_or_track(rack_attack, throttle_name, *args, &block) ⇒ Object


154
155
156
157
158
159
160
# File 'lib/gitlab/rack_attack.rb', line 154

def self.throttle_or_track(rack_attack, throttle_name, *args, &block)
  if track?(throttle_name)
    rack_attack.track(throttle_name, *args, &block)
  else
    rack_attack.throttle(throttle_name, *args, &block)
  end
end

.throttled_response_headers(matched, match_data) ⇒ Object

Rate Limit HTTP headers are not standardized anywhere. This is the latest draft submitted to IETF: github.com/ietf-wg-httpapi/ratelimit-headers/blob/main/draft-ietf-httpapi-ratelimit-headers.md

This method implement the most viable parts of the headers. Those headers will be sent back to the client when it gets throttled.

  • RateLimit-Limit: indicates the request quota associated to the client

in 60 seconds. The time window for the quota here is supposed to be mirrored to throttle_*_period_in_seconds application settings. However, our HAProxy as well as some ecosystem libraries are using a fixed 60-second window. Therefore, the returned limit is approximately rounded up to fit into that window.

  • RateLimit-Observed: indicates the current request amount associated to

the client within the time window.

  • RateLimit-Remaining: indicates the remaining quota within the time

window. It is the result of RateLimit-Limit - RateLimit-Remaining

  • Retry-After: the remaining duration in seconds until the quota is

reset. This is a standardized HTTP header: tools.ietf.org/html/rfc7231#page-69

  • RateLimit-Reset: the point of time that the request quota is reset, in Unix time

  • RateLimit-ResetTime: the point of time that the request quota is reset, in HTTP date format


57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# File 'lib/gitlab/rack_attack.rb', line 57

def self.throttled_response_headers(matched, match_data)
  # Match data example:
  # {:discriminator=>"127.0.0.1", :count=>12, :period=>60 seconds, :limit=>1, :epoch_time=>1609833930}
  # Source: https://github.com/rack/rack-attack/blob/v6.3.0/lib/rack/attack/throttle.rb#L33
  period = match_data[:period]
  limit = match_data[:limit]
  rounded_limit = (limit.to_f * 1.minute / match_data[:period]).ceil
  observed = match_data[:count]
  now = match_data[:epoch_time]
  retry_after = period - (now % period)
  reset_time = Time.at(now + retry_after) # rubocop:disable Rails/TimeZone
  {
    'RateLimit-Name' => matched.to_s,
    'RateLimit-Limit' => rounded_limit.to_s,
    'RateLimit-Observed' => observed.to_s,
    'RateLimit-Remaining' => (limit > observed ? limit - observed : 0).to_s,
    'RateLimit-Reset' => reset_time.to_i.to_s,
    'RateLimit-ResetTime' => reset_time.httpdate,
    'Retry-After' => retry_after.to_s
  }
end

.track?(name) ⇒ Boolean

Returns:

  • (Boolean)

162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
# File 'lib/gitlab/rack_attack.rb', line 162

def self.track?(name)
  dry_run_config = ENV['GITLAB_THROTTLE_DRY_RUN'].to_s.strip

  return false if dry_run_config.empty?
  return true if dry_run_config == '*'

  dry_run_throttles = dry_run_config.split(',').map(&:strip)

  # `throttle_unauthenticated` was split into API and web, so to maintain backwards-compatibility
  # this throttle name now controls both rate limits.
  if dry_run_throttles.include?('throttle_unauthenticated')
    dry_run_throttles += %w[throttle_unauthenticated_api throttle_unauthenticated_web]
  end

  dry_run_throttles.include?(name)
end

.user_allowlistObject


179
180
181
182
183
184
185
# File 'lib/gitlab/rack_attack.rb', line 179

def self.user_allowlist
  @user_allowlist ||= begin
    list = UserAllowlist.new(ENV['GITLAB_THROTTLE_USER_ALLOWLIST'])
    Gitlab::AuthLogger.info(gitlab_throttle_user_allowlist: list.to_a)
    list
  end
end