otto 2.0.0.pre8 → 2.0.0.pre9
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 -1
- data/.github/workflows/claude-code-review.yml +1 -1
- data/.github/workflows/claude.yml +1 -1
- data/.github/workflows/code-smells.yml +2 -2
- data/CHANGELOG.rst +54 -35
- data/Gemfile.lock +6 -6
- data/README.md +20 -0
- data/docs/.gitignore +2 -0
- data/docs/modern-authentication-authorization-landscape.md +558 -0
- data/docs/multi-strategy-authentication-design.md +1401 -0
- data/lib/otto/core/error_handler.rb +19 -8
- data/lib/otto/core/freezable.rb +0 -2
- data/lib/otto/core/middleware_stack.rb +12 -8
- data/lib/otto/core/router.rb +25 -31
- data/lib/otto/errors.rb +92 -0
- data/lib/otto/mcp/rate_limiting.rb +6 -2
- data/lib/otto/mcp/schema_validation.rb +1 -1
- data/lib/otto/response_handlers/json.rb +1 -3
- data/lib/otto/response_handlers/view.rb +1 -1
- data/lib/otto/route_handlers/base.rb +86 -1
- data/lib/otto/route_handlers/class_method.rb +9 -67
- data/lib/otto/route_handlers/instance_method.rb +10 -57
- data/lib/otto/route_handlers/logic_class.rb +85 -90
- data/lib/otto/security/authentication/auth_strategy.rb +2 -2
- data/lib/otto/security/authentication/strategy_result.rb +9 -9
- data/lib/otto/security/authorization_error.rb +1 -1
- data/lib/otto/security/config.rb +3 -3
- data/lib/otto/security/rate_limiter.rb +7 -3
- data/lib/otto/version.rb +1 -1
- data/lib/otto.rb +47 -3
- metadata +4 -1
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
# lib/otto/route_handlers/logic_class.rb
|
|
2
2
|
#
|
|
3
3
|
# frozen_string_literal: true
|
|
4
|
-
require 'json'
|
|
5
|
-
require 'securerandom'
|
|
6
4
|
|
|
7
5
|
require_relative 'base'
|
|
8
6
|
|
|
@@ -20,98 +18,95 @@ class Otto
|
|
|
20
18
|
# For endpoints requiring direct request access (sessions, cookies, headers,
|
|
21
19
|
# or logout flows), use controller handlers (Controller#action or Controller.action).
|
|
22
20
|
class LogicClassHandler < BaseHandler
|
|
23
|
-
|
|
24
|
-
start_time = Otto::Utils.now_in_μs
|
|
25
|
-
req = Rack::Request.new(env)
|
|
26
|
-
res = Rack::Response.new
|
|
21
|
+
protected
|
|
27
22
|
|
|
23
|
+
# Invoke Logic class with constrained signature
|
|
24
|
+
# @param req [Rack::Request] Request object
|
|
25
|
+
# @param res [Rack::Response] Response object
|
|
26
|
+
# @return [Array] [result, context] for handle_response
|
|
27
|
+
def invoke_target(req, _res)
|
|
28
|
+
env = req.env
|
|
29
|
+
|
|
30
|
+
# Get strategy result (guaranteed to exist from RouteAuthWrapper)
|
|
31
|
+
strategy_result = env['otto.strategy_result']
|
|
32
|
+
|
|
33
|
+
# Extract params including JSON body parsing
|
|
34
|
+
logic_params = extract_logic_params(req, env)
|
|
35
|
+
|
|
36
|
+
# Get locale
|
|
37
|
+
locale = env['otto.locale'] || 'en'
|
|
38
|
+
|
|
39
|
+
# Instantiate Logic class
|
|
40
|
+
logic = target_class.new(strategy_result, logic_params, locale)
|
|
41
|
+
|
|
42
|
+
# Execute standard Logic class lifecycle
|
|
43
|
+
logic.raise_concerns if logic.respond_to?(:raise_concerns)
|
|
44
|
+
|
|
45
|
+
result = if logic.respond_to?(:process)
|
|
46
|
+
logic.process
|
|
47
|
+
else
|
|
48
|
+
logic.call || logic
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
context = {
|
|
52
|
+
logic_instance: logic,
|
|
53
|
+
request: req,
|
|
54
|
+
status_code: logic.respond_to?(:status_code) ? logic.status_code : nil,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
[result, context]
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Extract logic parameters including JSON body parsing
|
|
61
|
+
# @param req [Rack::Request] Request object
|
|
62
|
+
# @param env [Hash] Rack environment
|
|
63
|
+
# @return [Hash] Parameters for Logic class
|
|
64
|
+
def extract_logic_params(req, env)
|
|
65
|
+
# req.params already has extra_params merged and indifferent_params applied
|
|
66
|
+
# by setup_request_response in BaseHandler
|
|
67
|
+
logic_params = req.params.dup
|
|
68
|
+
|
|
69
|
+
# Handle JSON request bodies
|
|
70
|
+
if req.content_type&.include?('application/json') && req.body.size.positive?
|
|
71
|
+
logic_params = parse_json_body(req, env, logic_params)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
logic_params
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Parse JSON request body with error handling
|
|
78
|
+
# @param req [Rack::Request] Request object
|
|
79
|
+
# @param env [Hash] Rack environment
|
|
80
|
+
# @param logic_params [Hash] Current parameters
|
|
81
|
+
# @return [Hash] Parameters with JSON merged (or original if parsing fails)
|
|
82
|
+
def parse_json_body(req, env, logic_params)
|
|
28
83
|
begin
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
)
|
|
57
|
-
end
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
locale = env['otto.locale'] || 'en'
|
|
61
|
-
|
|
62
|
-
logic = target_class.new(strategy_result, logic_params, locale)
|
|
63
|
-
|
|
64
|
-
# Execute standard Logic class lifecycle
|
|
65
|
-
logic.raise_concerns if logic.respond_to?(:raise_concerns)
|
|
66
|
-
|
|
67
|
-
result = if logic.respond_to?(:process)
|
|
68
|
-
logic.process
|
|
69
|
-
else
|
|
70
|
-
logic.call || logic
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
# Handle response with Logic instance context
|
|
74
|
-
handle_response(result, res, {
|
|
75
|
-
logic_instance: logic,
|
|
76
|
-
request: req,
|
|
77
|
-
status_code: logic.respond_to?(:status_code) ? logic.status_code : nil,
|
|
78
|
-
})
|
|
79
|
-
rescue StandardError => e
|
|
80
|
-
# Check if we're being called through Otto's integrated context (vs direct handler testing)
|
|
81
|
-
# In integrated context, let Otto's centralized error handler manage the response
|
|
82
|
-
# In direct testing context, handle errors locally for unit testing
|
|
83
|
-
if otto_instance
|
|
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
|
-
|
|
89
|
-
raise e # Re-raise to let Otto's centralized error handler manage the response
|
|
90
|
-
else
|
|
91
|
-
# Direct handler testing context - handle errors locally with security improvements
|
|
92
|
-
error_id = SecureRandom.hex(8)
|
|
93
|
-
Otto.logger.error "[#{error_id}] #{e.class}: #{e.message}"
|
|
94
|
-
Otto.logger.debug "[#{error_id}] Backtrace: #{e.backtrace.join("\n")}" if Otto.debug
|
|
95
|
-
|
|
96
|
-
res.status = 500
|
|
97
|
-
res.headers['content-type'] = 'text/plain'
|
|
98
|
-
|
|
99
|
-
if Otto.env?(:dev, :development)
|
|
100
|
-
res.write "Server error (ID: #{error_id}). Check logs for details."
|
|
101
|
-
else
|
|
102
|
-
res.write 'An error occurred. Please try again later.'
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
# Add security headers if available
|
|
106
|
-
if otto_instance.respond_to?(:security_config) && otto_instance.security_config
|
|
107
|
-
otto_instance.security_config.security_headers.each do |header, value|
|
|
108
|
-
res.headers[header] = value
|
|
109
|
-
end
|
|
110
|
-
end
|
|
111
|
-
end
|
|
84
|
+
req.body.rewind
|
|
85
|
+
json_data = JSON.parse(req.body.read)
|
|
86
|
+
logic_params = logic_params.merge(json_data) if json_data.is_a?(Hash)
|
|
87
|
+
rescue JSON::ParserError => e
|
|
88
|
+
# Base context pattern: create once, reuse for correlation
|
|
89
|
+
log_context = Otto::LoggingHelpers.request_context(env)
|
|
90
|
+
|
|
91
|
+
Otto.structured_log(:error, 'JSON parsing error',
|
|
92
|
+
log_context.merge(
|
|
93
|
+
handler: handler_name,
|
|
94
|
+
error: e.message,
|
|
95
|
+
error_class: e.class.name,
|
|
96
|
+
duration: Otto::Utils.now_in_μs - @start_time
|
|
97
|
+
))
|
|
98
|
+
|
|
99
|
+
Otto::LoggingHelpers.log_backtrace(e,
|
|
100
|
+
log_context.merge(handler: handler_name))
|
|
112
101
|
end
|
|
113
102
|
|
|
114
|
-
|
|
103
|
+
logic_params
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Format handler name for Logic routes
|
|
107
|
+
# @return [String] Handler name in format "ClassName#call"
|
|
108
|
+
def handler_name
|
|
109
|
+
"#{target_class.name}#call"
|
|
115
110
|
end
|
|
116
111
|
end
|
|
117
112
|
end
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# lib/otto/security/authentication/auth_strategy.rb
|
|
2
2
|
#
|
|
3
3
|
# frozen_string_literal: true
|
|
4
|
-
|
|
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
|
|
|
@@ -33,7 +33,7 @@ class Otto
|
|
|
33
33
|
user: user,
|
|
34
34
|
auth_method: auth_method || self.class.name.split('::').last,
|
|
35
35
|
metadata: metadata,
|
|
36
|
-
strategy_name: nil
|
|
36
|
+
strategy_name: nil # Will be set by RouteAuthWrapper
|
|
37
37
|
)
|
|
38
38
|
end
|
|
39
39
|
|
|
@@ -320,16 +320,16 @@ class Otto
|
|
|
320
320
|
# @return [Hash] Hash representation of the context
|
|
321
321
|
def to_h
|
|
322
322
|
{
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
323
|
+
session: session,
|
|
324
|
+
user: user,
|
|
325
|
+
auth_method: auth_method,
|
|
326
|
+
metadata: metadata,
|
|
327
|
+
authenticated: authenticated?,
|
|
328
328
|
auth_attempt_succeeded: auth_attempt_succeeded?,
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
329
|
+
user_id: user_id,
|
|
330
|
+
user_name: user_name,
|
|
331
|
+
roles: roles,
|
|
332
|
+
permissions: permissions,
|
|
333
333
|
}
|
|
334
334
|
end
|
|
335
335
|
end
|
data/lib/otto/security/config.rb
CHANGED
|
@@ -436,12 +436,12 @@ class Otto
|
|
|
436
436
|
end
|
|
437
437
|
|
|
438
438
|
# Raised when a request exceeds the configured size limit
|
|
439
|
-
class RequestTooLargeError <
|
|
439
|
+
class RequestTooLargeError < Otto::PayloadTooLargeError; end
|
|
440
440
|
|
|
441
441
|
# Raised when CSRF token validation fails
|
|
442
|
-
class CSRFError <
|
|
442
|
+
class CSRFError < Otto::ForbiddenError; end
|
|
443
443
|
|
|
444
444
|
# Raised when input validation fails (XSS, SQL injection, etc.)
|
|
445
|
-
class ValidationError <
|
|
445
|
+
class ValidationError < Otto::BadRequestError; end
|
|
446
446
|
end
|
|
447
447
|
end
|
|
@@ -55,9 +55,13 @@ class Otto
|
|
|
55
55
|
'retry-after' => (match_data[:period] - (now % match_data[:period])).to_s,
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
#
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
# Content negotiation for rate limit response
|
|
59
|
+
# Route's response_type takes precedence over Accept header
|
|
60
|
+
route_def = request.env['otto.route_definition']
|
|
61
|
+
wants_json = (route_def&.response_type == 'json') ||
|
|
62
|
+
request.env['HTTP_ACCEPT'].to_s.include?('application/json')
|
|
63
|
+
|
|
64
|
+
if wants_json
|
|
61
65
|
error_response = {
|
|
62
66
|
error: 'Rate limit exceeded',
|
|
63
67
|
message: 'Too many requests',
|
data/lib/otto/version.rb
CHANGED
data/lib/otto.rb
CHANGED
|
@@ -17,6 +17,7 @@ require_relative 'otto/static'
|
|
|
17
17
|
require_relative 'otto/helpers'
|
|
18
18
|
require_relative 'otto/response_handlers'
|
|
19
19
|
require_relative 'otto/route_handlers'
|
|
20
|
+
require_relative 'otto/errors'
|
|
20
21
|
require_relative 'otto/locale'
|
|
21
22
|
require_relative 'otto/mcp'
|
|
22
23
|
require_relative 'otto/core'
|
|
@@ -79,9 +80,10 @@ class Otto
|
|
|
79
80
|
load(path) unless path.nil?
|
|
80
81
|
super()
|
|
81
82
|
|
|
82
|
-
# Auto-register
|
|
83
|
-
# This allows Logic classes
|
|
84
|
-
|
|
83
|
+
# Auto-register all Otto framework error classes
|
|
84
|
+
# This allows Logic classes and framework code to raise appropriate errors
|
|
85
|
+
# without requiring manual registration in implementing projects
|
|
86
|
+
register_framework_errors
|
|
85
87
|
|
|
86
88
|
# Build the middleware app once after all initialization is complete
|
|
87
89
|
build_app!
|
|
@@ -586,6 +588,48 @@ class Otto
|
|
|
586
588
|
configure_mcp(opts)
|
|
587
589
|
end
|
|
588
590
|
|
|
591
|
+
# Register all Otto framework error classes with appropriate status codes
|
|
592
|
+
#
|
|
593
|
+
# This method auto-registers base HTTP error classes and all framework-specific
|
|
594
|
+
# error classes (Security, MCP) so that raising them automatically returns the
|
|
595
|
+
# correct HTTP status code instead of 500.
|
|
596
|
+
#
|
|
597
|
+
# Users can override these registrations by calling register_error_handler
|
|
598
|
+
# after Otto.new with custom status codes or log levels.
|
|
599
|
+
#
|
|
600
|
+
# @return [void]
|
|
601
|
+
# @api private
|
|
602
|
+
def register_framework_errors
|
|
603
|
+
# Base HTTP errors (for direct use or subclassing by implementing projects)
|
|
604
|
+
register_error_from_class(Otto::NotFoundError)
|
|
605
|
+
register_error_from_class(Otto::BadRequestError)
|
|
606
|
+
register_error_from_class(Otto::UnauthorizedError)
|
|
607
|
+
register_error_from_class(Otto::ForbiddenError)
|
|
608
|
+
register_error_from_class(Otto::PayloadTooLargeError)
|
|
609
|
+
|
|
610
|
+
# Security module errors
|
|
611
|
+
register_error_from_class(Otto::Security::AuthorizationError)
|
|
612
|
+
register_error_from_class(Otto::Security::CSRFError)
|
|
613
|
+
register_error_from_class(Otto::Security::RequestTooLargeError)
|
|
614
|
+
register_error_from_class(Otto::Security::ValidationError)
|
|
615
|
+
|
|
616
|
+
# MCP module errors
|
|
617
|
+
register_error_from_class(Otto::MCP::ValidationError)
|
|
618
|
+
end
|
|
619
|
+
|
|
620
|
+
# Register an error handler using the error class as the single source of truth
|
|
621
|
+
#
|
|
622
|
+
# @param error_class [Class] Error class that responds to default_status and default_log_level
|
|
623
|
+
# @return [void]
|
|
624
|
+
# @api private
|
|
625
|
+
def register_error_from_class(error_class)
|
|
626
|
+
register_error_handler(
|
|
627
|
+
error_class,
|
|
628
|
+
status: error_class.default_status,
|
|
629
|
+
log_level: error_class.default_log_level
|
|
630
|
+
)
|
|
631
|
+
end
|
|
632
|
+
|
|
589
633
|
class << self
|
|
590
634
|
attr_accessor :debug, :logger # rubocop:disable ThreadSafety/ClassAndModuleAttributes
|
|
591
635
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: otto
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.0.0.
|
|
4
|
+
version: 2.0.0.pre9
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Delano Mandelbaum
|
|
@@ -175,6 +175,8 @@ files:
|
|
|
175
175
|
- docs/ipaddr-encoding-quirk.md
|
|
176
176
|
- docs/migrating/v2.0.0-pre1.md
|
|
177
177
|
- docs/migrating/v2.0.0-pre2.md
|
|
178
|
+
- docs/modern-authentication-authorization-landscape.md
|
|
179
|
+
- docs/multi-strategy-authentication-design.md
|
|
178
180
|
- examples/.gitignore
|
|
179
181
|
- examples/advanced_routes/README.md
|
|
180
182
|
- examples/advanced_routes/app.rb
|
|
@@ -243,6 +245,7 @@ files:
|
|
|
243
245
|
- lib/otto/core/uri_generator.rb
|
|
244
246
|
- lib/otto/design_system.rb
|
|
245
247
|
- lib/otto/env_keys.rb
|
|
248
|
+
- lib/otto/errors.rb
|
|
246
249
|
- lib/otto/helpers.rb
|
|
247
250
|
- lib/otto/helpers/base.rb
|
|
248
251
|
- lib/otto/helpers/request.rb
|