otto 1.4.0 → 1.6.0

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.
@@ -0,0 +1,412 @@
1
+ # lib/otto/route_handlers.rb
2
+
3
+ class Otto
4
+ # Pluggable Route Handler Factory (Phase 4)
5
+ # Enables different execution patterns while maintaining backward compatibility
6
+ module RouteHandlers
7
+ # Factory for creating appropriate handlers based on route definitions
8
+ class HandlerFactory
9
+ # Create a handler for the given route definition
10
+ # @param route_definition [Otto::RouteDefinition] The route definition
11
+ # @param otto_instance [Otto] The Otto instance for configuration access
12
+ # @return [BaseHandler] Appropriate handler for the route
13
+ def self.create_handler(route_definition, otto_instance = nil)
14
+ case route_definition.kind
15
+ when :logic
16
+ LogicClassHandler.new(route_definition, otto_instance)
17
+ when :instance
18
+ InstanceMethodHandler.new(route_definition, otto_instance)
19
+ when :class
20
+ ClassMethodHandler.new(route_definition, otto_instance)
21
+ else
22
+ raise ArgumentError, "Unknown handler kind: #{route_definition.kind}"
23
+ end
24
+ end
25
+ end
26
+
27
+ # Base class for all route handlers
28
+ # Provides common functionality and interface
29
+ class BaseHandler
30
+ attr_reader :route_definition, :otto_instance
31
+
32
+ def initialize(route_definition, otto_instance = nil)
33
+ @route_definition = route_definition
34
+ @otto_instance = otto_instance
35
+ end
36
+
37
+ # Execute the route handler
38
+ # @param env [Hash] Rack environment
39
+ # @param extra_params [Hash] Additional parameters
40
+ # @return [Array] Rack response array
41
+ def call(env, extra_params = {})
42
+ raise NotImplementedError, 'Subclasses must implement #call'
43
+ end
44
+
45
+ protected
46
+
47
+ # Get the target class, loading it safely
48
+ # @return [Class] The target class
49
+ def target_class
50
+ @target_class ||= safe_const_get(route_definition.klass_name)
51
+ end
52
+
53
+ # Setup request and response with the same extensions and processing as Route#call
54
+ # @param req [Rack::Request] Request object
55
+ # @param res [Rack::Response] Response object
56
+ # @param env [Hash] Rack environment
57
+ # @param extra_params [Hash] Additional parameters
58
+ def setup_request_response(req, res, env, extra_params)
59
+ # Apply the same extensions as original Route#call
60
+ req.extend Otto::RequestHelpers
61
+ res.extend Otto::ResponseHelpers
62
+ res.request = req
63
+
64
+ # Make security config available to response helpers
65
+ if otto_instance.respond_to?(:security_config) && otto_instance.security_config
66
+ env['otto.security_config'] = otto_instance.security_config
67
+ end
68
+
69
+ # Make route definition and options available to middleware and handlers
70
+ env['otto.route_definition'] = route_definition
71
+ env['otto.route_options'] = route_definition.options
72
+
73
+ # Process parameters through security layer
74
+ req.params.merge! extra_params
75
+ req.params.replace Otto::Static.indifferent_params(req.params)
76
+
77
+ # Add security headers
78
+ if otto_instance.respond_to?(:security_config) && otto_instance.security_config
79
+ otto_instance.security_config.security_headers.each do |header, value|
80
+ res.headers[header] = value
81
+ end
82
+ end
83
+
84
+ # Setup class extensions if target_class is available
85
+ if target_class
86
+ target_class.extend Otto::Route::ClassMethods
87
+ target_class.otto = otto_instance if otto_instance
88
+ end
89
+
90
+ # Add security helpers if CSRF is enabled
91
+ if otto_instance.respond_to?(:security_config) && otto_instance.security_config&.csrf_enabled?
92
+ res.extend Otto::Security::CSRFHelpers
93
+ end
94
+
95
+ # Add validation helpers
96
+ res.extend Otto::Security::ValidationHelpers
97
+ end
98
+
99
+ # Finalize response with the same processing as Route#call
100
+ # @param res [Rack::Response] Response object
101
+ # @return [Array] Rack response array
102
+ def finalize_response(res)
103
+ res.body = [res.body] unless res.body.respond_to?(:each)
104
+ res.finish
105
+ end
106
+
107
+ # Handle response using appropriate response handler
108
+ # @param result [Object] Result from route execution
109
+ # @param response [Rack::Response] Response object
110
+ # @param context [Hash] Additional context for response handling
111
+ def handle_response(result, response, context = {})
112
+ response_type = route_definition.response_type
113
+
114
+ # Get the appropriate response handler
115
+ handler_class = case response_type
116
+ when 'json' then Otto::ResponseHandlers::JSONHandler
117
+ when 'redirect' then Otto::ResponseHandlers::RedirectHandler
118
+ when 'view' then Otto::ResponseHandlers::ViewHandler
119
+ when 'auto' then Otto::ResponseHandlers::AutoHandler
120
+ else Otto::ResponseHandlers::DefaultHandler
121
+ end
122
+
123
+ handler_class.handle(result, response, context)
124
+ end
125
+
126
+ private
127
+
128
+ # Safely get a constant from a string name
129
+ # @param name [String] Class name
130
+ # @return [Class] The class
131
+ def safe_const_get(name)
132
+ name.split('::').inject(Object) do |scope, const_name|
133
+ scope.const_get(const_name)
134
+ end
135
+ rescue NameError => ex
136
+ raise NameError, "Unknown class: #{name} (#{ex})"
137
+ end
138
+ end
139
+
140
+ # Handler for Logic classes (new in Otto Framework Enhancement)
141
+ # Supports the OneTime Secret Logic class pattern
142
+ class LogicClassHandler < BaseHandler
143
+ def call(env, extra_params = {})
144
+ req = Rack::Request.new(env)
145
+ res = Rack::Response.new
146
+
147
+ begin
148
+ # Get authentication context if available
149
+ auth_result = env['otto.auth_result']
150
+
151
+ # Initialize Logic class with standard parameters
152
+ # Logic classes expect: session, user, params, locale
153
+ logic_params = req.params.merge(extra_params)
154
+ locale = env['otto.locale'] || 'en'
155
+
156
+ logic = if target_class.instance_method(:initialize).arity == 4
157
+ # Standard Logic class constructor
158
+ target_class.new(
159
+ auth_result&.session,
160
+ auth_result&.user,
161
+ logic_params,
162
+ locale,
163
+ )
164
+ else
165
+ # Fallback for custom constructors
166
+ target_class.new(req, res)
167
+ end
168
+
169
+ # Execute standard Logic class lifecycle
170
+ if logic.respond_to?(:raise_concerns)
171
+ logic.raise_concerns
172
+ end
173
+
174
+ result = if logic.respond_to?(:process)
175
+ logic.process
176
+ else
177
+ logic.call || logic
178
+ end
179
+
180
+ # Handle response with Logic instance context
181
+ handle_response(result, res, {
182
+ logic_instance: logic,
183
+ request: req,
184
+ status_code: logic.respond_to?(:status_code) ? logic.status_code : nil,
185
+ }
186
+ )
187
+ rescue StandardError => ex
188
+ # Check if we're being called through Otto's integrated context (vs direct handler testing)
189
+ # In integrated context, let Otto's centralized error handler manage the response
190
+ # In direct testing context, handle errors locally for unit testing
191
+ if otto_instance
192
+ # Log error for handler-specific context but let Otto's centralized error handler manage the response
193
+ Otto.logger.error "[LogicClassHandler] #{ex.class}: #{ex.message}"
194
+ Otto.logger.debug "[LogicClassHandler] Backtrace: #{ex.backtrace.join("\n")}" if Otto.debug
195
+ raise ex # Re-raise to let Otto's centralized error handler manage the response
196
+ else
197
+ # Direct handler testing context - handle errors locally with security improvements
198
+ error_id = SecureRandom.hex(8)
199
+ Otto.logger.error "[#{error_id}] #{ex.class}: #{ex.message}"
200
+ Otto.logger.debug "[#{error_id}] Backtrace: #{ex.backtrace.join("\n")}" if Otto.debug
201
+
202
+ res.status = 500
203
+ res.headers['content-type'] = 'text/plain'
204
+
205
+ if Otto.env?(:dev, :development)
206
+ res.write "Server error (ID: #{error_id}). Check logs for details."
207
+ else
208
+ res.write 'An error occurred. Please try again later.'
209
+ end
210
+
211
+ # Add security headers if available
212
+ if otto_instance.respond_to?(:security_config) && otto_instance.security_config
213
+ otto_instance.security_config.security_headers.each do |header, value|
214
+ res.headers[header] = value
215
+ end
216
+ end
217
+ end
218
+ end
219
+
220
+ res.finish
221
+ end
222
+ end
223
+
224
+ # Handler for instance methods (existing Otto pattern)
225
+ # Maintains backward compatibility for Controller#action patterns
226
+ class InstanceMethodHandler < BaseHandler
227
+ def call(env, extra_params = {})
228
+ req = Rack::Request.new(env)
229
+ res = Rack::Response.new
230
+
231
+ begin
232
+ # Apply the same extensions and processing as original Route#call
233
+ setup_request_response(req, res, env, extra_params)
234
+
235
+ # Create instance and call method (existing Otto behavior)
236
+ instance = target_class.new(req, res)
237
+ result = instance.send(route_definition.method_name)
238
+
239
+ # Only handle response if response_type is not default
240
+ if route_definition.response_type != 'default'
241
+ handle_response(result, res, {
242
+ instance: instance,
243
+ request: req,
244
+ }
245
+ )
246
+ end
247
+ rescue StandardError => ex
248
+ # Check if we're being called through Otto's integrated context (vs direct handler testing)
249
+ # In integrated context, let Otto's centralized error handler manage the response
250
+ # In direct testing context, handle errors locally for unit testing
251
+ if otto_instance
252
+ # Log error for handler-specific context but let Otto's centralized error handler manage the response
253
+ Otto.logger.error "[InstanceMethodHandler] #{ex.class}: #{ex.message}"
254
+ Otto.logger.debug "[InstanceMethodHandler] Backtrace: #{ex.backtrace.join("\n")}" if Otto.debug
255
+ raise ex # Re-raise to let Otto's centralized error handler manage the response
256
+ else
257
+ # Direct handler testing context - handle errors locally with security improvements
258
+ error_id = SecureRandom.hex(8)
259
+ Otto.logger.error "[#{error_id}] #{ex.class}: #{ex.message}"
260
+ Otto.logger.debug "[#{error_id}] Backtrace: #{ex.backtrace.join("\n")}" if Otto.debug
261
+
262
+ res.status = 500
263
+ res.headers['content-type'] = 'text/plain'
264
+
265
+ if Otto.env?(:dev, :development)
266
+ res.write "Server error (ID: #{error_id}). Check logs for details."
267
+ else
268
+ res.write 'An error occurred. Please try again later.'
269
+ end
270
+
271
+ # Add security headers if available
272
+ if otto_instance.respond_to?(:security_config) && otto_instance.security_config
273
+ otto_instance.security_config.security_headers.each do |header, value|
274
+ res.headers[header] = value
275
+ end
276
+ end
277
+ end
278
+ end
279
+
280
+ finalize_response(res)
281
+ end
282
+ end
283
+
284
+ # Handler for class methods (existing Otto pattern)
285
+ # Maintains backward compatibility for Controller.action patterns
286
+ class ClassMethodHandler < BaseHandler
287
+ def call(env, extra_params = {})
288
+ req = Rack::Request.new(env)
289
+ res = Rack::Response.new
290
+
291
+ begin
292
+ # Apply the same extensions and processing as original Route#call
293
+ setup_request_response(req, res, env, extra_params)
294
+
295
+ # Call class method directly (existing Otto behavior)
296
+ result = target_class.send(route_definition.method_name, req, res)
297
+
298
+ # Only handle response if response_type is not default
299
+ if route_definition.response_type != 'default'
300
+ handle_response(result, res, {
301
+ class: target_class,
302
+ request: req,
303
+ }
304
+ )
305
+ end
306
+ rescue StandardError => ex
307
+ # Check if we're being called through Otto's integrated context (vs direct handler testing)
308
+ # In integrated context, let Otto's centralized error handler manage the response
309
+ # In direct testing context, handle errors locally for unit testing
310
+ if otto_instance
311
+ # Log error for handler-specific context but let Otto's centralized error handler manage the response
312
+ Otto.logger.error "[ClassMethodHandler] #{ex.class}: #{ex.message}"
313
+ Otto.logger.debug "[ClassMethodHandler] Backtrace: #{ex.backtrace.join("\n")}" if Otto.debug
314
+ raise ex # Re-raise to let Otto's centralized error handler manage the response
315
+ else
316
+ # Direct handler testing context - handle errors locally with security improvements
317
+ error_id = SecureRandom.hex(8)
318
+ Otto.logger.error "[#{error_id}] #{ex.class}: #{ex.message}"
319
+ Otto.logger.debug "[#{error_id}] Backtrace: #{ex.backtrace.join("\n")}" if Otto.debug
320
+
321
+ res.status = 500
322
+
323
+ # Content negotiation for error response
324
+ accept_header = env['HTTP_ACCEPT'].to_s
325
+ if accept_header.include?('application/json')
326
+ res.headers['content-type'] = 'application/json'
327
+ error_data = if Otto.env?(:dev, :development)
328
+ {
329
+ error: 'Internal Server Error',
330
+ message: 'Server error occurred. Check logs for details.',
331
+ error_id: error_id,
332
+ }
333
+ else
334
+ {
335
+ error: 'Internal Server Error',
336
+ message: 'An error occurred. Please try again later.',
337
+ }
338
+ end
339
+ res.write JSON.generate(error_data)
340
+ else
341
+ res.headers['content-type'] = 'text/plain'
342
+ if Otto.env?(:dev, :development)
343
+ res.write "Server error (ID: #{error_id}). Check logs for details."
344
+ else
345
+ res.write 'An error occurred. Please try again later.'
346
+ end
347
+ end
348
+
349
+ # Add security headers if available
350
+ if otto_instance.respond_to?(:security_config) && otto_instance.security_config
351
+ otto_instance.security_config.security_headers.each do |header, value|
352
+ res.headers[header] = value
353
+ end
354
+ end
355
+ end
356
+ end
357
+
358
+ finalize_response(res)
359
+ end
360
+ end
361
+
362
+ # Custom handler for lambda/proc definitions (future extension)
363
+ class LambdaHandler < BaseHandler
364
+ def call(env, extra_params = {})
365
+ req = Rack::Request.new(env)
366
+ res = Rack::Response.new
367
+
368
+ begin
369
+ # Security: Lambda handlers require pre-configured procs from Otto instance
370
+ # This prevents code injection via eval and maintains security
371
+ handler_name = route_definition.klass_name
372
+ lambda_registry = otto_instance&.config&.dig(:lambda_handlers) || {}
373
+
374
+ lambda_proc = lambda_registry[handler_name]
375
+ unless lambda_proc.respond_to?(:call)
376
+ raise ArgumentError, "Lambda handler '#{handler_name}' not found in registry or not callable"
377
+ end
378
+
379
+ result = lambda_proc.call(req, res, extra_params)
380
+
381
+ handle_response(result, res, {
382
+ lambda: lambda_proc,
383
+ request: req,
384
+ }
385
+ )
386
+ rescue StandardError => ex
387
+ error_id = SecureRandom.hex(8)
388
+ Otto.logger.error "[#{error_id}] #{ex.class}: #{ex.message}"
389
+ Otto.logger.debug "[#{error_id}] Backtrace: #{ex.backtrace.join("\n")}" if Otto.debug
390
+
391
+ res.status = 500
392
+ res.headers['content-type'] = 'text/plain'
393
+
394
+ if Otto.env?(:dev, :development)
395
+ res.write "Lambda handler error (ID: #{error_id}). Check logs for details."
396
+ else
397
+ res.write 'An error occurred. Please try again later.'
398
+ end
399
+
400
+ # Add security headers if available
401
+ if otto_instance.respond_to?(:security_config) && otto_instance.security_config
402
+ otto_instance.security_config.security_headers.each do |header, value|
403
+ res.headers[header] = value
404
+ end
405
+ end
406
+ end
407
+
408
+ res.finish
409
+ end
410
+ end
411
+ end
412
+ end
@@ -0,0 +1,289 @@
1
+ # lib/otto/security/authentication.rb
2
+ #
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('-', '_')}"]
153
+
154
+ if api_key.nil?
155
+ request = Rack::Request.new(env)
156
+ api_key = request.params[@param_name]
157
+ end
158
+
159
+ return failure('No API key provided') unless api_key
160
+
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
288
+ end
289
+ end