otto 2.0.0.pre3 → 2.0.0.pre8

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 (104) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +1 -1
  3. data/.github/workflows/claude-code-review.yml +1 -1
  4. data/.github/workflows/code-smells.yml +143 -0
  5. data/.gitignore +4 -0
  6. data/.pre-commit-config.yaml +2 -2
  7. data/.reek.yml +99 -0
  8. data/CHANGELOG.rst +156 -0
  9. data/CLAUDE.md +74 -540
  10. data/Gemfile +4 -2
  11. data/Gemfile.lock +58 -19
  12. data/README.md +49 -1
  13. data/examples/advanced_routes/README.md +137 -20
  14. data/examples/authentication_strategies/README.md +212 -19
  15. data/examples/backtrace_sanitization_demo.rb +86 -0
  16. data/examples/basic/README.md +61 -10
  17. data/examples/error_handler_registration.rb +136 -0
  18. data/examples/logging_improvements.rb +76 -0
  19. data/examples/mcp_demo/README.md +187 -27
  20. data/examples/security_features/README.md +249 -30
  21. data/examples/simple_geo_resolver.rb +107 -0
  22. data/lib/otto/core/configuration.rb +15 -20
  23. data/lib/otto/core/error_handler.rb +138 -8
  24. data/lib/otto/core/file_safety.rb +2 -2
  25. data/lib/otto/core/freezable.rb +2 -2
  26. data/lib/otto/core/middleware_stack.rb +2 -2
  27. data/lib/otto/core/router.rb +61 -8
  28. data/lib/otto/core/uri_generator.rb +2 -2
  29. data/lib/otto/core.rb +2 -0
  30. data/lib/otto/design_system.rb +2 -2
  31. data/lib/otto/env_keys.rb +61 -12
  32. data/lib/otto/helpers/base.rb +2 -2
  33. data/lib/otto/helpers/request.rb +8 -3
  34. data/lib/otto/helpers/response.rb +2 -2
  35. data/lib/otto/helpers/validation.rb +2 -2
  36. data/lib/otto/helpers.rb +2 -0
  37. data/lib/otto/locale/config.rb +2 -2
  38. data/lib/otto/locale/middleware.rb +160 -0
  39. data/lib/otto/locale.rb +10 -0
  40. data/lib/otto/logging_helpers.rb +273 -0
  41. data/lib/otto/mcp/auth/token.rb +2 -2
  42. data/lib/otto/mcp/protocol.rb +2 -2
  43. data/lib/otto/mcp/rate_limiting.rb +2 -2
  44. data/lib/otto/mcp/registry.rb +2 -2
  45. data/lib/otto/mcp/route_parser.rb +2 -2
  46. data/lib/otto/mcp/schema_validation.rb +2 -2
  47. data/lib/otto/mcp/server.rb +2 -2
  48. data/lib/otto/mcp.rb +2 -0
  49. data/lib/otto/privacy/config.rb +2 -0
  50. data/lib/otto/privacy/geo_resolver.rb +199 -29
  51. data/lib/otto/privacy/ip_privacy.rb +2 -0
  52. data/lib/otto/privacy/redacted_fingerprint.rb +18 -8
  53. data/lib/otto/privacy.rb +2 -0
  54. data/lib/otto/response_handlers/auto.rb +2 -0
  55. data/lib/otto/response_handlers/base.rb +2 -0
  56. data/lib/otto/response_handlers/default.rb +2 -0
  57. data/lib/otto/response_handlers/factory.rb +2 -0
  58. data/lib/otto/response_handlers/json.rb +2 -0
  59. data/lib/otto/response_handlers/redirect.rb +2 -0
  60. data/lib/otto/response_handlers/view.rb +2 -0
  61. data/lib/otto/response_handlers.rb +2 -2
  62. data/lib/otto/route.rb +4 -4
  63. data/lib/otto/route_definition.rb +42 -15
  64. data/lib/otto/route_handlers/base.rb +2 -0
  65. data/lib/otto/route_handlers/class_method.rb +26 -26
  66. data/lib/otto/route_handlers/factory.rb +2 -2
  67. data/lib/otto/route_handlers/instance_method.rb +16 -6
  68. data/lib/otto/route_handlers/lambda.rb +8 -20
  69. data/lib/otto/route_handlers/logic_class.rb +33 -8
  70. data/lib/otto/route_handlers.rb +2 -2
  71. data/lib/otto/security/authentication/auth_failure.rb +2 -2
  72. data/lib/otto/security/authentication/auth_strategy.rb +11 -4
  73. data/lib/otto/security/authentication/route_auth_wrapper/response_builder.rb +123 -0
  74. data/lib/otto/security/authentication/route_auth_wrapper/role_authorization.rb +120 -0
  75. data/lib/otto/security/authentication/route_auth_wrapper/strategy_resolver.rb +69 -0
  76. data/lib/otto/security/authentication/route_auth_wrapper.rb +185 -195
  77. data/lib/otto/security/authentication/strategies/api_key_strategy.rb +2 -0
  78. data/lib/otto/security/authentication/strategies/noauth_strategy.rb +2 -0
  79. data/lib/otto/security/authentication/strategies/permission_strategy.rb +2 -0
  80. data/lib/otto/security/authentication/strategies/role_strategy.rb +2 -0
  81. data/lib/otto/security/authentication/strategies/session_strategy.rb +2 -0
  82. data/lib/otto/security/authentication/strategy_result.rb +6 -5
  83. data/lib/otto/security/authentication.rb +2 -2
  84. data/lib/otto/security/authorization_error.rb +73 -0
  85. data/lib/otto/security/config.rb +2 -2
  86. data/lib/otto/security/configurator.rb +17 -2
  87. data/lib/otto/security/csrf.rb +2 -2
  88. data/lib/otto/security/middleware/csrf_middleware.rb +11 -1
  89. data/lib/otto/security/middleware/ip_privacy_middleware.rb +31 -11
  90. data/lib/otto/security/middleware/rate_limit_middleware.rb +2 -0
  91. data/lib/otto/security/middleware/validation_middleware.rb +15 -0
  92. data/lib/otto/security/rate_limiter.rb +2 -2
  93. data/lib/otto/security/rate_limiting.rb +2 -2
  94. data/lib/otto/security/validator.rb +2 -2
  95. data/lib/otto/security.rb +3 -0
  96. data/lib/otto/static.rb +2 -2
  97. data/lib/otto/utils.rb +27 -2
  98. data/lib/otto/version.rb +3 -3
  99. data/lib/otto.rb +174 -14
  100. data/otto.gemspec +7 -3
  101. metadata +25 -15
  102. data/benchmark_middleware_wrap.rb +0 -163
  103. data/changelog.d/20251014_144317_delano_54_thats_a_wrapper.rst +0 -36
  104. data/changelog.d/20251014_161526_delano_54_thats_a_wrapper.rst +0 -5
@@ -1,6 +1,6 @@
1
- # frozen_string_literal: true
2
-
3
1
  # lib/otto/route_handlers/lambda.rb
2
+ #
3
+ # frozen_string_literal: true
4
4
  require 'securerandom'
5
5
 
6
6
  require_relative 'base'
@@ -10,6 +10,7 @@ class Otto
10
10
  # Custom handler for lambda/proc definitions (future extension)
11
11
  class LambdaHandler < BaseHandler
12
12
  def call(env, extra_params = {})
13
+ start_time = Otto::Utils.now_in_μs
13
14
  req = Rack::Request.new(env)
14
15
  res = Rack::Response.new
15
16
 
@@ -31,25 +32,12 @@ class Otto
31
32
  request: req,
32
33
  })
33
34
  rescue StandardError => e
34
- error_id = SecureRandom.hex(8)
35
- Otto.logger.error "[#{error_id}] #{e.class}: #{e.message}"
36
- Otto.logger.debug "[#{error_id}] Backtrace: #{e.backtrace.join("\n")}" if Otto.debug
37
-
38
- res.status = 500
39
- res.headers['content-type'] = 'text/plain'
35
+ # Store handler context in env for centralized error handler
36
+ handler_name = "Lambda[#{route_definition.klass_name}]"
37
+ env['otto.handler'] = handler_name
38
+ env['otto.handler_duration'] = Otto::Utils.now_in_μs - start_time
40
39
 
41
- if Otto.env?(:dev, :development)
42
- res.write "Lambda handler error (ID: #{error_id}). Check logs for details."
43
- else
44
- res.write 'An error occurred. Please try again later.'
45
- end
46
-
47
- # Add security headers if available
48
- if otto_instance.respond_to?(:security_config) && otto_instance.security_config
49
- otto_instance.security_config.security_headers.each do |header, value|
50
- res.headers[header] = value
51
- end
52
- end
40
+ raise e # Re-raise to let Otto's centralized error handler manage the response
53
41
  end
54
42
 
55
43
  res.finish
@@ -1,6 +1,6 @@
1
- # frozen_string_literal: true
2
-
3
1
  # lib/otto/route_handlers/logic_class.rb
2
+ #
3
+ # frozen_string_literal: true
4
4
  require 'json'
5
5
  require 'securerandom'
6
6
 
@@ -9,10 +9,19 @@ require_relative 'base'
9
9
  class Otto
10
10
  module RouteHandlers
11
11
  # Handler for Logic classes (new in Otto Framework Enhancement)
12
- # Handles Logic class routes with the modern RequestContext pattern
13
- # Logic classes use signature: initialize(context, params, locale)
12
+ #
13
+ # Logic classes use a constrained signature: initialize(context, params, locale)
14
+ # - context: The authentication strategy result (user info, session data)
15
+ # - params: Merged request parameters (URL params + body + extra_params)
16
+ # - locale: The locale string from env['otto.locale']
17
+ #
18
+ # IMPORTANT: Logic classes do NOT receive the Rack request or env hash.
19
+ # This is intentional - Logic classes work with clean, authenticated contexts.
20
+ # For endpoints requiring direct request access (sessions, cookies, headers,
21
+ # or logout flows), use controller handlers (Controller#action or Controller.action).
14
22
  class LogicClassHandler < BaseHandler
15
23
  def call(env, extra_params = {})
24
+ start_time = Otto::Utils.now_in_μs
16
25
  req = Rack::Request.new(env)
17
26
  res = Rack::Response.new
18
27
 
@@ -30,7 +39,21 @@ class Otto
30
39
  json_data = JSON.parse(req.body.read)
31
40
  logic_params = logic_params.merge(json_data) if json_data.is_a?(Hash)
32
41
  rescue JSON::ParserError => e
33
- Otto.logger.error "[LogicClassHandler] JSON parsing error: #{e.message}"
42
+ # Base context pattern: create once, reuse for correlation
43
+ base_context = Otto::LoggingHelpers.request_context(env)
44
+
45
+ Otto.structured_log(:error, "JSON parsing error",
46
+ base_context.merge(
47
+ handler: "#{target_class}#call",
48
+ error: e.message,
49
+ error_class: e.class.name,
50
+ duration: Otto::Utils.now_in_μs - start_time
51
+ )
52
+ )
53
+
54
+ Otto::LoggingHelpers.log_backtrace(e,
55
+ base_context.merge(handler: "#{target_class}#call")
56
+ )
34
57
  end
35
58
  end
36
59
 
@@ -58,9 +81,11 @@ class Otto
58
81
  # In integrated context, let Otto's centralized error handler manage the response
59
82
  # In direct testing context, handle errors locally for unit testing
60
83
  if otto_instance
61
- # Log error for handler-specific context but let Otto's centralized error handler manage the response
62
- Otto.logger.error "[LogicClassHandler] #{e.class}: #{e.message}"
63
- Otto.logger.debug "[LogicClassHandler] Backtrace: #{e.backtrace.join("\n")}" if Otto.debug
84
+ # Store handler context in env for centralized error handler
85
+ handler_name = "#{target_class}#call"
86
+ env['otto.handler'] = handler_name
87
+ env['otto.handler_duration'] = Otto::Utils.now_in_μs - start_time
88
+
64
89
  raise e # Re-raise to let Otto's centralized error handler manage the response
65
90
  else
66
91
  # Direct handler testing context - handle errors locally with security improvements
@@ -1,6 +1,6 @@
1
- # frozen_string_literal: true
2
-
3
1
  # lib/otto/route_handlers.rb
2
+ #
3
+ # frozen_string_literal: true
4
4
 
5
5
  class Otto
6
6
  # Pluggable Route Handler Factory
@@ -1,7 +1,7 @@
1
+ # lib/otto/security/authentication/auth_failure.rb
2
+ #
1
3
  # frozen_string_literal: true
2
4
 
3
- # lib/otto/security/authentication/failure_result.rb
4
-
5
5
  class Otto
6
6
  module Security
7
7
  module Authentication
@@ -1,7 +1,7 @@
1
- # frozen_string_literal: true
2
-
3
1
  # lib/otto/security/authentication/auth_strategy.rb
4
2
  #
3
+ # frozen_string_literal: true
4
+ #
5
5
  # Base class for all authentication strategies in Otto framework
6
6
  # Provides pluggable authentication patterns that can be customized per application
7
7
 
@@ -13,7 +13,9 @@ class Otto
13
13
  # Check if the request meets the authentication requirements
14
14
  # @param env [Hash] Rack environment
15
15
  # @param requirement [String] Authentication requirement string
16
- # @return [Otto::Security::Authentication::StrategyResult, Otto::Security::Authentication::AuthFailure] StrategyResult for success, AuthFailure for failure
16
+ # @return [Otto::Security::Authentication::StrategyResult,
17
+ # Otto::Security::Authentication::AuthFailure]
18
+ # StrategyResult for success, AuthFailure for failure
17
19
  def authenticate(env, requirement)
18
20
  raise NotImplementedError, 'Subclasses must implement #authenticate'
19
21
  end
@@ -21,12 +23,17 @@ class Otto
21
23
  protected
22
24
 
23
25
  # Helper to create successful strategy result
26
+ #
27
+ # NOTE: strategy_name will be injected by RouteAuthWrapper after strategy execution.
28
+ # Strategies don't know their registered name, so we pass nil here and let the wrapper
29
+ # set it based on how the strategy was registered via add_auth_strategy(name, strategy).
24
30
  def success(user:, session: {}, auth_method: nil, **metadata)
25
31
  Otto::Security::Authentication::StrategyResult.new(
26
32
  session: session,
27
33
  user: user,
28
34
  auth_method: auth_method || self.class.name.split('::').last,
29
- metadata: metadata
35
+ metadata: metadata,
36
+ strategy_name: nil # Will be set by RouteAuthWrapper
30
37
  )
31
38
  end
32
39
 
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Otto
4
+ module Security
5
+ module Authentication
6
+ module RouteAuthWrapperComponents
7
+ # Builds HTTP error responses for authentication/authorization failures
8
+ #
9
+ # Handles content negotiation (JSON vs HTML) and applies security headers.
10
+ # Route's declared response_type takes precedence over Accept header.
11
+ #
12
+ # @example
13
+ # builder = ResponseBuilder.new(route_definition, auth_config, security_config)
14
+ # response = builder.unauthorized(env, "Invalid token")
15
+ # response = builder.forbidden(env, "Admin role required")
16
+ # response = builder.auth_failure(env, auth_failure_result)
17
+ #
18
+ class ResponseBuilder
19
+ # @param route_definition [RouteDefinition] Route with response_type info
20
+ # @param auth_config [Hash] Auth config with :login_path for HTML redirects
21
+ # @param security_config [SecurityConfig, nil] Optional security config for headers
22
+ def initialize(route_definition, auth_config, security_config = nil)
23
+ @route_definition = route_definition
24
+ @auth_config = auth_config
25
+ @security_config = security_config
26
+ end
27
+
28
+ # Generate response for authentication failure
29
+ #
30
+ # @param env [Hash] Rack environment
31
+ # @param result [AuthFailure] Failure result from strategy
32
+ # @return [Array] Rack response array
33
+ def auth_failure(env, result)
34
+ wants_json?(env) ? json_auth_error(result) : html_auth_error(result)
35
+ end
36
+
37
+ # Generate 401 Unauthorized response
38
+ #
39
+ # @param env [Hash] Rack environment
40
+ # @param message [String] Error message
41
+ # @return [Array] Rack response array
42
+ def unauthorized(env, message)
43
+ if wants_json?(env)
44
+ json_response(401, error: message)
45
+ else
46
+ text_response(401, message)
47
+ end
48
+ end
49
+
50
+ # Generate 403 Forbidden response
51
+ #
52
+ # @param env [Hash] Rack environment
53
+ # @param message [String] Error message
54
+ # @return [Array] Rack response array
55
+ def forbidden(env, message)
56
+ if wants_json?(env)
57
+ json_response(403, error: 'Forbidden', message: message)
58
+ else
59
+ text_response(403, message)
60
+ end
61
+ end
62
+
63
+ private
64
+
65
+ # Determine if response should be JSON based on route config and Accept header
66
+ #
67
+ # Route's declared response type takes precedence over Accept header.
68
+ # This ensures API routes (response=json) always get JSON errors.
69
+ #
70
+ # @param env [Hash] Rack environment
71
+ # @return [Boolean] true if response should be JSON
72
+ def wants_json?(env)
73
+ return true if @route_definition.response_type == 'json'
74
+
75
+ accept_header = env['HTTP_ACCEPT'] || ''
76
+ accept_header.include?('application/json')
77
+ end
78
+
79
+ # Generate JSON 401 response for auth failure
80
+ def json_auth_error(result)
81
+ json_response(401,
82
+ error: 'Authentication Required',
83
+ message: result.failure_reason || 'Not authenticated',
84
+ timestamp: Time.now.to_i)
85
+ end
86
+
87
+ # Generate HTML 401 response (redirect to login)
88
+ def html_auth_error(_result)
89
+ login_path = @auth_config[:login_path] || '/signin'
90
+ headers = { 'location' => login_path }
91
+ merge_security_headers!(headers)
92
+ [302, headers, ["Redirecting to #{login_path}"]]
93
+ end
94
+
95
+ # Build a JSON response with security headers
96
+ def json_response(status, body_hash)
97
+ body = body_hash.to_json
98
+ headers = {
99
+ 'content-type' => 'application/json',
100
+ 'content-length' => body.bytesize.to_s,
101
+ }
102
+ merge_security_headers!(headers)
103
+ [status, headers, [body]]
104
+ end
105
+
106
+ # Build a plain text response with security headers
107
+ def text_response(status, message)
108
+ headers = { 'content-type' => 'text/plain' }
109
+ merge_security_headers!(headers)
110
+ [status, headers, [message]]
111
+ end
112
+
113
+ # Merge security headers into response headers
114
+ def merge_security_headers!(headers)
115
+ return unless @security_config
116
+
117
+ headers.merge!(@security_config.security_headers)
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Otto
4
+ module Security
5
+ module Authentication
6
+ module RouteAuthWrapperComponents
7
+ # Handles Layer 1 (route-level) role-based authorization
8
+ #
9
+ # Extracts user roles from authentication results and checks against
10
+ # route requirements using OR logic (user needs ANY of the required roles).
11
+ #
12
+ # @example
13
+ # authorizer = RoleAuthorization.new(route_definition)
14
+ # authorizer.check!(strategy_result, env) # raises or returns true
15
+ #
16
+ # @note This is Layer 1 authorization only. Layer 2 (resource-level)
17
+ # authorization should be handled in Logic classes via raise_concerns.
18
+ #
19
+ class RoleAuthorization
20
+ # @param route_definition [RouteDefinition] Route with role requirements
21
+ def initialize(route_definition)
22
+ @route_definition = route_definition
23
+ end
24
+
25
+ # Check if authentication result satisfies role requirements
26
+ #
27
+ # @param result [StrategyResult] Authentication result
28
+ # @param env [Hash] Rack environment (for logging)
29
+ # @return [true] if authorized
30
+ # @return [Hash] failure info if not authorized: { authorized: false, required: [...], actual: [...] }
31
+ def check(result, env)
32
+ role_requirements = @route_definition.role_requirements
33
+ return true if role_requirements.empty?
34
+
35
+ user_roles = extract_roles(result)
36
+
37
+ # OR logic: user needs ANY of the required roles
38
+ if (user_roles & role_requirements).any?
39
+ log_success(env, role_requirements, user_roles)
40
+ true
41
+ else
42
+ log_failure(env, role_requirements, user_roles, result)
43
+ {
44
+ authorized: false,
45
+ required: role_requirements,
46
+ actual: user_roles,
47
+ }
48
+ end
49
+ end
50
+
51
+ # Check authorization, returning boolean
52
+ #
53
+ # @param result [StrategyResult] Authentication result
54
+ # @return [Boolean] true if authorized
55
+ def authorized?(result)
56
+ role_requirements = @route_definition.role_requirements
57
+ return true if role_requirements.empty?
58
+
59
+ user_roles = extract_roles(result)
60
+ (user_roles & role_requirements).any?
61
+ end
62
+
63
+ # Get the role requirements for error messages
64
+ #
65
+ # @return [Array<String>] Required roles
66
+ def requirements
67
+ @route_definition.role_requirements
68
+ end
69
+
70
+ private
71
+
72
+ # Extract user roles from authentication result
73
+ #
74
+ # Supports multiple role sources in order of precedence:
75
+ # 1. result.user_roles (Array)
76
+ # 2. result.user[:roles] (Array)
77
+ # 3. result.user['roles'] (Array)
78
+ # 4. result.metadata[:user_roles] (Array)
79
+ #
80
+ # @param result [StrategyResult] Authentication result
81
+ # @return [Array<String>] Array of role strings
82
+ def extract_roles(result)
83
+ # Try direct user_roles accessor (e.g., from RoleStrategy)
84
+ return Array(result.user_roles) if result.respond_to?(:user_roles) && result.user_roles
85
+
86
+ # Try user hash/object with roles
87
+ if result.user
88
+ roles = result.user[:roles] || result.user['roles']
89
+ return Array(roles) if roles
90
+ end
91
+
92
+ # Try metadata
93
+ return Array(result.metadata[:user_roles]) if result.metadata && result.metadata[:user_roles]
94
+
95
+ # No roles found
96
+ []
97
+ end
98
+
99
+ def log_success(env, required_roles, user_roles)
100
+ Otto.structured_log(:debug, 'Role authorization succeeded',
101
+ Otto::LoggingHelpers.request_context(env).merge(
102
+ required_roles: required_roles,
103
+ user_roles: user_roles,
104
+ matched_roles: user_roles & required_roles
105
+ ))
106
+ end
107
+
108
+ def log_failure(env, required_roles, user_roles, result)
109
+ Otto.structured_log(:warn, 'Role authorization failed',
110
+ Otto::LoggingHelpers.request_context(env).merge(
111
+ required_roles: required_roles,
112
+ user_roles: user_roles,
113
+ user_id: result.user_id
114
+ ))
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Otto
4
+ module Security
5
+ module Authentication
6
+ module RouteAuthWrapperComponents
7
+ # Resolves authentication strategy names to strategy instances
8
+ #
9
+ # Handles strategy lookup with caching and pattern matching:
10
+ # - Exact match: 'authenticated' → looks up auth_config[:auth_strategies]['authenticated']
11
+ # - Prefix match: 'custom:value' → looks up 'custom' strategy
12
+ #
13
+ # Results are cached to avoid repeated lookups for the same requirement.
14
+ #
15
+ # @example
16
+ # resolver = StrategyResolver.new(auth_config)
17
+ # strategy, name = resolver.resolve('session')
18
+ # strategy, name = resolver.resolve('oauth:google') # prefix match
19
+ #
20
+ class StrategyResolver
21
+ # @param auth_config [Hash] Auth configuration with :auth_strategies key
22
+ def initialize(auth_config)
23
+ @auth_config = auth_config
24
+ @cache = {}
25
+ end
26
+
27
+ # Resolve a requirement string to a strategy instance
28
+ #
29
+ # @param requirement [String] Auth requirement from route (e.g., 'session', 'oauth:google')
30
+ # @return [Array<AuthStrategy, String>, Array<nil, nil>] Tuple of [strategy, name] or [nil, nil]
31
+ def resolve(requirement)
32
+ return [nil, nil] unless @auth_config && @auth_config[:auth_strategies]
33
+
34
+ # Check cache first
35
+ return @cache[requirement] if @cache.key?(requirement)
36
+
37
+ result = find_strategy(requirement)
38
+ @cache[requirement] = result
39
+ result
40
+ end
41
+
42
+ # Clear the strategy cache
43
+ def clear_cache
44
+ @cache.clear
45
+ end
46
+
47
+ private
48
+
49
+ def find_strategy(requirement)
50
+ strategies = @auth_config[:auth_strategies]
51
+
52
+ # Try exact match first - highest priority
53
+ strategy = strategies[requirement]
54
+ return [strategy, requirement] if strategy
55
+
56
+ # For colon-separated requirements like "custom:value", try prefix match
57
+ if requirement.include?(':')
58
+ prefix = requirement.split(':', 2).first
59
+ prefix_strategy = strategies[prefix]
60
+ return [prefix_strategy, prefix] if prefix_strategy
61
+ end
62
+
63
+ [nil, nil]
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end