otto 2.0.0.pre1 → 2.0.0.pre3

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 (60) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +2 -3
  3. data/.github/workflows/claude-code-review.yml +30 -14
  4. data/.github/workflows/claude.yml +1 -1
  5. data/.rubocop.yml +4 -1
  6. data/CHANGELOG.rst +54 -6
  7. data/CLAUDE.md +537 -0
  8. data/Gemfile +3 -2
  9. data/Gemfile.lock +34 -26
  10. data/benchmark_middleware_wrap.rb +163 -0
  11. data/changelog.d/20251014_144317_delano_54_thats_a_wrapper.rst +36 -0
  12. data/changelog.d/20251014_161526_delano_54_thats_a_wrapper.rst +5 -0
  13. data/docs/.gitignore +2 -0
  14. data/docs/ipaddr-encoding-quirk.md +34 -0
  15. data/docs/migrating/v2.0.0-pre2.md +338 -0
  16. data/examples/authentication_strategies/config.ru +0 -1
  17. data/lib/otto/core/configuration.rb +91 -41
  18. data/lib/otto/core/freezable.rb +93 -0
  19. data/lib/otto/core/middleware_stack.rb +103 -16
  20. data/lib/otto/core/router.rb +8 -7
  21. data/lib/otto/core.rb +8 -0
  22. data/lib/otto/env_keys.rb +118 -0
  23. data/lib/otto/helpers/base.rb +2 -21
  24. data/lib/otto/helpers/request.rb +80 -2
  25. data/lib/otto/helpers/response.rb +25 -3
  26. data/lib/otto/helpers.rb +4 -0
  27. data/lib/otto/locale/config.rb +56 -0
  28. data/lib/otto/mcp/{validation.rb → schema_validation.rb} +3 -2
  29. data/lib/otto/mcp/server.rb +26 -13
  30. data/lib/otto/mcp.rb +3 -0
  31. data/lib/otto/privacy/config.rb +199 -0
  32. data/lib/otto/privacy/geo_resolver.rb +115 -0
  33. data/lib/otto/privacy/ip_privacy.rb +175 -0
  34. data/lib/otto/privacy/redacted_fingerprint.rb +136 -0
  35. data/lib/otto/privacy.rb +29 -0
  36. data/lib/otto/response_handlers/json.rb +6 -0
  37. data/lib/otto/route.rb +44 -48
  38. data/lib/otto/route_handlers/base.rb +1 -2
  39. data/lib/otto/route_handlers/factory.rb +24 -9
  40. data/lib/otto/route_handlers/logic_class.rb +2 -2
  41. data/lib/otto/security/authentication/auth_failure.rb +44 -0
  42. data/lib/otto/security/authentication/auth_strategy.rb +3 -3
  43. data/lib/otto/security/authentication/route_auth_wrapper.rb +260 -0
  44. data/lib/otto/security/authentication/strategies/{public_strategy.rb → noauth_strategy.rb} +6 -2
  45. data/lib/otto/security/authentication/strategy_result.rb +129 -15
  46. data/lib/otto/security/authentication.rb +5 -6
  47. data/lib/otto/security/config.rb +51 -18
  48. data/lib/otto/security/configurator.rb +2 -15
  49. data/lib/otto/security/middleware/ip_privacy_middleware.rb +211 -0
  50. data/lib/otto/security/middleware/rate_limit_middleware.rb +19 -3
  51. data/lib/otto/security.rb +9 -0
  52. data/lib/otto/version.rb +1 -1
  53. data/lib/otto.rb +183 -89
  54. data/otto.gemspec +5 -0
  55. metadata +83 -8
  56. data/changelog.d/20250911_235619_delano_next.rst +0 -28
  57. data/changelog.d/20250912_123055_delano_remove_ostruct.rst +0 -21
  58. data/changelog.d/20250912_175625_claude_delano_remove_ostruct.rst +0 -21
  59. data/lib/otto/security/authentication/authentication_middleware.rb +0 -123
  60. data/lib/otto/security/authentication/failure_result.rb +0 -36
@@ -1,123 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'strategy_result'
4
- require_relative 'failure_result'
5
- require_relative 'strategies/public_strategy'
6
- require_relative 'strategies/role_strategy'
7
- require_relative 'strategies/permission_strategy'
8
-
9
- class Otto
10
- module Security
11
- module Authentication
12
- # Authentication middleware that enforces route-level auth requirements
13
- class AuthenticationMiddleware
14
- def initialize(app, security_config = {}, config = {})
15
- @app = app
16
- @security_config = security_config
17
- @config = config
18
- @strategies = config[:auth_strategies] || {}
19
- @default_strategy = config[:default_auth_strategy] || 'publicly'
20
-
21
- # Add default public strategy if not provided
22
- @strategies['publicly'] ||= Strategies::PublicStrategy.new
23
- end
24
-
25
- def call(env)
26
- # Check if this route has auth requirements
27
- route_definition = env['otto.route_definition']
28
-
29
- # If no route definition, create anonymous result and continue
30
- unless route_definition
31
- env['otto.strategy_result'] = Otto::Security::Authentication::StrategyResult.anonymous(
32
- metadata: { ip: env['REMOTE_ADDR'] }
33
- )
34
- return @app.call(env)
35
- end
36
-
37
- auth_requirement = route_definition.auth_requirement
38
-
39
- # If no auth requirement, create anonymous result and continue
40
- unless auth_requirement
41
- env['otto.strategy_result'] = Otto::Security::Authentication::StrategyResult.anonymous(
42
- metadata: { ip: env['REMOTE_ADDR'] }
43
- )
44
- return @app.call(env)
45
- end
46
-
47
- # Find appropriate strategy
48
- strategy = find_strategy(auth_requirement)
49
- return auth_error_response("Unknown authentication strategy: #{auth_requirement}") unless strategy
50
-
51
- # Perform authentication
52
- strategy_result = strategy.authenticate(env, auth_requirement)
53
-
54
- if strategy_result&.success?
55
- # Success - store the strategy result directly
56
- env['otto.strategy_result'] = strategy_result
57
- env['otto.user'] = strategy_result.user # For convenience
58
- env['otto.user_context'] = strategy_result.user_context # For convenience
59
- @app.call(env)
60
- else
61
- # Failure - create anonymous result with failure info
62
- failure_reason = strategy_result&.failure_reason || 'Authentication failed'
63
- env['otto.strategy_result'] = Otto::Security::Authentication::StrategyResult.anonymous(
64
- metadata: {
65
- ip: env['REMOTE_ADDR'],
66
- auth_failure: failure_reason,
67
- attempted_strategy: auth_requirement,
68
- }
69
- )
70
- auth_error_response(failure_reason)
71
- end
72
- end
73
-
74
- private
75
-
76
- def find_strategy(requirement)
77
- # Try exact match first - this has highest priority
78
- return @strategies[requirement] if @strategies[requirement]
79
-
80
- # For colon-separated requirements like "role:admin", try prefix match
81
- if requirement.include?(':')
82
- prefix = requirement.split(':', 2).first
83
-
84
- # Check if we have a strategy registered for the prefix
85
- prefix_strategy = @strategies[prefix]
86
- return prefix_strategy if prefix_strategy
87
-
88
- # Try fallback patterns for role: and permission: requirements
89
- if requirement.start_with?('role:')
90
- return @strategies['role'] || Strategies::RoleStrategy.new([])
91
- elsif requirement.start_with?('permission:')
92
- return @strategies['permission'] || Strategies::PermissionStrategy.new([])
93
- end
94
- end
95
-
96
- nil
97
- end
98
-
99
- def auth_error_response(message)
100
- body = JSON.generate({
101
- error: 'Authentication Required',
102
- message: message,
103
- timestamp: Time.now.to_i,
104
- })
105
-
106
- headers = {
107
- 'Content-Type' => 'application/json',
108
- 'Content-Length' => body.bytesize.to_s,
109
- }
110
-
111
- # Add security headers if available from config hash or Otto instance
112
- if @config.is_a?(Hash) && @config[:security_headers]
113
- headers.merge!(@config[:security_headers])
114
- elsif @config.respond_to?(:security_config) && @config.security_config
115
- headers.merge!(@config.security_config.security_headers)
116
- end
117
-
118
- [401, headers, [body]]
119
- end
120
- end
121
- end
122
- end
123
- end
@@ -1,36 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # lib/otto/security/authentication/failure_result.rb
4
-
5
- class Otto
6
- module Security
7
- module Authentication
8
- # Failure result for authentication failures
9
- FailureResult = Data.define(:failure_reason, :auth_method) do
10
- def success?
11
- false
12
- end
13
-
14
- def failure?
15
- true
16
- end
17
-
18
- def authenticated?
19
- false
20
- end
21
-
22
- def anonymous?
23
- true
24
- end
25
-
26
- def user_context
27
- {}
28
- end
29
-
30
- def inspect
31
- "#<FailureResult reason=#{failure_reason.inspect} method=#{auth_method}>"
32
- end
33
- end
34
- end
35
- end
36
- end