summaryrefslogtreecommitdiff
path: root/config/initializers/rack_attack_new.rb
blob: 267d4c1eda9518f0cdbdb20767e0e6b0be8ca375 (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
82
83
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
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
# Specs for this file can be found on:
# * spec/lib/gitlab/throttle_spec.rb
# * spec/requests/rack_attack_global_spec.rb
module Gitlab::Throttle
  def self.settings
    Gitlab::CurrentSettings.current_application_settings
  end

  # Returns true if we should use the Admin Area protected paths throttle
  def self.protected_paths_enabled?
    return false if should_use_omnibus_protected_paths?

    self.settings.throttle_protected_paths_enabled?
  end

  # To be removed in 13.0: https://gitlab.com/gitlab-org/gitlab/issues/29952
  def self.should_use_omnibus_protected_paths?
    !Settings.rack_attack.admin_area_protected_paths_enabled &&
      self.omnibus_protected_paths_present?
  end

  def self.omnibus_protected_paths_present?
    Rack::Attack.throttles.key?('protected paths')
  end

  def self.unauthenticated_options
    limit_proc = proc { |req| settings.throttle_unauthenticated_requests_per_period }
    period_proc = proc { |req| settings.throttle_unauthenticated_period_in_seconds.seconds }
    { limit: limit_proc, period: period_proc }
  end

  def self.authenticated_api_options
    limit_proc = proc { |req| settings.throttle_authenticated_api_requests_per_period }
    period_proc = proc { |req| settings.throttle_authenticated_api_period_in_seconds.seconds }
    { limit: limit_proc, period: period_proc }
  end

  def self.authenticated_web_options
    limit_proc = proc { |req| settings.throttle_authenticated_web_requests_per_period }
    period_proc = proc { |req| settings.throttle_authenticated_web_period_in_seconds.seconds }
    { limit: limit_proc, period: period_proc }
  end

  def self.protected_paths_options
    limit_proc = proc { |req| settings.throttle_protected_paths_requests_per_period }
    period_proc = proc { |req| settings.throttle_protected_paths_period_in_seconds.seconds }

    { limit: limit_proc, period: period_proc }
  end
end

class Rack::Attack
  # Order conditions by how expensive they are:
  # 1. The most expensive is the `req.unauthenticated?` and
  #    `req.authenticated_user_id` as it performs an expensive
  #    DB/Redis query to validate the request
  # 2. Slightly less expensive is the need to query DB/Redis
  #    to unmarshal settings (`Gitlab::Throttle.settings`)
  #
  # We deliberately skip `/-/health|liveness|readiness`
  # from Rack Attack as they need to always be accessible
  # by Load Balancer and additional measure is implemented
  # (token and whitelisting) to prevent abuse.
  throttle('throttle_unauthenticated', Gitlab::Throttle.unauthenticated_options) do |req|
    if !req.should_be_skipped? &&
        Gitlab::Throttle.settings.throttle_unauthenticated_enabled &&
        req.unauthenticated?
      req.ip
    end
  end

  throttle('throttle_authenticated_api', Gitlab::Throttle.authenticated_api_options) do |req|
    if req.api_request? &&
        Gitlab::Throttle.settings.throttle_authenticated_api_enabled
      req.authenticated_user_id([:api])
    end
  end

  throttle('throttle_authenticated_web', Gitlab::Throttle.authenticated_web_options) do |req|
    if req.web_request? &&
        Gitlab::Throttle.settings.throttle_authenticated_web_enabled
      req.authenticated_user_id([:api, :rss, :ics])
    end
  end

  throttle('throttle_unauthenticated_protected_paths', Gitlab::Throttle.protected_paths_options) do |req|
    if req.post? &&
        !req.should_be_skipped? &&
        req.protected_path? &&
        Gitlab::Throttle.protected_paths_enabled? &&
        req.unauthenticated?
      req.ip
    end
  end

  throttle('throttle_authenticated_protected_paths_api', Gitlab::Throttle.protected_paths_options) do |req|
    if req.post? &&
        req.api_request? &&
        req.protected_path? &&
        Gitlab::Throttle.protected_paths_enabled?
      req.authenticated_user_id([:api])
    end
  end

  throttle('throttle_authenticated_protected_paths_web', Gitlab::Throttle.protected_paths_options) do |req|
    if req.post? &&
        req.web_request? &&
        req.protected_path? &&
        Gitlab::Throttle.protected_paths_enabled?
      req.authenticated_user_id([:api, :rss, :ics])
    end
  end

  class Request
    def unauthenticated?
      !(authenticated_user_id([:api, :rss, :ics]) || authenticated_runner_id)
    end

    def authenticated_user_id(request_formats)
      request_authenticator.user(request_formats)&.id
    end

    def authenticated_runner_id
      request_authenticator.runner&.id
    end

    def api_request?
      path.start_with?('/api')
    end

    def api_internal_request?
      path =~ %r{^/api/v\d+/internal/}
    end

    def health_check_request?
      path =~ %r{^/-/(health|liveness|readiness)}
    end

    def should_be_skipped?
      api_internal_request? || health_check_request?
    end

    def web_request?
      !api_request? && !health_check_request?
    end

    def protected_path?
      !protected_path_regex.nil?
    end

    def protected_path_regex
      path =~ protected_paths_regex
    end

    private

    def request_authenticator
      @request_authenticator ||= Gitlab::Auth::RequestAuthenticator.new(self)
    end

    def protected_paths
      Gitlab::CurrentSettings.current_application_settings.protected_paths
    end

    def protected_paths_regex
      Regexp.union(protected_paths.map { |path| /\A#{Regexp.escape(path)}/ })
    end
  end
end

::Rack::Attack.extend_if_ee('::EE::Gitlab::Rack::Attack') # rubocop: disable Cop/InjectEnterpriseEditionModule
::Rack::Attack::Request.prepend_if_ee('::EE::Gitlab::Rack::Attack::Request')