otto 2.0.0.pre2 → 2.0.0.pre7
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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +1 -3
- data/.github/workflows/claude-code-review.yml +29 -13
- data/.github/workflows/code-smells.yml +146 -0
- data/.gitignore +4 -0
- data/.pre-commit-config.yaml +2 -2
- data/.reek.yml +99 -0
- data/CHANGELOG.rst +90 -0
- data/CLAUDE.md +116 -45
- data/Gemfile +5 -2
- data/Gemfile.lock +70 -24
- data/README.md +49 -1
- data/changelog.d/20251103_235431_delano_86_improve_error_logging.rst +15 -0
- data/changelog.d/20251109_025012_claude_fix_backtrace_sanitization.rst +37 -0
- data/docs/.gitignore +1 -0
- data/docs/ipaddr-encoding-quirk.md +34 -0
- data/docs/migrating/v2.0.0-pre2.md +11 -18
- data/examples/advanced_routes/README.md +137 -20
- data/examples/authentication_strategies/README.md +212 -19
- data/examples/authentication_strategies/config.ru +0 -1
- data/examples/backtrace_sanitization_demo.rb +86 -0
- data/examples/basic/README.md +61 -10
- data/examples/error_handler_registration.rb +136 -0
- data/examples/logging_improvements.rb +76 -0
- data/examples/mcp_demo/README.md +187 -27
- data/examples/security_features/README.md +249 -30
- data/examples/simple_geo_resolver.rb +107 -0
- data/lib/otto/core/configuration.rb +90 -45
- data/lib/otto/core/error_handler.rb +138 -8
- data/lib/otto/core/file_safety.rb +2 -2
- data/lib/otto/core/freezable.rb +93 -0
- data/lib/otto/core/middleware_stack.rb +25 -18
- data/lib/otto/core/router.rb +62 -9
- data/lib/otto/core/uri_generator.rb +2 -2
- data/lib/otto/core.rb +10 -0
- data/lib/otto/design_system.rb +2 -2
- data/lib/otto/env_keys.rb +65 -12
- data/lib/otto/helpers/base.rb +2 -2
- data/lib/otto/helpers/request.rb +85 -2
- data/lib/otto/helpers/response.rb +5 -5
- data/lib/otto/helpers/validation.rb +2 -2
- data/lib/otto/helpers.rb +6 -0
- data/lib/otto/locale/config.rb +56 -0
- data/lib/otto/locale/middleware.rb +160 -0
- data/lib/otto/locale.rb +10 -0
- data/lib/otto/logging_helpers.rb +273 -0
- data/lib/otto/mcp/auth/token.rb +2 -2
- data/lib/otto/mcp/protocol.rb +2 -2
- data/lib/otto/mcp/rate_limiting.rb +2 -2
- data/lib/otto/mcp/registry.rb +2 -2
- data/lib/otto/mcp/route_parser.rb +2 -2
- data/lib/otto/mcp/schema_validation.rb +2 -2
- data/lib/otto/mcp/server.rb +2 -2
- data/lib/otto/mcp.rb +5 -0
- data/lib/otto/privacy/config.rb +201 -0
- data/lib/otto/privacy/geo_resolver.rb +285 -0
- data/lib/otto/privacy/ip_privacy.rb +177 -0
- data/lib/otto/privacy/redacted_fingerprint.rb +146 -0
- data/lib/otto/privacy.rb +31 -0
- data/lib/otto/response_handlers/auto.rb +2 -0
- data/lib/otto/response_handlers/base.rb +2 -0
- data/lib/otto/response_handlers/default.rb +2 -0
- data/lib/otto/response_handlers/factory.rb +2 -0
- data/lib/otto/response_handlers/json.rb +2 -0
- data/lib/otto/response_handlers/redirect.rb +2 -0
- data/lib/otto/response_handlers/view.rb +2 -0
- data/lib/otto/response_handlers.rb +2 -2
- data/lib/otto/route.rb +4 -4
- data/lib/otto/route_definition.rb +42 -15
- data/lib/otto/route_handlers/base.rb +2 -1
- data/lib/otto/route_handlers/class_method.rb +18 -25
- data/lib/otto/route_handlers/factory.rb +18 -16
- data/lib/otto/route_handlers/instance_method.rb +8 -5
- data/lib/otto/route_handlers/lambda.rb +8 -20
- data/lib/otto/route_handlers/logic_class.rb +25 -8
- data/lib/otto/route_handlers.rb +2 -2
- data/lib/otto/security/authentication/{failure_result.rb → auth_failure.rb} +5 -5
- data/lib/otto/security/authentication/auth_strategy.rb +13 -6
- data/lib/otto/security/authentication/route_auth_wrapper.rb +304 -41
- data/lib/otto/security/authentication/strategies/api_key_strategy.rb +2 -0
- data/lib/otto/security/authentication/strategies/noauth_strategy.rb +7 -1
- data/lib/otto/security/authentication/strategies/permission_strategy.rb +2 -0
- data/lib/otto/security/authentication/strategies/role_strategy.rb +2 -0
- data/lib/otto/security/authentication/strategies/session_strategy.rb +2 -0
- data/lib/otto/security/authentication/strategy_result.rb +6 -5
- data/lib/otto/security/authentication.rb +5 -6
- data/lib/otto/security/authorization_error.rb +73 -0
- data/lib/otto/security/config.rb +53 -9
- data/lib/otto/security/configurator.rb +17 -15
- data/lib/otto/security/csrf.rb +2 -2
- data/lib/otto/security/middleware/csrf_middleware.rb +11 -1
- data/lib/otto/security/middleware/ip_privacy_middleware.rb +231 -0
- data/lib/otto/security/middleware/rate_limit_middleware.rb +2 -0
- data/lib/otto/security/middleware/validation_middleware.rb +15 -0
- data/lib/otto/security/rate_limiter.rb +2 -2
- data/lib/otto/security/rate_limiting.rb +2 -2
- data/lib/otto/security/validator.rb +2 -2
- data/lib/otto/security.rb +12 -0
- data/lib/otto/static.rb +2 -2
- data/lib/otto/utils.rb +27 -2
- data/lib/otto/version.rb +3 -3
- data/lib/otto.rb +344 -89
- data/otto.gemspec +9 -2
- metadata +72 -8
- data/lib/otto/security/authentication/authentication_middleware.rb +0 -140
data/lib/otto/route.rb
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
1
|
# lib/otto/route.rb
|
|
2
|
+
#
|
|
3
|
+
# frozen_string_literal: true
|
|
4
4
|
|
|
5
5
|
class Otto
|
|
6
6
|
# Otto::Route
|
|
@@ -158,8 +158,8 @@ class Otto
|
|
|
158
158
|
if response_type != 'default'
|
|
159
159
|
context = {
|
|
160
160
|
logic_instance: (kind == :instance ? inst : nil),
|
|
161
|
-
|
|
162
|
-
|
|
161
|
+
status_code: nil,
|
|
162
|
+
redirect_path: nil,
|
|
163
163
|
}
|
|
164
164
|
|
|
165
165
|
Otto::ResponseHandlers::HandlerFactory.handle_response(result, res, response_type, context)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
1
|
# lib/otto/route_definition.rb
|
|
2
|
+
#
|
|
3
|
+
# frozen_string_literal: true
|
|
4
4
|
|
|
5
5
|
class Otto
|
|
6
6
|
# Immutable data class representing a complete route definition
|
|
@@ -73,10 +73,37 @@ class Otto
|
|
|
73
73
|
@options.fetch(key.to_sym, default)
|
|
74
74
|
end
|
|
75
75
|
|
|
76
|
-
# Get authentication requirement
|
|
76
|
+
# Get authentication requirement (backward compatibility - returns first requirement)
|
|
77
77
|
# @return [String, nil] The auth requirement or nil
|
|
78
78
|
def auth_requirement
|
|
79
|
-
|
|
79
|
+
auth_requirements.first
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Get all authentication requirements as an array
|
|
83
|
+
# Supports multiple strategies: auth=session,apikey,oauth
|
|
84
|
+
# @return [Array<String>] Array of auth requirement strings
|
|
85
|
+
def auth_requirements
|
|
86
|
+
auth = option(:auth)
|
|
87
|
+
return [] unless auth
|
|
88
|
+
|
|
89
|
+
auth.split(',').map(&:strip).reject(&:empty?)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Get role requirement for route-level authorization
|
|
93
|
+
# Supports single role or comma-separated roles (OR logic): role=admin,editor
|
|
94
|
+
# @return [String, nil] The role requirement or nil
|
|
95
|
+
def role_requirement
|
|
96
|
+
option(:role)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Get all role requirements as an array
|
|
100
|
+
# Supports multiple roles with OR logic: role=admin,editor
|
|
101
|
+
# @return [Array<String>] Array of role requirement strings
|
|
102
|
+
def role_requirements
|
|
103
|
+
role = option(:role)
|
|
104
|
+
return [] unless role
|
|
105
|
+
|
|
106
|
+
role.split(',').map(&:strip).reject(&:empty?)
|
|
80
107
|
end
|
|
81
108
|
|
|
82
109
|
# Get response type
|
|
@@ -111,16 +138,16 @@ class Otto
|
|
|
111
138
|
# @return [Hash]
|
|
112
139
|
def to_h
|
|
113
140
|
{
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
141
|
+
verb: @verb,
|
|
142
|
+
path: @path,
|
|
143
|
+
definition: @definition,
|
|
144
|
+
target: @target,
|
|
145
|
+
klass_name: @klass_name,
|
|
119
146
|
method_name: @method_name,
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
147
|
+
kind: @kind,
|
|
148
|
+
options: @options,
|
|
149
|
+
pattern: @pattern,
|
|
150
|
+
keys: @keys,
|
|
124
151
|
}
|
|
125
152
|
end
|
|
126
153
|
|
|
@@ -166,11 +193,11 @@ class Otto
|
|
|
166
193
|
case target
|
|
167
194
|
when /^(.+)\.(.+)$/
|
|
168
195
|
# Class.method - call class method directly
|
|
169
|
-
{ klass_name:
|
|
196
|
+
{ klass_name: ::Regexp.last_match(1), method_name: ::Regexp.last_match(2), kind: :class }
|
|
170
197
|
|
|
171
198
|
when /^(.+)#(.+)$/
|
|
172
199
|
# Class#method - instantiate then call instance method
|
|
173
|
-
{ klass_name:
|
|
200
|
+
{ klass_name: ::Regexp.last_match(1), method_name: ::Regexp.last_match(2), kind: :instance }
|
|
174
201
|
|
|
175
202
|
when /^[A-Z][A-Za-z0-9_]*(?:::[A-Z][A-Za-z0-9_]*)*$/
|
|
176
203
|
# Bare class name - instantiate the class
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
1
|
# lib/otto/route_handlers/class_method.rb
|
|
2
|
+
#
|
|
3
|
+
# frozen_string_literal: true
|
|
4
4
|
|
|
5
5
|
require 'json'
|
|
6
6
|
require 'securerandom'
|
|
@@ -13,6 +13,7 @@ class Otto
|
|
|
13
13
|
# Maintains backward compatibility for Controller.action patterns
|
|
14
14
|
class ClassMethodHandler < BaseHandler
|
|
15
15
|
def call(env, extra_params = {})
|
|
16
|
+
start_time = Otto::Utils.now_in_μs
|
|
16
17
|
req = Rack::Request.new(env)
|
|
17
18
|
res = Rack::Response.new
|
|
18
19
|
|
|
@@ -25,19 +26,22 @@ class Otto
|
|
|
25
26
|
|
|
26
27
|
# Only handle response if response_type is not default
|
|
27
28
|
if route_definition.response_type != 'default'
|
|
28
|
-
handle_response(result, res,
|
|
29
|
-
|
|
29
|
+
handle_response(result, res,
|
|
30
|
+
{
|
|
31
|
+
class: target_class,
|
|
30
32
|
request: req,
|
|
31
|
-
|
|
33
|
+
})
|
|
32
34
|
end
|
|
33
35
|
rescue StandardError => e
|
|
34
36
|
# Check if we're being called through Otto's integrated context (vs direct handler testing)
|
|
35
37
|
# In integrated context, let Otto's centralized error handler manage the response
|
|
36
38
|
# In direct testing context, handle errors locally for unit testing
|
|
37
39
|
if otto_instance
|
|
38
|
-
#
|
|
39
|
-
|
|
40
|
-
|
|
40
|
+
# Store handler context in env for centralized error handler
|
|
41
|
+
handler_name = "#{target_class.name}##{route_definition.method_name}"
|
|
42
|
+
env['otto.handler'] = handler_name
|
|
43
|
+
env['otto.handler_duration'] = Otto::Utils.now_in_μs - start_time
|
|
44
|
+
|
|
41
45
|
raise e # Re-raise to let Otto's centralized error handler manage the response
|
|
42
46
|
else
|
|
43
47
|
# Direct handler testing context - handle errors locally with security improvements
|
|
@@ -51,26 +55,15 @@ class Otto
|
|
|
51
55
|
accept_header = env['HTTP_ACCEPT'].to_s
|
|
52
56
|
if accept_header.include?('application/json')
|
|
53
57
|
res.headers['content-type'] = 'application/json'
|
|
54
|
-
error_data =
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}
|
|
60
|
-
else
|
|
61
|
-
{
|
|
62
|
-
error: 'Internal Server Error',
|
|
63
|
-
message: 'An error occurred. Please try again later.',
|
|
64
|
-
}
|
|
65
|
-
end
|
|
58
|
+
error_data = {
|
|
59
|
+
error: 'Internal Server Error',
|
|
60
|
+
message: 'Server error occurred. Check logs for details.',
|
|
61
|
+
error_id: error_id,
|
|
62
|
+
}
|
|
66
63
|
res.write JSON.generate(error_data)
|
|
67
64
|
else
|
|
68
65
|
res.headers['content-type'] = 'text/plain'
|
|
69
|
-
|
|
70
|
-
res.write "Server error (ID: #{error_id}). Check logs for details."
|
|
71
|
-
else
|
|
72
|
-
res.write 'An error occurred. Please try again later.'
|
|
73
|
-
end
|
|
66
|
+
res.write "Server error (ID: #{error_id}). Check logs for details."
|
|
74
67
|
end
|
|
75
68
|
|
|
76
69
|
# Add security headers if available
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
1
|
# lib/otto/route_handlers/factory.rb
|
|
2
|
+
#
|
|
3
|
+
# frozen_string_literal: true
|
|
4
4
|
|
|
5
5
|
require_relative 'base'
|
|
6
|
+
require_relative '../security/authentication/route_auth_wrapper'
|
|
6
7
|
|
|
7
8
|
class Otto
|
|
8
9
|
module RouteHandlers
|
|
@@ -14,24 +15,25 @@ class Otto
|
|
|
14
15
|
# @return [BaseHandler] Appropriate handler for the route
|
|
15
16
|
def self.create_handler(route_definition, otto_instance = nil)
|
|
16
17
|
# Create base handler based on route kind
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
end
|
|
18
|
+
handler_class = case route_definition.kind
|
|
19
|
+
when :logic then LogicClassHandler
|
|
20
|
+
when :instance then InstanceMethodHandler
|
|
21
|
+
when :class then ClassMethodHandler
|
|
22
|
+
else
|
|
23
|
+
raise ArgumentError, "Unknown handler kind: #{route_definition.kind}"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
handler = handler_class.new(route_definition, otto_instance)
|
|
27
27
|
|
|
28
|
-
#
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
# Always wrap with RouteAuthWrapper to ensure env['otto.strategy_result'] is set
|
|
29
|
+
# - Routes WITH auth requirement: Enforces authentication
|
|
30
|
+
# - Routes WITHOUT auth requirement: Sets anonymous StrategyResult
|
|
31
|
+
if otto_instance&.auth_config
|
|
31
32
|
handler = Otto::Security::Authentication::RouteAuthWrapper.new(
|
|
32
33
|
handler,
|
|
33
34
|
route_definition,
|
|
34
|
-
otto_instance.auth_config
|
|
35
|
+
otto_instance.auth_config,
|
|
36
|
+
otto_instance.security_config
|
|
35
37
|
)
|
|
36
38
|
end
|
|
37
39
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
1
|
# lib/otto/route_handlers/instance_method.rb
|
|
2
|
+
#
|
|
3
|
+
# frozen_string_literal: true
|
|
4
4
|
require 'securerandom'
|
|
5
5
|
|
|
6
6
|
require_relative 'base'
|
|
@@ -11,6 +11,7 @@ class Otto
|
|
|
11
11
|
# Maintains backward compatibility for Controller#action patterns
|
|
12
12
|
class InstanceMethodHandler < BaseHandler
|
|
13
13
|
def call(env, extra_params = {})
|
|
14
|
+
start_time = Otto::Utils.now_in_μs
|
|
14
15
|
req = Rack::Request.new(env)
|
|
15
16
|
res = Rack::Response.new
|
|
16
17
|
|
|
@@ -34,9 +35,11 @@ class Otto
|
|
|
34
35
|
# In integrated context, let Otto's centralized error handler manage the response
|
|
35
36
|
# In direct testing context, handle errors locally for unit testing
|
|
36
37
|
if otto_instance
|
|
37
|
-
#
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
# Store handler context in env for centralized error handler
|
|
39
|
+
handler_name = "#{target_class}##{route_definition.method_name}"
|
|
40
|
+
env['otto.handler'] = handler_name
|
|
41
|
+
env['otto.handler_duration'] = Otto::Utils.now_in_μs - start_time
|
|
42
|
+
|
|
40
43
|
raise e # Re-raise to let Otto's centralized error handler manage the response
|
|
41
44
|
else
|
|
42
45
|
# 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/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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
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
|
|
|
@@ -13,12 +13,13 @@ class Otto
|
|
|
13
13
|
# Logic classes use signature: initialize(context, params, locale)
|
|
14
14
|
class LogicClassHandler < BaseHandler
|
|
15
15
|
def call(env, extra_params = {})
|
|
16
|
+
start_time = Otto::Utils.now_in_μs
|
|
16
17
|
req = Rack::Request.new(env)
|
|
17
18
|
res = Rack::Response.new
|
|
18
19
|
|
|
19
20
|
begin
|
|
20
|
-
# Get strategy result (guaranteed to exist from
|
|
21
|
-
strategy_result = env['otto.strategy_result']
|
|
21
|
+
# Get strategy result (guaranteed to exist from RouteAuthWrapper)
|
|
22
|
+
strategy_result = env['otto.strategy_result']
|
|
22
23
|
|
|
23
24
|
# Initialize Logic class with new signature: context, params, locale
|
|
24
25
|
logic_params = req.params.merge(extra_params)
|
|
@@ -30,7 +31,21 @@ class Otto
|
|
|
30
31
|
json_data = JSON.parse(req.body.read)
|
|
31
32
|
logic_params = logic_params.merge(json_data) if json_data.is_a?(Hash)
|
|
32
33
|
rescue JSON::ParserError => e
|
|
33
|
-
|
|
34
|
+
# Base context pattern: create once, reuse for correlation
|
|
35
|
+
base_context = Otto::LoggingHelpers.request_context(env)
|
|
36
|
+
|
|
37
|
+
Otto.structured_log(:error, "JSON parsing error",
|
|
38
|
+
base_context.merge(
|
|
39
|
+
handler: "#{target_class}#call",
|
|
40
|
+
error: e.message,
|
|
41
|
+
error_class: e.class.name,
|
|
42
|
+
duration: Otto::Utils.now_in_μs - start_time
|
|
43
|
+
)
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
Otto::LoggingHelpers.log_backtrace(e,
|
|
47
|
+
base_context.merge(handler: "#{target_class}#call")
|
|
48
|
+
)
|
|
34
49
|
end
|
|
35
50
|
end
|
|
36
51
|
|
|
@@ -58,9 +73,11 @@ class Otto
|
|
|
58
73
|
# In integrated context, let Otto's centralized error handler manage the response
|
|
59
74
|
# In direct testing context, handle errors locally for unit testing
|
|
60
75
|
if otto_instance
|
|
61
|
-
#
|
|
62
|
-
|
|
63
|
-
|
|
76
|
+
# Store handler context in env for centralized error handler
|
|
77
|
+
handler_name = "#{target_class}#call"
|
|
78
|
+
env['otto.handler'] = handler_name
|
|
79
|
+
env['otto.handler_duration'] = Otto::Utils.now_in_μs - start_time
|
|
80
|
+
|
|
64
81
|
raise e # Re-raise to let Otto's centralized error handler manage the response
|
|
65
82
|
else
|
|
66
83
|
# Direct handler testing context - handle errors locally with security improvements
|
data/lib/otto/route_handlers.rb
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
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
|
|
8
8
|
# Failure result for authentication failures
|
|
9
|
-
|
|
10
|
-
#
|
|
9
|
+
AuthFailure = Data.define(:failure_reason, :auth_method) do
|
|
10
|
+
# AuthFailure represents authentication failure
|
|
11
11
|
# Returned by strategies when authentication fails
|
|
12
12
|
# Contains failure reason for error messages
|
|
13
13
|
|
|
@@ -36,7 +36,7 @@ class Otto
|
|
|
36
36
|
#
|
|
37
37
|
# @return [String] Debug representation
|
|
38
38
|
def inspect
|
|
39
|
-
"#<
|
|
39
|
+
"#<AuthFailure reason=#{failure_reason.inspect} method=#{auth_method}>"
|
|
40
40
|
end
|
|
41
41
|
end
|
|
42
42
|
end
|
|
@@ -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,
|
|
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,19 +23,24 @@ 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
|
|
|
33
|
-
# Helper for authentication failure - return
|
|
40
|
+
# Helper for authentication failure - return AuthFailure
|
|
34
41
|
def failure(reason = nil)
|
|
35
42
|
Otto.logger.debug "[#{self.class}] Authentication failed: #{reason}" if reason
|
|
36
|
-
Otto::Security::Authentication::
|
|
43
|
+
Otto::Security::Authentication::AuthFailure.new(
|
|
37
44
|
failure_reason: reason || 'Authentication failed',
|
|
38
45
|
auth_method: self.class.name.split('::').last
|
|
39
46
|
)
|