otto 2.0.0.pre2 → 2.0.0.pre3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +0 -2
  3. data/.github/workflows/claude-code-review.yml +29 -13
  4. data/CLAUDE.md +537 -0
  5. data/Gemfile +2 -1
  6. data/Gemfile.lock +17 -10
  7. data/benchmark_middleware_wrap.rb +163 -0
  8. data/changelog.d/20251014_144317_delano_54_thats_a_wrapper.rst +36 -0
  9. data/changelog.d/20251014_161526_delano_54_thats_a_wrapper.rst +5 -0
  10. data/docs/.gitignore +1 -0
  11. data/docs/ipaddr-encoding-quirk.md +34 -0
  12. data/docs/migrating/v2.0.0-pre2.md +11 -18
  13. data/examples/authentication_strategies/config.ru +0 -1
  14. data/lib/otto/core/configuration.rb +89 -39
  15. data/lib/otto/core/freezable.rb +93 -0
  16. data/lib/otto/core/middleware_stack.rb +24 -17
  17. data/lib/otto/core/router.rb +1 -1
  18. data/lib/otto/core.rb +8 -0
  19. data/lib/otto/env_keys.rb +8 -4
  20. data/lib/otto/helpers/request.rb +80 -2
  21. data/lib/otto/helpers/response.rb +3 -3
  22. data/lib/otto/helpers.rb +4 -0
  23. data/lib/otto/locale/config.rb +56 -0
  24. data/lib/otto/mcp.rb +3 -0
  25. data/lib/otto/privacy/config.rb +199 -0
  26. data/lib/otto/privacy/geo_resolver.rb +115 -0
  27. data/lib/otto/privacy/ip_privacy.rb +175 -0
  28. data/lib/otto/privacy/redacted_fingerprint.rb +136 -0
  29. data/lib/otto/privacy.rb +29 -0
  30. data/lib/otto/route_handlers/base.rb +1 -2
  31. data/lib/otto/route_handlers/factory.rb +16 -14
  32. data/lib/otto/route_handlers/logic_class.rb +2 -2
  33. data/lib/otto/security/authentication/{failure_result.rb → auth_failure.rb} +3 -3
  34. data/lib/otto/security/authentication/auth_strategy.rb +3 -3
  35. data/lib/otto/security/authentication/route_auth_wrapper.rb +137 -26
  36. data/lib/otto/security/authentication/strategies/noauth_strategy.rb +5 -1
  37. data/lib/otto/security/authentication.rb +3 -4
  38. data/lib/otto/security/config.rb +51 -7
  39. data/lib/otto/security/configurator.rb +0 -13
  40. data/lib/otto/security/middleware/ip_privacy_middleware.rb +211 -0
  41. data/lib/otto/security.rb +9 -0
  42. data/lib/otto/version.rb +1 -1
  43. data/lib/otto.rb +181 -86
  44. data/otto.gemspec +3 -0
  45. metadata +58 -3
  46. data/lib/otto/security/authentication/authentication_middleware.rb +0 -140
@@ -25,22 +25,41 @@ class Otto
25
25
  # wrapped.call(env, extra_params)
26
26
  #
27
27
  class RouteAuthWrapper
28
- attr_reader :wrapped_handler, :route_definition, :auth_config
28
+ attr_reader :wrapped_handler, :route_definition, :auth_config, :security_config
29
29
 
30
- def initialize(wrapped_handler, route_definition, auth_config)
30
+ def initialize(wrapped_handler, route_definition, auth_config, security_config = nil)
31
31
  @wrapped_handler = wrapped_handler
32
32
  @route_definition = route_definition
33
33
  @auth_config = auth_config # Hash: { auth_strategies: {}, default_auth_strategy: 'publicly' }
34
+ @security_config = security_config
35
+ @strategy_cache = {} # Cache resolved strategies to avoid repeated lookups
34
36
  end
35
37
 
36
38
  # Execute authentication then call wrapped handler
37
39
  #
40
+ # For routes WITHOUT auth requirement: Sets anonymous StrategyResult
41
+ # For routes WITH auth requirement: Enforces authentication
42
+ #
38
43
  # @param env [Hash] Rack environment
39
44
  # @param extra_params [Hash] Additional parameters
40
45
  # @return [Array] Rack response array
41
46
  def call(env, extra_params = {})
42
- # Execute authentication strategy for this route
43
47
  auth_requirement = route_definition.auth_requirement
48
+
49
+ # Routes without auth requirement get anonymous StrategyResult
50
+ unless auth_requirement
51
+ # Note: env['REMOTE_ADDR'] is masked by IPPrivacyMiddleware by default
52
+ metadata = { ip: env['REMOTE_ADDR'] }
53
+ metadata[:country] = env['otto.geo_country'] if env['otto.geo_country']
54
+
55
+ result = StrategyResult.anonymous(metadata: metadata)
56
+ env['otto.strategy_result'] = result
57
+ env['otto.user'] = nil
58
+ env['otto.user_context'] = result.user_context
59
+ return wrapped_handler.call(env, extra_params)
60
+ end
61
+
62
+ # Routes WITH auth requirement: Execute authentication strategy
44
63
  strategy = get_strategy(auth_requirement)
45
64
 
46
65
  unless strategy
@@ -51,36 +70,107 @@ class Otto
51
70
  # Execute the strategy
52
71
  result = strategy.authenticate(env, auth_requirement)
53
72
 
54
- # Set environment variables for controllers/logic
55
- env['otto.strategy_result'] = result
56
- env['otto.user'] = result.user if result.is_a?(StrategyResult)
57
- env['otto.user_context'] = result.user_context if result.is_a?(StrategyResult)
58
-
59
73
  # Handle authentication failure
60
- if result.is_a?(FailureResult)
74
+ if result.is_a?(AuthFailure)
75
+ # Create anonymous result with failure info for logging/auditing
76
+ # Note: env['REMOTE_ADDR'] is masked by IPPrivacyMiddleware by default
77
+ metadata = {
78
+ ip: env['REMOTE_ADDR'],
79
+ auth_failure: result.failure_reason,
80
+ attempted_strategy: auth_requirement
81
+ }
82
+ metadata[:country] = env['otto.geo_country'] if env['otto.geo_country']
83
+
84
+ env['otto.strategy_result'] = StrategyResult.anonymous(metadata: metadata)
85
+ env['otto.user'] = nil
86
+ env['otto.user_context'] = {}
61
87
  return auth_failure_response(env, result)
62
88
  end
63
89
 
90
+ # Set environment variables for controllers/logic on success
91
+ env['otto.strategy_result'] = result
92
+ env['otto.user'] = result.user
93
+ env['otto.user_context'] = result.user_context
94
+
95
+ # SESSION PERSISTENCE: This assignment is INTENTIONAL, not a merge operation.
96
+ # We must ensure env['rack.session'] and strategy_result.session reference
97
+ # the SAME object so that:
98
+ # 1. Logic classes write to strategy_result.session
99
+ # 2. Rack's session middleware persists env['rack.session']
100
+ # 3. Changes from (1) are included in (2)
101
+ #
102
+ # Using merge! instead would break this - the objects must be identical.
103
+ env['rack.session'] = result.session if result.is_a?(StrategyResult) && result.session
104
+
64
105
  # Authentication succeeded - call wrapped handler
65
106
  wrapped_handler.call(env, extra_params)
66
107
  end
67
108
 
68
109
  private
69
110
 
70
- # Get strategy from auth_config hash
111
+ # Get strategy from auth_config hash with sophisticated pattern matching
112
+ #
113
+ # Supports:
114
+ # - Exact match: 'authenticated' → looks up auth_config[:auth_strategies]['authenticated']
115
+ # - Prefix match: 'role:admin' → looks up 'role' strategy
116
+ # - Fallback: 'role:*' → creates default RoleStrategy
117
+ # - Fallback: 'permission:*' → creates default PermissionStrategy
118
+ #
119
+ # Results are cached to avoid repeated lookups for the same requirement.
71
120
  #
72
121
  # @param requirement [String] Auth requirement from route
73
122
  # @return [AuthStrategy, nil] Strategy instance or nil
74
123
  def get_strategy(requirement)
75
124
  return nil unless auth_config && auth_config[:auth_strategies]
76
125
 
77
- auth_config[:auth_strategies][requirement]
126
+ # Check cache first
127
+ return @strategy_cache[requirement] if @strategy_cache.key?(requirement)
128
+
129
+ # Try exact match first - this has highest priority
130
+ strategy = auth_config[:auth_strategies][requirement]
131
+ if strategy
132
+ @strategy_cache[requirement] = strategy
133
+ return strategy
134
+ end
135
+
136
+ # For colon-separated requirements like "role:admin", try prefix match
137
+ if requirement.include?(':')
138
+ prefix = requirement.split(':', 2).first
139
+
140
+ # Check if we have a strategy registered for the prefix
141
+ prefix_strategy = auth_config[:auth_strategies][prefix]
142
+ if prefix_strategy
143
+ @strategy_cache[requirement] = prefix_strategy
144
+ return prefix_strategy
145
+ end
146
+
147
+ # Try fallback patterns for role: and permission: requirements
148
+ if requirement.start_with?('role:')
149
+ # Cache the fallback strategy under 'role:' key to create it only once
150
+ strategy = @strategy_cache['role:'] ||= begin
151
+ auth_config[:auth_strategies]['role'] || Strategies::RoleStrategy.new([])
152
+ end
153
+ @strategy_cache[requirement] = strategy
154
+ return strategy
155
+ elsif requirement.start_with?('permission:')
156
+ # Cache the fallback strategy under 'permission:' key
157
+ strategy = @strategy_cache['permission:'] ||= begin
158
+ auth_config[:auth_strategies]['permission'] || Strategies::PermissionStrategy.new([])
159
+ end
160
+ @strategy_cache[requirement] = strategy
161
+ return strategy
162
+ end
163
+ end
164
+
165
+ # Cache nil results too to avoid repeated failed lookups
166
+ @strategy_cache[requirement] = nil
167
+ nil
78
168
  end
79
169
 
80
170
  # Generate 401 response for authentication failure
81
171
  #
82
172
  # @param env [Hash] Rack environment
83
- # @param result [FailureResult] Failure result from strategy
173
+ # @param result [AuthFailure] Failure result from strategy
84
174
  # @return [Array] Rack response array
85
175
  def auth_failure_response(env, result)
86
176
  # Check if request wants JSON
@@ -96,7 +186,7 @@ class Otto
96
186
 
97
187
  # Generate JSON 401 response
98
188
  #
99
- # @param result [FailureResult] Failure result
189
+ # @param result [AuthFailure] Failure result
100
190
  # @return [Array] Rack response array
101
191
  def json_auth_error(result)
102
192
  body = {
@@ -105,26 +195,31 @@ class Otto
105
195
  timestamp: Time.now.to_i
106
196
  }.to_json
107
197
 
108
- [
109
- 401,
110
- { 'content-type' => 'application/json' },
111
- [body]
112
- ]
198
+ headers = {
199
+ 'content-type' => 'application/json',
200
+ 'content-length' => body.bytesize.to_s
201
+ }
202
+
203
+ # Add security headers if available
204
+ merge_security_headers!(headers)
205
+
206
+ [401, headers, [body]]
113
207
  end
114
208
 
115
209
  # Generate HTML 401 response or redirect
116
210
  #
117
- # @param result [FailureResult] Failure result
211
+ # @param result [AuthFailure] Failure result
118
212
  # @return [Array] Rack response array
119
213
  def html_auth_error(result)
120
214
  # For HTML requests, redirect to login
121
215
  login_path = auth_config[:login_path] || '/signin'
122
216
 
123
- [
124
- 302,
125
- { 'location' => login_path },
126
- ["Redirecting to #{login_path}"]
127
- ]
217
+ headers = { 'location' => login_path }
218
+
219
+ # Add security headers if available
220
+ merge_security_headers!(headers)
221
+
222
+ [302, headers, ["Redirecting to #{login_path}"]]
128
223
  end
129
224
 
130
225
  # Generate generic unauthorized response
@@ -138,11 +233,27 @@ class Otto
138
233
 
139
234
  if wants_json
140
235
  body = { error: message }.to_json
141
- [401, { 'content-type' => 'application/json' }, [body]]
236
+ headers = {
237
+ 'content-type' => 'application/json',
238
+ 'content-length' => body.bytesize.to_s
239
+ }
240
+ merge_security_headers!(headers)
241
+ [401, headers, [body]]
142
242
  else
143
- [401, { 'content-type' => 'text/plain' }, [message]]
243
+ headers = { 'content-type' => 'text/plain' }
244
+ merge_security_headers!(headers)
245
+ [401, headers, [message]]
144
246
  end
145
247
  end
248
+
249
+ # Merge security headers into response headers
250
+ #
251
+ # @param headers [Hash] Response headers hash to merge into
252
+ def merge_security_headers!(headers)
253
+ return unless security_config
254
+
255
+ headers.merge!(security_config.security_headers)
256
+ end
146
257
  end
147
258
  end
148
259
  end
@@ -10,7 +10,11 @@ class Otto
10
10
  # Public access strategy - always allows access
11
11
  class NoAuthStrategy < AuthStrategy
12
12
  def authenticate(env, _requirement)
13
- Otto::Security::Authentication::StrategyResult.anonymous(metadata: { ip: env['REMOTE_ADDR'] })
13
+ # Note: env['REMOTE_ADDR'] is masked by IPPrivacyMiddleware by default
14
+ metadata = { ip: env['REMOTE_ADDR'] }
15
+ metadata[:country] = env['otto.geo_country'] if env['otto.geo_country']
16
+
17
+ Otto::Security::Authentication::StrategyResult.anonymous(metadata: metadata)
14
18
  end
15
19
  end
16
20
  end
@@ -7,8 +7,8 @@
7
7
 
8
8
  require_relative 'authentication/auth_strategy'
9
9
  require_relative 'authentication/strategy_result'
10
- require_relative 'authentication/failure_result'
11
- require_relative 'authentication/authentication_middleware'
10
+ require_relative 'authentication/auth_failure'
11
+ require_relative 'authentication/route_auth_wrapper'
12
12
 
13
13
  # Load all strategies
14
14
  require_relative 'authentication/strategies/noauth_strategy'
@@ -26,10 +26,9 @@ class Otto
26
26
  RoleStrategy = Authentication::Strategies::RoleStrategy
27
27
  APIKeyStrategy = Authentication::Strategies::APIKeyStrategy
28
28
  PermissionStrategy = Authentication::Strategies::PermissionStrategy
29
- AuthenticationMiddleware = Authentication::AuthenticationMiddleware
30
29
  end
31
30
 
32
31
  # Top-level backward compatibility aliases
33
32
  StrategyResult = Security::Authentication::StrategyResult
34
- FailureResult = Security::Authentication::FailureResult
33
+ AuthFailure = Security::Authentication::AuthFailure
35
34
  end
@@ -4,6 +4,7 @@
4
4
 
5
5
  require 'securerandom'
6
6
  require 'digest'
7
+ require_relative '../core/freezable'
7
8
 
8
9
  class Otto
9
10
  module Security
@@ -23,12 +24,15 @@ class Otto
23
24
  # config.max_request_size = 5 * 1024 * 1024 # 5MB
24
25
  # config.max_param_depth = 16
25
26
  class Config
26
- attr_accessor :csrf_protection, :csrf_token_key, :csrf_header_key, :csrf_session_key,
27
- :max_request_size, :max_param_depth, :max_param_keys,
28
- :trusted_proxies, :require_secure_cookies,
29
- :security_headers, :input_validation,
30
- :csp_nonce_enabled, :debug_csp, :mcp_auth,
31
- :rate_limiting_config
27
+ include Otto::Core::Freezable
28
+
29
+ attr_accessor :input_validation, :max_param_depth, :csrf_token_key, :rate_limiting_config, :csrf_session_key, :max_request_size, :max_param_keys
30
+
31
+ attr_reader :csrf_protection, :csrf_header_key,
32
+ :trusted_proxies, :require_secure_cookies,
33
+ :security_headers,
34
+ :csp_nonce_enabled, :debug_csp, :mcp_auth,
35
+ :ip_privacy_config
32
36
 
33
37
  # Initialize security configuration with safe defaults
34
38
  #
@@ -48,7 +52,8 @@ class Otto
48
52
  @input_validation = true
49
53
  @csp_nonce_enabled = false
50
54
  @debug_csp = false
51
- @rate_limiting_config = {}
55
+ @rate_limiting_config = { custom_rules: {} }
56
+ @ip_privacy_config = Otto::Privacy::Config.new
52
57
  end
53
58
 
54
59
  # Enable CSRF (Cross-Site Request Forgery) protection
@@ -60,14 +65,20 @@ class Otto
60
65
  # - Provide helper methods for forms and AJAX requests
61
66
  #
62
67
  # @return [void]
68
+ # @raise [FrozenError] if configuration is frozen
63
69
  def enable_csrf_protection!
70
+ raise FrozenError, 'Cannot modify frozen configuration' if frozen?
71
+
64
72
  @csrf_protection = true
65
73
  end
66
74
 
67
75
  # Disable CSRF protection
68
76
  #
69
77
  # @return [void]
78
+ # @raise [FrozenError] if configuration is frozen
70
79
  def disable_csrf_protection!
80
+ raise FrozenError, 'Cannot modify frozen configuration' if frozen?
81
+
71
82
  @csrf_protection = false
72
83
  end
73
84
 
@@ -86,6 +97,7 @@ class Otto
86
97
  #
87
98
  # @param proxy [String, Array] IP address, CIDR range, or array of addresses
88
99
  # @raise [ArgumentError] if proxy is not a String or Array
100
+ # @raise [FrozenError] if configuration is frozen
89
101
  # @return [void]
90
102
  #
91
103
  # @example Add single proxy
@@ -97,6 +109,8 @@ class Otto
97
109
  # @example Add multiple proxies
98
110
  # config.add_trusted_proxy(['10.0.0.1', '172.16.0.0/12'])
99
111
  def add_trusted_proxy(proxy)
112
+ raise FrozenError, 'Cannot modify frozen configuration' if frozen?
113
+
100
114
  case proxy
101
115
  when String, Regexp
102
116
  @trusted_proxies << proxy
@@ -175,7 +189,10 @@ class Otto
175
189
  # @param max_age [Integer] Maximum age in seconds (default: 1 year)
176
190
  # @param include_subdomains [Boolean] Apply to all subdomains (default: true)
177
191
  # @return [void]
192
+ # @raise [FrozenError] if configuration is frozen
178
193
  def enable_hsts!(max_age: 31_536_000, include_subdomains: true)
194
+ raise FrozenError, 'Cannot modify frozen configuration' if frozen?
195
+
179
196
  hsts_value = "max-age=#{max_age}"
180
197
  hsts_value += '; includeSubDomains' if include_subdomains
181
198
  @security_headers['strict-transport-security'] = hsts_value
@@ -188,10 +205,13 @@ class Otto
188
205
  #
189
206
  # @param policy [String] CSP policy string (default: "default-src 'self'")
190
207
  # @return [void]
208
+ # @raise [FrozenError] if configuration is frozen
191
209
  #
192
210
  # @example Custom policy
193
211
  # config.enable_csp!("default-src 'self'; script-src 'self' 'unsafe-inline'")
194
212
  def enable_csp!(policy = "default-src 'self'")
213
+ raise FrozenError, 'Cannot modify frozen configuration' if frozen?
214
+
195
215
  @security_headers['content-security-policy'] = policy
196
216
  end
197
217
 
@@ -203,10 +223,13 @@ class Otto
203
223
  #
204
224
  # @param debug [Boolean] Enable debug logging for CSP headers (default: false)
205
225
  # @return [void]
226
+ # @raise [FrozenError] if configuration is frozen
206
227
  #
207
228
  # @example
208
229
  # config.enable_csp_with_nonce!(debug: true)
209
230
  def enable_csp_with_nonce!(debug: false)
231
+ raise FrozenError, 'Cannot modify frozen configuration' if frozen?
232
+
210
233
  @csp_nonce_enabled = true
211
234
  @debug_csp = debug
212
235
  end
@@ -214,7 +237,10 @@ class Otto
214
237
  # Disable CSP nonce support
215
238
  #
216
239
  # @return [void]
240
+ # @raise [FrozenError] if configuration is frozen
217
241
  def disable_csp_nonce!
242
+ raise FrozenError, 'Cannot modify frozen configuration' if frozen?
243
+
218
244
  @csp_nonce_enabled = false
219
245
  end
220
246
 
@@ -246,7 +272,10 @@ class Otto
246
272
  #
247
273
  # @param option [String] Frame options: 'DENY', 'SAMEORIGIN', or 'ALLOW-FROM uri'
248
274
  # @return [void]
275
+ # @raise [FrozenError] if configuration is frozen
249
276
  def enable_frame_protection!(option = 'SAMEORIGIN')
277
+ raise FrozenError, 'Cannot modify frozen configuration' if frozen?
278
+
250
279
  @security_headers['x-frame-options'] = option
251
280
  end
252
281
 
@@ -254,6 +283,7 @@ class Otto
254
283
  #
255
284
  # @param headers [Hash] Hash of header name => value pairs
256
285
  # @return [void]
286
+ # @raise [FrozenError] if configuration is frozen
257
287
  #
258
288
  # @example
259
289
  # config.set_custom_headers({
@@ -261,9 +291,23 @@ class Otto
261
291
  # 'cross-origin-opener-policy' => 'same-origin'
262
292
  # })
263
293
  def set_custom_headers(headers)
294
+ raise FrozenError, 'Cannot modify frozen configuration' if frozen?
295
+
264
296
  @security_headers.merge!(headers)
265
297
  end
266
298
 
299
+ # Override deep_freeze! to ensure rate_limiting_config has custom_rules initialized
300
+ #
301
+ # This pre-initializes any lazy values before freezing to prevent FrozenError
302
+ # when accessing configuration after it's frozen.
303
+ #
304
+ # @return [self] The frozen configuration
305
+ def deep_freeze!
306
+ # Ensure custom_rules is initialized (should already be done in constructor)
307
+ @rate_limiting_config[:custom_rules] ||= {}
308
+ super
309
+ end
310
+
267
311
  def get_or_create_session_id(request)
268
312
  # Try existing sources first
269
313
  session_id = extract_existing_session_id(request)
@@ -4,7 +4,6 @@
4
4
 
5
5
  require_relative 'middleware/csrf_middleware'
6
6
  require_relative 'middleware/validation_middleware'
7
- require_relative 'authentication/authentication_middleware'
8
7
  require_relative 'middleware/rate_limit_middleware'
9
8
 
10
9
  # Security configuration facade for Otto framework
@@ -75,7 +74,6 @@ class Otto
75
74
  enable_hsts! if hsts
76
75
  enable_csp! if csp
77
76
  enable_frame_protection! if frame_protection
78
- enable_authentication! if authentication
79
77
  end
80
78
 
81
79
  # Enable CSRF protection for POST, PUT, DELETE, and PATCH requests.
@@ -118,7 +116,6 @@ class Otto
118
116
  # @option options [Integer] :period Time period in seconds (default: 60)
119
117
  # @option options [Proc] :condition Optional condition proc that receives request
120
118
  def add_rate_limit_rule(name, options)
121
- @security_config.rate_limiting_config[:custom_rules] ||= {}
122
119
  @security_config.rate_limiting_config[:custom_rules][name.to_s] = options
123
120
  end
124
121
 
@@ -171,21 +168,12 @@ class Otto
171
168
  @security_config.enable_csp_with_nonce!(debug: debug)
172
169
  end
173
170
 
174
- # Enable authentication middleware for route-level access control.
175
- # This will automatically check route auth parameters and enforce authentication.
176
- def enable_authentication!
177
- return if middleware_enabled?(Otto::Security::Authentication::AuthenticationMiddleware)
178
-
179
- @middleware_stack.add(Otto::Security::Authentication::AuthenticationMiddleware, @auth_config)
180
- end
181
-
182
171
  # Add a single authentication strategy
183
172
  #
184
173
  # @param name [String] Strategy name
185
174
  # @param strategy [Otto::Security::Authentication::AuthStrategy] Strategy instance
186
175
  def add_auth_strategy(name, strategy)
187
176
  @auth_config[:auth_strategies][name] = strategy
188
- enable_authentication!
189
177
  end
190
178
 
191
179
  # Configure authentication strategies for route-level access control.
@@ -196,7 +184,6 @@ class Otto
196
184
  # Merge new strategies with existing ones, preserving shared state
197
185
  @auth_config[:auth_strategies].merge!(strategies)
198
186
  @auth_config[:default_auth_strategy] = default_strategy
199
- enable_authentication! unless strategies.empty?
200
187
  end
201
188
 
202
189
  # Configure rate limiting settings.