otto 1.6.0 → 2.0.0.pre1

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 (136) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +1 -1
  3. data/.github/workflows/claude-code-review.yml +53 -0
  4. data/.github/workflows/claude.yml +49 -0
  5. data/.gitignore +3 -0
  6. data/.rubocop.yml +24 -345
  7. data/CHANGELOG.rst +83 -0
  8. data/CLAUDE.md +56 -0
  9. data/Gemfile +10 -3
  10. data/Gemfile.lock +23 -28
  11. data/README.md +2 -0
  12. data/bin/rspec +4 -4
  13. data/changelog.d/20250911_235619_delano_next.rst +28 -0
  14. data/changelog.d/20250912_123055_delano_remove_ostruct.rst +21 -0
  15. data/changelog.d/20250912_175625_claude_delano_remove_ostruct.rst +21 -0
  16. data/changelog.d/README.md +120 -0
  17. data/changelog.d/scriv.ini +5 -0
  18. data/docs/.gitignore +1 -0
  19. data/docs/migrating/v2.0.0-pre1.md +276 -0
  20. data/examples/.gitignore +1 -0
  21. data/examples/advanced_routes/README.md +33 -0
  22. data/examples/advanced_routes/app/controllers/handlers/async.rb +9 -0
  23. data/examples/advanced_routes/app/controllers/handlers/dynamic.rb +9 -0
  24. data/examples/advanced_routes/app/controllers/handlers/static.rb +9 -0
  25. data/examples/advanced_routes/app/controllers/modules/auth.rb +9 -0
  26. data/examples/advanced_routes/app/controllers/modules/transformer.rb +9 -0
  27. data/examples/advanced_routes/app/controllers/modules/validator.rb +9 -0
  28. data/examples/advanced_routes/app/controllers/routes_app.rb +232 -0
  29. data/examples/advanced_routes/app/controllers/v2/admin.rb +9 -0
  30. data/examples/advanced_routes/app/controllers/v2/config.rb +9 -0
  31. data/examples/advanced_routes/app/controllers/v2/settings.rb +9 -0
  32. data/examples/advanced_routes/app/logic/admin/logic/manager.rb +27 -0
  33. data/examples/advanced_routes/app/logic/admin/panel.rb +27 -0
  34. data/examples/advanced_routes/app/logic/analytics_processor.rb +25 -0
  35. data/examples/advanced_routes/app/logic/complex/business/handler.rb +27 -0
  36. data/examples/advanced_routes/app/logic/data_logic.rb +23 -0
  37. data/examples/advanced_routes/app/logic/data_processor.rb +25 -0
  38. data/examples/advanced_routes/app/logic/input_validator.rb +24 -0
  39. data/examples/advanced_routes/app/logic/nested/feature/logic.rb +27 -0
  40. data/examples/advanced_routes/app/logic/reports_generator.rb +27 -0
  41. data/examples/advanced_routes/app/logic/simple_logic.rb +25 -0
  42. data/examples/advanced_routes/app/logic/system/config/manager.rb +27 -0
  43. data/examples/advanced_routes/app/logic/test_logic.rb +23 -0
  44. data/examples/advanced_routes/app/logic/transform_logic.rb +23 -0
  45. data/examples/advanced_routes/app/logic/upload_logic.rb +23 -0
  46. data/examples/advanced_routes/app/logic/v2/logic/dashboard.rb +27 -0
  47. data/examples/advanced_routes/app/logic/v2/logic/processor.rb +27 -0
  48. data/examples/advanced_routes/app.rb +33 -0
  49. data/examples/advanced_routes/config.rb +23 -0
  50. data/examples/advanced_routes/config.ru +7 -0
  51. data/examples/advanced_routes/puma.rb +20 -0
  52. data/examples/advanced_routes/routes +167 -0
  53. data/examples/advanced_routes/run.rb +39 -0
  54. data/examples/advanced_routes/test.rb +58 -0
  55. data/examples/authentication_strategies/README.md +32 -0
  56. data/examples/authentication_strategies/app/auth.rb +68 -0
  57. data/examples/authentication_strategies/app/controllers/auth_controller.rb +29 -0
  58. data/examples/authentication_strategies/app/controllers/main_controller.rb +28 -0
  59. data/examples/authentication_strategies/config.ru +24 -0
  60. data/examples/authentication_strategies/routes +37 -0
  61. data/examples/basic/README.md +29 -0
  62. data/examples/basic/app.rb +7 -35
  63. data/examples/basic/routes +0 -9
  64. data/examples/mcp_demo/README.md +87 -0
  65. data/examples/mcp_demo/app.rb +29 -34
  66. data/examples/mcp_demo/config.ru +9 -60
  67. data/examples/security_features/README.md +46 -0
  68. data/examples/security_features/app.rb +23 -24
  69. data/examples/security_features/config.ru +8 -10
  70. data/lib/otto/core/configuration.rb +167 -0
  71. data/lib/otto/core/error_handler.rb +86 -0
  72. data/lib/otto/core/file_safety.rb +61 -0
  73. data/lib/otto/core/middleware_stack.rb +157 -0
  74. data/lib/otto/core/router.rb +183 -0
  75. data/lib/otto/core/uri_generator.rb +44 -0
  76. data/lib/otto/design_system.rb +7 -5
  77. data/lib/otto/helpers/base.rb +3 -0
  78. data/lib/otto/helpers/request.rb +10 -8
  79. data/lib/otto/helpers/response.rb +5 -4
  80. data/lib/otto/helpers/validation.rb +9 -7
  81. data/lib/otto/mcp/auth/token.rb +10 -9
  82. data/lib/otto/mcp/protocol.rb +24 -27
  83. data/lib/otto/mcp/rate_limiting.rb +8 -3
  84. data/lib/otto/mcp/registry.rb +7 -2
  85. data/lib/otto/mcp/route_parser.rb +10 -15
  86. data/lib/otto/mcp/server.rb +21 -11
  87. data/lib/otto/mcp/validation.rb +14 -10
  88. data/lib/otto/response_handlers/auto.rb +39 -0
  89. data/lib/otto/response_handlers/base.rb +16 -0
  90. data/lib/otto/response_handlers/default.rb +16 -0
  91. data/lib/otto/response_handlers/factory.rb +39 -0
  92. data/lib/otto/response_handlers/json.rb +28 -0
  93. data/lib/otto/response_handlers/redirect.rb +25 -0
  94. data/lib/otto/response_handlers/view.rb +24 -0
  95. data/lib/otto/response_handlers.rb +9 -135
  96. data/lib/otto/route.rb +9 -9
  97. data/lib/otto/route_definition.rb +15 -18
  98. data/lib/otto/route_handlers/base.rb +121 -0
  99. data/lib/otto/route_handlers/class_method.rb +89 -0
  100. data/lib/otto/route_handlers/factory.rb +29 -0
  101. data/lib/otto/route_handlers/instance_method.rb +69 -0
  102. data/lib/otto/route_handlers/lambda.rb +59 -0
  103. data/lib/otto/route_handlers/logic_class.rb +93 -0
  104. data/lib/otto/route_handlers.rb +10 -405
  105. data/lib/otto/security/authentication/auth_strategy.rb +44 -0
  106. data/lib/otto/security/authentication/authentication_middleware.rb +123 -0
  107. data/lib/otto/security/authentication/failure_result.rb +36 -0
  108. data/lib/otto/security/authentication/strategies/api_key_strategy.rb +40 -0
  109. data/lib/otto/security/authentication/strategies/permission_strategy.rb +47 -0
  110. data/lib/otto/security/authentication/strategies/public_strategy.rb +19 -0
  111. data/lib/otto/security/authentication/strategies/role_strategy.rb +57 -0
  112. data/lib/otto/security/authentication/strategies/session_strategy.rb +41 -0
  113. data/lib/otto/security/authentication/strategy_result.rb +223 -0
  114. data/lib/otto/security/authentication.rb +28 -282
  115. data/lib/otto/security/config.rb +14 -12
  116. data/lib/otto/security/configurator.rb +219 -0
  117. data/lib/otto/security/csrf.rb +8 -143
  118. data/lib/otto/security/middleware/csrf_middleware.rb +151 -0
  119. data/lib/otto/security/middleware/rate_limit_middleware.rb +38 -0
  120. data/lib/otto/security/middleware/validation_middleware.rb +252 -0
  121. data/lib/otto/security/rate_limiter.rb +86 -0
  122. data/lib/otto/security/rate_limiting.rb +10 -105
  123. data/lib/otto/security/validator.rb +8 -253
  124. data/lib/otto/static.rb +3 -0
  125. data/lib/otto/utils.rb +14 -0
  126. data/lib/otto/version.rb +3 -1
  127. data/lib/otto.rb +142 -498
  128. data/otto.gemspec +2 -2
  129. metadata +89 -28
  130. data/examples/dynamic_pages/app.rb +0 -115
  131. data/examples/dynamic_pages/config.ru +0 -30
  132. data/examples/dynamic_pages/routes +0 -21
  133. data/examples/helpers_demo/app.rb +0 -244
  134. data/examples/helpers_demo/config.ru +0 -26
  135. data/examples/helpers_demo/routes +0 -7
  136. data/lib/concurrent_cache_store.rb +0 -68
@@ -1,289 +1,35 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # lib/otto/security/authentication.rb
2
4
  #
3
- # Configurable authentication strategy system for Otto framework
4
- # Provides pluggable authentication patterns that can be customized per application
5
- #
6
- # Usage:
7
- # otto = Otto.new('routes.txt', {
8
- # auth_strategies: {
9
- # 'publically' => PublicStrategy.new,
10
- # 'authenticated' => SessionStrategy.new,
11
- # 'role:admin' => RoleStrategy.new(['admin']),
12
- # 'api_key' => APIKeyStrategy.new
13
- # }
14
- # })
15
-
16
- class Otto
17
- module Security
18
- # Base class for all authentication strategies
19
- class AuthStrategy
20
- # Check if the request meets the authentication requirements
21
- # @param env [Hash] Rack environment
22
- # @param requirement [String] Authentication requirement string
23
- # @return [AuthResult] Result containing success status and context
24
- def authenticate(env, requirement)
25
- raise NotImplementedError, 'Subclasses must implement #authenticate'
26
- end
27
-
28
- # Optional: Extract user context for authenticated requests
29
- # @param env [Hash] Rack environment
30
- # @return [Hash] User context hash
31
- def user_context(env)
32
- {}
33
- end
34
-
35
- protected
36
-
37
- # Helper to create successful auth result
38
- def success(user_context = {})
39
- AuthResult.new(true, user_context)
40
- end
41
-
42
- # Helper to create failed auth result
43
- def failure(reason = 'Authentication failed')
44
- AuthResult.new(false, {}, reason)
45
- end
46
- end
47
-
48
- # Result object for authentication attempts
49
- class AuthResult
50
- attr_reader :user_context, :failure_reason
51
-
52
- def initialize(success, user_context = {}, failure_reason = nil)
53
- @success = success
54
- @user_context = user_context
55
- @failure_reason = failure_reason
56
- end
57
-
58
- def success?
59
- @success
60
- end
61
-
62
- def failure?
63
- !@success
64
- end
65
- end
66
-
67
- # Public access strategy - always allows access
68
- class PublicStrategy < AuthStrategy
69
- def authenticate(env, requirement)
70
- success
71
- end
72
- end
73
-
74
- # Session-based authentication strategy
75
- class SessionStrategy < AuthStrategy
76
- def initialize(session_key: 'user_id', session_store: nil)
77
- @session_key = session_key
78
- @session_store = session_store
79
- end
80
-
81
- def authenticate(env, requirement)
82
- session = env['rack.session']
83
- return failure('No session available') unless session
84
-
85
- user_id = session[@session_key]
86
- return failure('Not authenticated') unless user_id
87
-
88
- success(user_id: user_id, session: session)
89
- end
90
-
91
- def user_context(env)
92
- session = env['rack.session']
93
- return {} unless session
94
-
95
- user_id = session[@session_key]
96
- user_id ? { user_id: user_id } : {}
97
- end
98
- end
99
-
100
- # Role-based authentication strategy
101
- class RoleStrategy < AuthStrategy
102
- def initialize(allowed_roles, session_key: 'user_roles')
103
- @allowed_roles = Array(allowed_roles)
104
- @session_key = session_key
105
- end
106
-
107
- def authenticate(env, requirement)
108
- session = env['rack.session']
109
- return failure('No session available') unless session
110
-
111
- user_roles = session[@session_key] || []
112
- user_roles = Array(user_roles)
113
-
114
- # For requirements like "role:admin", extract the role part
115
- if requirement.include?(':')
116
- required_role = requirement.split(':', 2).last
117
- if user_roles.include?(required_role)
118
- success(user_roles: user_roles, required_role: required_role)
119
- else
120
- failure("Insufficient privileges - requires role: #{required_role}")
121
- end
122
- else
123
- # For direct strategy matches, check if user has any of the allowed roles
124
- matching_roles = user_roles & @allowed_roles
125
- if matching_roles.any?
126
- success(user_roles: user_roles, allowed_roles: @allowed_roles, matching_roles: matching_roles)
127
- else
128
- failure("Insufficient privileges - requires one of roles: #{@allowed_roles.join(', ')}")
129
- end
130
- end
131
- end
132
-
133
- def user_context(env)
134
- session = env['rack.session']
135
- return {} unless session
136
-
137
- user_roles = session[@session_key] || []
138
- { user_roles: Array(user_roles) }
139
- end
140
- end
141
-
142
- # API key authentication strategy
143
- class APIKeyStrategy < AuthStrategy
144
- def initialize(api_keys: [], header_name: 'X-API-Key', param_name: 'api_key')
145
- @api_keys = Array(api_keys)
146
- @header_name = header_name
147
- @param_name = param_name
148
- end
149
-
150
- def authenticate(env, requirement)
151
- # Try header first, then query parameter
152
- api_key = env["HTTP_#{@header_name.upcase.tr('-', '_')}"]
5
+ # Index file for Otto authentication module
6
+ # Requires all authentication-related components for backward compatibility
153
7
 
154
- if api_key.nil?
155
- request = Rack::Request.new(env)
156
- api_key = request.params[@param_name]
157
- end
8
+ require_relative 'authentication/auth_strategy'
9
+ require_relative 'authentication/strategy_result'
10
+ require_relative 'authentication/failure_result'
11
+ require_relative 'authentication/authentication_middleware'
158
12
 
159
- return failure('No API key provided') unless api_key
13
+ # Load all strategies
14
+ require_relative 'authentication/strategies/public_strategy'
15
+ require_relative 'authentication/strategies/session_strategy'
16
+ require_relative 'authentication/strategies/role_strategy'
17
+ require_relative 'authentication/strategies/api_key_strategy'
18
+ require_relative 'authentication/strategies/permission_strategy'
160
19
 
161
- if @api_keys.empty? || @api_keys.include?(api_key)
162
- success(api_key: api_key)
163
- else
164
- failure('Invalid API key')
165
- end
166
- end
167
- end
168
-
169
- # Permission-based authentication strategy
170
- class PermissionStrategy < AuthStrategy
171
- def initialize(required_permissions, session_key: 'user_permissions')
172
- @required_permissions = Array(required_permissions)
173
- @session_key = session_key
174
- end
175
-
176
- def authenticate(env, requirement)
177
- session = env['rack.session']
178
- return failure('No session available') unless session
179
-
180
- user_permissions = session[@session_key] || []
181
- user_permissions = Array(user_permissions)
182
-
183
- # Extract permission from requirement (e.g., "permission:write" -> "write")
184
- required_permission = requirement.split(':', 2).last
185
-
186
- if user_permissions.include?(required_permission)
187
- success(user_permissions: user_permissions, required_permission: required_permission)
188
- else
189
- failure("Insufficient privileges - requires permission: #{required_permission}")
190
- end
191
- end
192
-
193
- def user_context(env)
194
- session = env['rack.session']
195
- return {} unless session
196
-
197
- user_permissions = session[@session_key] || []
198
- { user_permissions: Array(user_permissions) }
199
- end
200
- end
201
-
202
- # Authentication middleware that enforces route-level auth requirements
203
- class AuthenticationMiddleware
204
- def initialize(app, config = {})
205
- @app = app
206
- @config = config
207
- @strategies = config[:auth_strategies] || {}
208
- @default_strategy = config[:default_auth_strategy] || 'publically'
209
-
210
- # Add default public strategy if not provided
211
- @strategies['publically'] ||= PublicStrategy.new
212
- end
213
-
214
- def call(env)
215
- # Check if this route has auth requirements
216
- route_definition = env['otto.route_definition']
217
- return @app.call(env) unless route_definition
218
-
219
- auth_requirement = route_definition.auth_requirement
220
- return @app.call(env) unless auth_requirement
221
-
222
- # Find appropriate strategy
223
- strategy = find_strategy(auth_requirement)
224
- unless strategy
225
- return auth_error_response("Unknown authentication strategy: #{auth_requirement}")
226
- end
227
-
228
- # Perform authentication
229
- auth_result = strategy.authenticate(env, auth_requirement)
230
-
231
- if auth_result.success?
232
- # Add user context to environment for handlers to use
233
- env['otto.user_context'] = auth_result.user_context
234
- env['otto.auth_result'] = auth_result
235
- @app.call(env)
236
- else
237
- auth_error_response(auth_result.failure_reason)
238
- end
239
- end
240
-
241
- private
242
-
243
- def find_strategy(requirement)
244
- # Try exact match first - this has highest priority
245
- return @strategies[requirement] if @strategies[requirement]
246
-
247
- # For colon-separated requirements like "role:admin", try prefix match
248
- if requirement.include?(':')
249
- prefix = requirement.split(':', 2).first
250
-
251
- # Check if we have a strategy registered for the prefix
252
- prefix_strategy = @strategies[prefix]
253
- return prefix_strategy if prefix_strategy
254
-
255
- # Try fallback patterns for role: and permission: requirements
256
- if requirement.start_with?('role:')
257
- return @strategies['role'] || RoleStrategy.new([])
258
- elsif requirement.start_with?('permission:')
259
- return @strategies['permission'] || PermissionStrategy.new([])
260
- end
261
- end
262
-
263
- nil
264
- end
265
-
266
- def auth_error_response(message)
267
- body = JSON.generate({
268
- error: 'Authentication Required',
269
- message: message,
270
- timestamp: Time.now.to_i
271
- })
272
-
273
- headers = {
274
- 'Content-Type' => 'application/json',
275
- 'Content-Length' => body.bytesize.to_s
276
- }
277
-
278
- # Add security headers if available from config hash or Otto instance
279
- if @config.is_a?(Hash) && @config[:security_headers]
280
- headers.merge!(@config[:security_headers])
281
- elsif @config.respond_to?(:security_config) && @config.security_config
282
- headers.merge!(@config.security_config.security_headers)
283
- end
284
-
285
- [401, headers, [body]]
286
- end
287
- end
20
+ class Otto
21
+ module Security
22
+ # Backward compatibility aliases for the old namespace
23
+ AuthStrategy = Authentication::AuthStrategy
24
+ PublicStrategy = Authentication::Strategies::PublicStrategy
25
+ SessionStrategy = Authentication::Strategies::SessionStrategy
26
+ RoleStrategy = Authentication::Strategies::RoleStrategy
27
+ APIKeyStrategy = Authentication::Strategies::APIKeyStrategy
28
+ PermissionStrategy = Authentication::Strategies::PermissionStrategy
29
+ AuthenticationMiddleware = Authentication::AuthenticationMiddleware
288
30
  end
31
+
32
+ # Top-level backward compatibility aliases
33
+ StrategyResult = Security::Authentication::StrategyResult
34
+ FailureResult = Security::Authentication::FailureResult
289
35
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # lib/otto/security/config.rb
2
4
 
3
5
  require 'securerandom'
@@ -22,11 +24,11 @@ class Otto
22
24
  # config.max_param_depth = 16
23
25
  class Config
24
26
  attr_accessor :csrf_protection, :csrf_token_key, :csrf_header_key, :csrf_session_key,
25
- :max_request_size, :max_param_depth, :max_param_keys,
26
- :trusted_proxies, :require_secure_cookies,
27
- :security_headers, :input_validation,
28
- :csp_nonce_enabled, :debug_csp, :mcp_auth,
29
- :rate_limiting_config
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
30
32
 
31
33
  # Initialize security configuration with safe defaults
32
34
  #
@@ -96,12 +98,12 @@ class Otto
96
98
  # config.add_trusted_proxy(['10.0.0.1', '172.16.0.0/12'])
97
99
  def add_trusted_proxy(proxy)
98
100
  case proxy
99
- when String
101
+ when String, Regexp
100
102
  @trusted_proxies << proxy
101
103
  when Array
102
104
  @trusted_proxies.concat(proxy)
103
105
  else
104
- raise ArgumentError, 'Proxy must be a String or Array'
106
+ raise ArgumentError, 'Proxy must be a String, Regexp, or Array'
105
107
  end
106
108
  end
107
109
 
@@ -135,7 +137,7 @@ class Otto
135
137
  size = content_length.to_i
136
138
  if size > @max_request_size
137
139
  raise Otto::Security::RequestTooLargeError,
138
- "Request size #{size} exceeds maximum #{@max_request_size}"
140
+ "Request size #{size} exceeds maximum #{@max_request_size}"
139
141
  end
140
142
  true
141
143
  end
@@ -308,8 +310,8 @@ class Otto
308
310
  end
309
311
 
310
312
  def store_session_id(request, session_id)
311
- session = request.session
312
- session[csrf_session_key] = session_id if session
313
+ session = request.session
314
+ session[csrf_session_key] = session_id if session
313
315
  rescue StandardError
314
316
  # Cookie fallback handled in inject_csrf_token
315
317
  end
@@ -361,9 +363,9 @@ class Otto
361
363
  def development_csp_directives(nonce)
362
364
  [
363
365
  "default-src 'none';",
364
- "script-src 'nonce-#{nonce}' 'unsafe-inline';", # Allow inline scripts for development tools
366
+ "script-src 'nonce-#{nonce}' 'unsafe-inline';", # Allow inline scripts for development tools
365
367
  "style-src 'self' 'unsafe-inline';",
366
- "connect-src 'self' ws: wss: http: https:;", # Allow HTTP and all WebSocket connections for dev tools
368
+ "connect-src 'self' ws: wss: http: https:;", # Allow HTTP and all WebSocket connections for dev tools
367
369
  "img-src 'self' data:;",
368
370
  "font-src 'self';",
369
371
  "object-src 'none';",
@@ -0,0 +1,219 @@
1
+ # frozen_string_literal: true
2
+
3
+ # lib/otto/security/configurator.rb
4
+
5
+ require_relative 'middleware/csrf_middleware'
6
+ require_relative 'middleware/validation_middleware'
7
+ require_relative 'authentication/authentication_middleware'
8
+ require_relative 'middleware/rate_limit_middleware'
9
+
10
+ # Security configuration facade for Otto framework
11
+ class Otto
12
+ module Security
13
+ # Consolidates all security configuration methods into a single configurator class.
14
+ # This provides a unified interface for configuring CSRF protection, input validation,
15
+ # rate limiting, trusted proxies, and authentication strategies.
16
+ class Configurator
17
+ attr_reader :security_config, :middleware_stack
18
+ attr_accessor :auth_config
19
+
20
+ def initialize(security_config, middleware_stack, auth_config = nil)
21
+ @security_config = security_config
22
+ @middleware_stack = middleware_stack
23
+ # Use provided auth_config or initialize a new one
24
+ @auth_config = auth_config || { auth_strategies: {}, default_auth_strategy: 'publicly' }
25
+ end
26
+
27
+ # Unified security configuration method with sensible defaults
28
+ #
29
+ # Provides a comprehensive, one-stop configuration method for Otto's security features.
30
+ # This method allows configuring multiple security aspects in a single call, with flexible options.
31
+ #
32
+ # @param csrf_protection [Boolean, Hash] Enable CSRF protection
33
+ # - `true`: Enable with default settings
34
+ # - `Hash`: Provide custom CSRF configuration
35
+ # @param request_validation [Boolean] Enable input validation and sanitization
36
+ # @param rate_limiting [Boolean, Hash] Enable rate limiting
37
+ # - `true`: Enable with default settings
38
+ # - `Hash`: Provide custom rate limiting rules
39
+ # @param trusted_proxies [String, Array<String>] IP addresses or CIDR ranges to trust
40
+ # @param security_headers [Hash] Custom security headers to merge with defaults
41
+ # @param hsts [Boolean] Enable HTTP Strict Transport Security
42
+ # @param csp [Boolean, String] Enable Content Security Policy
43
+ # @param frame_protection [Boolean, String] Enable frame protection
44
+ # @param authentication [Boolean] Enable authentication
45
+ #
46
+ # @example Configure multiple security features in one call
47
+ # otto.security.configure(
48
+ # csrf_protection: true,
49
+ # request_validation: true,
50
+ # rate_limiting: { requests_per_minute: 100 },
51
+ # trusted_proxies: ['10.0.0.0/8'],
52
+ # security_headers: { 'x-custom-header' => 'value' },
53
+ # hsts: true,
54
+ # csp: "default-src 'self'",
55
+ # frame_protection: 'SAMEORIGIN'
56
+ # )
57
+ def configure(
58
+ csrf_protection: false,
59
+ request_validation: false,
60
+ rate_limiting: false,
61
+ trusted_proxies: [],
62
+ security_headers: {},
63
+ hsts: false,
64
+ csp: false,
65
+ frame_protection: false,
66
+ authentication: false
67
+ )
68
+ enable_csrf_protection! if csrf_protection
69
+ enable_request_validation! if request_validation
70
+ enable_rate_limiting!(rate_limiting.is_a?(Hash) ? rate_limiting : {}) if rate_limiting
71
+
72
+ Array(trusted_proxies).each { |proxy| add_trusted_proxy(proxy) }
73
+ self.security_headers = security_headers unless security_headers.empty?
74
+
75
+ enable_hsts! if hsts
76
+ enable_csp! if csp
77
+ enable_frame_protection! if frame_protection
78
+ enable_authentication! if authentication
79
+ end
80
+
81
+ # Enable CSRF protection for POST, PUT, DELETE, and PATCH requests.
82
+ # This will automatically add CSRF tokens to HTML forms and validate
83
+ # them on unsafe HTTP methods.
84
+ def enable_csrf_protection!
85
+ return if middleware_enabled?(Otto::Security::Middleware::CSRFMiddleware)
86
+
87
+ @security_config.enable_csrf_protection!
88
+ @middleware_stack.add(Otto::Security::Middleware::CSRFMiddleware)
89
+ end
90
+
91
+ # Enable request validation including input sanitization, size limits,
92
+ # and protection against XSS and SQL injection attacks.
93
+ def enable_request_validation!
94
+ return if middleware_enabled?(Otto::Security::Middleware::ValidationMiddleware)
95
+
96
+ @security_config.input_validation = true
97
+ @middleware_stack.add(Otto::Security::Middleware::ValidationMiddleware)
98
+ end
99
+
100
+ # Enable rate limiting to protect against abuse and DDoS attacks.
101
+ # This will automatically add rate limiting rules based on client IP.
102
+ #
103
+ # @param options [Hash] Rate limiting configuration options
104
+ # @option options [Integer] :requests_per_minute Maximum requests per minute per IP (default: 100)
105
+ # @option options [Hash] :custom_rules Custom rate limiting rules
106
+ def enable_rate_limiting!(options = {})
107
+ return if middleware_enabled?(Otto::Security::Middleware::RateLimitMiddleware)
108
+
109
+ configure_rate_limiting(options)
110
+ @middleware_stack.add(Otto::Security::Middleware::RateLimitMiddleware)
111
+ end
112
+
113
+ # Add a custom rate limiting rule.
114
+ #
115
+ # @param name [String, Symbol] Rule name
116
+ # @param options [Hash] Rule configuration
117
+ # @option options [Integer] :limit Maximum requests
118
+ # @option options [Integer] :period Time period in seconds (default: 60)
119
+ # @option options [Proc] :condition Optional condition proc that receives request
120
+ def add_rate_limit_rule(name, options)
121
+ @security_config.rate_limiting_config[:custom_rules] ||= {}
122
+ @security_config.rate_limiting_config[:custom_rules][name.to_s] = options
123
+ end
124
+
125
+ # Add a trusted proxy server for accurate client IP detection.
126
+ # Only requests from trusted proxies will have their forwarded headers honored.
127
+ #
128
+ # @param proxy [String, Regexp] IP address, CIDR range, or regex pattern
129
+ def add_trusted_proxy(proxy)
130
+ @security_config.add_trusted_proxy(proxy)
131
+ end
132
+
133
+ # Set custom security headers that will be added to all responses.
134
+ # These merge with the default security headers.
135
+ #
136
+ # @param headers [Hash] Hash of header name => value pairs
137
+ def security_headers=(headers)
138
+ @security_config.security_headers.merge!(headers)
139
+ end
140
+
141
+ # Enable HTTP Strict Transport Security (HSTS) header.
142
+ # WARNING: This can make your domain inaccessible if HTTPS is not properly
143
+ # configured. Only enable this when you're certain HTTPS is working correctly.
144
+ #
145
+ # @param max_age [Integer] Maximum age in seconds (default: 1 year)
146
+ # @param include_subdomains [Boolean] Apply to all subdomains (default: true)
147
+ def enable_hsts!(max_age: 31_536_000, include_subdomains: true)
148
+ @security_config.enable_hsts!(max_age: max_age, include_subdomains: include_subdomains)
149
+ end
150
+
151
+ # Enable Content Security Policy (CSP) header to prevent XSS attacks.
152
+ # The default policy only allows resources from the same origin.
153
+ #
154
+ # @param policy [String] CSP policy string (default: "default-src 'self'")
155
+ def enable_csp!(policy = "default-src 'self'")
156
+ @security_config.enable_csp!(policy)
157
+ end
158
+
159
+ # Enable X-Frame-Options header to prevent clickjacking attacks.
160
+ #
161
+ # @param option [String] Frame options: 'DENY', 'SAMEORIGIN', or 'ALLOW-FROM uri'
162
+ def enable_frame_protection!(option = 'SAMEORIGIN')
163
+ @security_config.enable_frame_protection!(option)
164
+ end
165
+
166
+ # Enable Content Security Policy (CSP) with nonce support for dynamic header generation.
167
+ # This enables the res.send_csp_headers response helper method.
168
+ #
169
+ # @param debug [Boolean] Enable debug logging for CSP headers (default: false)
170
+ def enable_csp_with_nonce!(debug: false)
171
+ @security_config.enable_csp_with_nonce!(debug: debug)
172
+ end
173
+
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
+ # Add a single authentication strategy
183
+ #
184
+ # @param name [String] Strategy name
185
+ # @param strategy [Otto::Security::Authentication::AuthStrategy] Strategy instance
186
+ def add_auth_strategy(name, strategy)
187
+ @auth_config[:auth_strategies][name] = strategy
188
+ enable_authentication!
189
+ end
190
+
191
+ # Configure authentication strategies for route-level access control.
192
+ #
193
+ # @param strategies [Hash] Hash mapping strategy names to strategy instances
194
+ # @param default_strategy [String] Default strategy to use when none specified
195
+ def configure_auth_strategies(strategies, default_strategy: 'publicly')
196
+ # Merge new strategies with existing ones, preserving shared state
197
+ @auth_config[:auth_strategies].merge!(strategies)
198
+ @auth_config[:default_auth_strategy] = default_strategy
199
+ enable_authentication! unless strategies.empty?
200
+ end
201
+
202
+ # Configure rate limiting settings.
203
+ #
204
+ # @param config [Hash] Rate limiting configuration
205
+ # @option config [Integer] :requests_per_minute Maximum requests per minute per IP
206
+ # @option config [Hash] :custom_rules Hash of custom rate limiting rules
207
+ # @option config [Object] :cache_store Custom cache store for rate limiting
208
+ def configure_rate_limiting(config)
209
+ @security_config.rate_limiting_config.merge!(config)
210
+ end
211
+
212
+ private
213
+
214
+ def middleware_enabled?(middleware_class)
215
+ @middleware_stack.includes?(middleware_class)
216
+ end
217
+ end
218
+ end
219
+ end