otto 2.0.0.pre2 → 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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +0 -2
  3. data/.github/workflows/claude-code-review.yml +29 -13
  4. data/CLAUDE.md +537 -0
  5. data/Gemfile +2 -1
  6. data/Gemfile.lock +17 -10
  7. data/benchmark_middleware_wrap.rb +163 -0
  8. data/changelog.d/20251014_144317_delano_54_thats_a_wrapper.rst +36 -0
  9. data/changelog.d/20251014_161526_delano_54_thats_a_wrapper.rst +5 -0
  10. data/docs/.gitignore +1 -0
  11. data/docs/ipaddr-encoding-quirk.md +34 -0
  12. data/docs/migrating/v2.0.0-pre2.md +11 -18
  13. data/examples/authentication_strategies/config.ru +0 -1
  14. data/lib/otto/core/configuration.rb +89 -39
  15. data/lib/otto/core/freezable.rb +93 -0
  16. data/lib/otto/core/middleware_stack.rb +24 -17
  17. data/lib/otto/core/router.rb +1 -1
  18. data/lib/otto/core.rb +8 -0
  19. data/lib/otto/env_keys.rb +8 -4
  20. data/lib/otto/helpers/request.rb +80 -2
  21. data/lib/otto/helpers/response.rb +3 -3
  22. data/lib/otto/helpers.rb +4 -0
  23. data/lib/otto/locale/config.rb +56 -0
  24. data/lib/otto/mcp.rb +3 -0
  25. data/lib/otto/privacy/config.rb +199 -0
  26. data/lib/otto/privacy/geo_resolver.rb +115 -0
  27. data/lib/otto/privacy/ip_privacy.rb +175 -0
  28. data/lib/otto/privacy/redacted_fingerprint.rb +136 -0
  29. data/lib/otto/privacy.rb +29 -0
  30. data/lib/otto/route_handlers/base.rb +1 -2
  31. data/lib/otto/route_handlers/factory.rb +16 -14
  32. data/lib/otto/route_handlers/logic_class.rb +2 -2
  33. data/lib/otto/security/authentication/{failure_result.rb → auth_failure.rb} +3 -3
  34. data/lib/otto/security/authentication/auth_strategy.rb +3 -3
  35. data/lib/otto/security/authentication/route_auth_wrapper.rb +137 -26
  36. data/lib/otto/security/authentication/strategies/noauth_strategy.rb +5 -1
  37. data/lib/otto/security/authentication.rb +3 -4
  38. data/lib/otto/security/config.rb +51 -7
  39. data/lib/otto/security/configurator.rb +0 -13
  40. data/lib/otto/security/middleware/ip_privacy_middleware.rb +211 -0
  41. data/lib/otto/security.rb +9 -0
  42. data/lib/otto/version.rb +1 -1
  43. data/lib/otto.rb +181 -86
  44. data/otto.gemspec +3 -0
  45. metadata +58 -3
  46. data/lib/otto/security/authentication/authentication_middleware.rb +0 -140
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.pre2
4
+ version: 2.0.0.pre3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Delano Mandelbaum
@@ -9,6 +9,46 @@ bindir: bin
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: ipaddr
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '1'
19
+ - - "<"
20
+ - !ruby/object:Gem::Version
21
+ version: '2.0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - "~>"
27
+ - !ruby/object:Gem::Version
28
+ version: '1'
29
+ - - "<"
30
+ - !ruby/object:Gem::Version
31
+ version: '2.0'
32
+ - !ruby/object:Gem::Dependency
33
+ name: concurrent-ruby
34
+ requirement: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - "~>"
37
+ - !ruby/object:Gem::Version
38
+ version: '1.3'
39
+ - - "<"
40
+ - !ruby/object:Gem::Version
41
+ version: '2.0'
42
+ type: :runtime
43
+ prerelease: false
44
+ version_requirements: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - "~>"
47
+ - !ruby/object:Gem::Version
48
+ version: '1.3'
49
+ - - "<"
50
+ - !ruby/object:Gem::Version
51
+ version: '2.0'
12
52
  - !ruby/object:Gem::Dependency
13
53
  name: logger
14
54
  requirement: !ruby/object:Gem::Requirement
@@ -126,10 +166,14 @@ files:
126
166
  - Gemfile.lock
127
167
  - LICENSE.txt
128
168
  - README.md
169
+ - benchmark_middleware_wrap.rb
129
170
  - bin/rspec
171
+ - changelog.d/20251014_144317_delano_54_thats_a_wrapper.rst
172
+ - changelog.d/20251014_161526_delano_54_thats_a_wrapper.rst
130
173
  - changelog.d/README.md
131
174
  - changelog.d/scriv.ini
132
175
  - docs/.gitignore
176
+ - docs/ipaddr-encoding-quirk.md
133
177
  - docs/migrating/v2.0.0-pre1.md
134
178
  - docs/migrating/v2.0.0-pre2.md
135
179
  - examples/.gitignore
@@ -186,18 +230,23 @@ files:
186
230
  - examples/security_features/config.ru
187
231
  - examples/security_features/routes
188
232
  - lib/otto.rb
233
+ - lib/otto/core.rb
189
234
  - lib/otto/core/configuration.rb
190
235
  - lib/otto/core/error_handler.rb
191
236
  - lib/otto/core/file_safety.rb
237
+ - lib/otto/core/freezable.rb
192
238
  - lib/otto/core/middleware_stack.rb
193
239
  - lib/otto/core/router.rb
194
240
  - lib/otto/core/uri_generator.rb
195
241
  - lib/otto/design_system.rb
196
242
  - lib/otto/env_keys.rb
243
+ - lib/otto/helpers.rb
197
244
  - lib/otto/helpers/base.rb
198
245
  - lib/otto/helpers/request.rb
199
246
  - lib/otto/helpers/response.rb
200
247
  - lib/otto/helpers/validation.rb
248
+ - lib/otto/locale/config.rb
249
+ - lib/otto/mcp.rb
201
250
  - lib/otto/mcp/auth/token.rb
202
251
  - lib/otto/mcp/protocol.rb
203
252
  - lib/otto/mcp/rate_limiting.rb
@@ -205,6 +254,11 @@ files:
205
254
  - lib/otto/mcp/route_parser.rb
206
255
  - lib/otto/mcp/schema_validation.rb
207
256
  - lib/otto/mcp/server.rb
257
+ - lib/otto/privacy.rb
258
+ - lib/otto/privacy/config.rb
259
+ - lib/otto/privacy/geo_resolver.rb
260
+ - lib/otto/privacy/ip_privacy.rb
261
+ - lib/otto/privacy/redacted_fingerprint.rb
208
262
  - lib/otto/response_handlers.rb
209
263
  - lib/otto/response_handlers/auto.rb
210
264
  - lib/otto/response_handlers/base.rb
@@ -222,10 +276,10 @@ files:
222
276
  - lib/otto/route_handlers/instance_method.rb
223
277
  - lib/otto/route_handlers/lambda.rb
224
278
  - lib/otto/route_handlers/logic_class.rb
279
+ - lib/otto/security.rb
225
280
  - lib/otto/security/authentication.rb
281
+ - lib/otto/security/authentication/auth_failure.rb
226
282
  - lib/otto/security/authentication/auth_strategy.rb
227
- - lib/otto/security/authentication/authentication_middleware.rb
228
- - lib/otto/security/authentication/failure_result.rb
229
283
  - lib/otto/security/authentication/route_auth_wrapper.rb
230
284
  - lib/otto/security/authentication/strategies/api_key_strategy.rb
231
285
  - lib/otto/security/authentication/strategies/noauth_strategy.rb
@@ -237,6 +291,7 @@ files:
237
291
  - lib/otto/security/configurator.rb
238
292
  - lib/otto/security/csrf.rb
239
293
  - lib/otto/security/middleware/csrf_middleware.rb
294
+ - lib/otto/security/middleware/ip_privacy_middleware.rb
240
295
  - lib/otto/security/middleware/rate_limit_middleware.rb
241
296
  - lib/otto/security/middleware/validation_middleware.rb
242
297
  - lib/otto/security/rate_limiter.rb
@@ -1,140 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'strategy_result'
4
- require_relative 'failure_result'
5
- require_relative 'route_auth_wrapper'
6
- require_relative 'strategies/noauth_strategy'
7
- require_relative 'strategies/role_strategy'
8
- require_relative 'strategies/permission_strategy'
9
-
10
- class Otto
11
- module Security
12
- module Authentication
13
- # Authentication middleware that enforces route-level auth requirements
14
- class AuthenticationMiddleware
15
- def initialize(app, security_config = {}, config = {})
16
- @app = app
17
- @security_config = security_config
18
- @config = config
19
- @strategies = config[:auth_strategies] || {}
20
- @default_strategy = config[:default_auth_strategy] || 'noauth'
21
-
22
- # Add default noauth strategy if not provided
23
- @strategies['noauth'] ||= Strategies::NoAuthStrategy.new
24
- end
25
-
26
- def call(env)
27
- # Check if this route has auth requirements
28
- route_definition = env['otto.route_definition']
29
-
30
- # If no route definition, create anonymous result and continue
31
- unless route_definition
32
- env['otto.strategy_result'] = Otto::Security::Authentication::StrategyResult.anonymous(
33
- metadata: { ip: env['REMOTE_ADDR'] }
34
- )
35
- return @app.call(env)
36
- end
37
-
38
- auth_requirement = route_definition.auth_requirement
39
-
40
- # If no auth requirement, create anonymous result and continue
41
- unless auth_requirement
42
- env['otto.strategy_result'] = Otto::Security::Authentication::StrategyResult.anonymous(
43
- metadata: { ip: env['REMOTE_ADDR'] }
44
- )
45
- return @app.call(env)
46
- end
47
-
48
- # Find appropriate strategy
49
- strategy = find_strategy(auth_requirement)
50
- return auth_error_response("Unknown authentication strategy: #{auth_requirement}") unless strategy
51
-
52
- # Perform authentication
53
- strategy_result = strategy.authenticate(env, auth_requirement)
54
-
55
- # Check result type: FailureResult indicates auth failure, StrategyResult indicates success
56
- if strategy_result.is_a?(Otto::Security::Authentication::FailureResult)
57
- # Failure - create anonymous result with failure info
58
- failure_reason = strategy_result.failure_reason || 'Authentication failed'
59
- env['otto.strategy_result'] = Otto::Security::Authentication::StrategyResult.anonymous(
60
- metadata: {
61
- ip: env['REMOTE_ADDR'],
62
- auth_failure: failure_reason,
63
- attempted_strategy: auth_requirement,
64
- }
65
- )
66
- auth_error_response(failure_reason)
67
- else
68
- # Success - store the strategy result directly
69
- env['otto.strategy_result'] = strategy_result
70
-
71
- # SESSION PERSISTENCE: This assignment is INTENTIONAL, not a merge operation.
72
- # We must ensure env['rack.session'] and strategy_result.session reference
73
- # the SAME object so that:
74
- # 1. Logic classes write to strategy_result.session
75
- # 2. Rack's session middleware persists env['rack.session']
76
- # 3. Changes from (1) are included in (2)
77
- #
78
- # Using merge! instead would break this - the objects must be identical.
79
- # See commit ed7fa0d for the bug this fixes.
80
- env['rack.session'] = strategy_result.session if strategy_result.session
81
- env['otto.user'] = strategy_result.user # For convenience
82
- env['otto.user_context'] = strategy_result.user_context # For convenience
83
- @app.call(env)
84
- end
85
- end
86
-
87
- private
88
-
89
- def find_strategy(requirement)
90
- # Try exact match first - this has highest priority
91
- return @strategies[requirement] if @strategies[requirement]
92
-
93
- # For colon-separated requirements like "role:admin", try prefix match
94
- if requirement.include?(':')
95
- prefix = requirement.split(':', 2).first
96
-
97
- # Check if we have a strategy registered for the prefix
98
- prefix_strategy = @strategies[prefix]
99
- return prefix_strategy if prefix_strategy
100
-
101
- # Try fallback patterns for role: and permission: requirements
102
- if requirement.start_with?('role:')
103
- return @strategies['role'] || Strategies::RoleStrategy.new([])
104
- elsif requirement.start_with?('permission:')
105
- return @strategies['permission'] || Strategies::PermissionStrategy.new([])
106
- end
107
- end
108
-
109
- nil
110
- end
111
-
112
- def auth_error_response(message)
113
- body = JSON.generate({
114
- error: 'Authentication Required',
115
- message: message,
116
- timestamp: Time.now.to_i,
117
- })
118
-
119
- headers = {
120
- 'Content-Type' => 'application/json',
121
- 'Content-Length' => body.bytesize.to_s,
122
- }
123
-
124
- # Add security headers if available from config hash or Otto instance
125
- # NOTE: Extracting this to a method was considered but rejected.
126
- # This logic appears only once and is clear in context. Extraction would
127
- # add ~10 lines (method def + docs) for a 5-line single-use block without
128
- # improving readability. Consider extracting if this pattern is duplicated.
129
- if @config.is_a?(Hash) && @config[:security_headers]
130
- headers.merge!(@config[:security_headers])
131
- elsif @config.respond_to?(:security_config) && @config.security_config
132
- headers.merge!(@config.security_config.security_headers)
133
- end
134
-
135
- [401, headers, [body]]
136
- end
137
- end
138
- end
139
- end
140
- end