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.
Files changed (105) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +1 -3
  3. data/.github/workflows/claude-code-review.yml +29 -13
  4. data/.github/workflows/code-smells.yml +146 -0
  5. data/.gitignore +4 -0
  6. data/.pre-commit-config.yaml +2 -2
  7. data/.reek.yml +99 -0
  8. data/CHANGELOG.rst +90 -0
  9. data/CLAUDE.md +116 -45
  10. data/Gemfile +5 -2
  11. data/Gemfile.lock +70 -24
  12. data/README.md +49 -1
  13. data/changelog.d/20251103_235431_delano_86_improve_error_logging.rst +15 -0
  14. data/changelog.d/20251109_025012_claude_fix_backtrace_sanitization.rst +37 -0
  15. data/docs/.gitignore +1 -0
  16. data/docs/ipaddr-encoding-quirk.md +34 -0
  17. data/docs/migrating/v2.0.0-pre2.md +11 -18
  18. data/examples/advanced_routes/README.md +137 -20
  19. data/examples/authentication_strategies/README.md +212 -19
  20. data/examples/authentication_strategies/config.ru +0 -1
  21. data/examples/backtrace_sanitization_demo.rb +86 -0
  22. data/examples/basic/README.md +61 -10
  23. data/examples/error_handler_registration.rb +136 -0
  24. data/examples/logging_improvements.rb +76 -0
  25. data/examples/mcp_demo/README.md +187 -27
  26. data/examples/security_features/README.md +249 -30
  27. data/examples/simple_geo_resolver.rb +107 -0
  28. data/lib/otto/core/configuration.rb +90 -45
  29. data/lib/otto/core/error_handler.rb +138 -8
  30. data/lib/otto/core/file_safety.rb +2 -2
  31. data/lib/otto/core/freezable.rb +93 -0
  32. data/lib/otto/core/middleware_stack.rb +25 -18
  33. data/lib/otto/core/router.rb +62 -9
  34. data/lib/otto/core/uri_generator.rb +2 -2
  35. data/lib/otto/core.rb +10 -0
  36. data/lib/otto/design_system.rb +2 -2
  37. data/lib/otto/env_keys.rb +65 -12
  38. data/lib/otto/helpers/base.rb +2 -2
  39. data/lib/otto/helpers/request.rb +85 -2
  40. data/lib/otto/helpers/response.rb +5 -5
  41. data/lib/otto/helpers/validation.rb +2 -2
  42. data/lib/otto/helpers.rb +6 -0
  43. data/lib/otto/locale/config.rb +56 -0
  44. data/lib/otto/locale/middleware.rb +160 -0
  45. data/lib/otto/locale.rb +10 -0
  46. data/lib/otto/logging_helpers.rb +273 -0
  47. data/lib/otto/mcp/auth/token.rb +2 -2
  48. data/lib/otto/mcp/protocol.rb +2 -2
  49. data/lib/otto/mcp/rate_limiting.rb +2 -2
  50. data/lib/otto/mcp/registry.rb +2 -2
  51. data/lib/otto/mcp/route_parser.rb +2 -2
  52. data/lib/otto/mcp/schema_validation.rb +2 -2
  53. data/lib/otto/mcp/server.rb +2 -2
  54. data/lib/otto/mcp.rb +5 -0
  55. data/lib/otto/privacy/config.rb +201 -0
  56. data/lib/otto/privacy/geo_resolver.rb +285 -0
  57. data/lib/otto/privacy/ip_privacy.rb +177 -0
  58. data/lib/otto/privacy/redacted_fingerprint.rb +146 -0
  59. data/lib/otto/privacy.rb +31 -0
  60. data/lib/otto/response_handlers/auto.rb +2 -0
  61. data/lib/otto/response_handlers/base.rb +2 -0
  62. data/lib/otto/response_handlers/default.rb +2 -0
  63. data/lib/otto/response_handlers/factory.rb +2 -0
  64. data/lib/otto/response_handlers/json.rb +2 -0
  65. data/lib/otto/response_handlers/redirect.rb +2 -0
  66. data/lib/otto/response_handlers/view.rb +2 -0
  67. data/lib/otto/response_handlers.rb +2 -2
  68. data/lib/otto/route.rb +4 -4
  69. data/lib/otto/route_definition.rb +42 -15
  70. data/lib/otto/route_handlers/base.rb +2 -1
  71. data/lib/otto/route_handlers/class_method.rb +18 -25
  72. data/lib/otto/route_handlers/factory.rb +18 -16
  73. data/lib/otto/route_handlers/instance_method.rb +8 -5
  74. data/lib/otto/route_handlers/lambda.rb +8 -20
  75. data/lib/otto/route_handlers/logic_class.rb +25 -8
  76. data/lib/otto/route_handlers.rb +2 -2
  77. data/lib/otto/security/authentication/{failure_result.rb → auth_failure.rb} +5 -5
  78. data/lib/otto/security/authentication/auth_strategy.rb +13 -6
  79. data/lib/otto/security/authentication/route_auth_wrapper.rb +304 -41
  80. data/lib/otto/security/authentication/strategies/api_key_strategy.rb +2 -0
  81. data/lib/otto/security/authentication/strategies/noauth_strategy.rb +7 -1
  82. data/lib/otto/security/authentication/strategies/permission_strategy.rb +2 -0
  83. data/lib/otto/security/authentication/strategies/role_strategy.rb +2 -0
  84. data/lib/otto/security/authentication/strategies/session_strategy.rb +2 -0
  85. data/lib/otto/security/authentication/strategy_result.rb +6 -5
  86. data/lib/otto/security/authentication.rb +5 -6
  87. data/lib/otto/security/authorization_error.rb +73 -0
  88. data/lib/otto/security/config.rb +53 -9
  89. data/lib/otto/security/configurator.rb +17 -15
  90. data/lib/otto/security/csrf.rb +2 -2
  91. data/lib/otto/security/middleware/csrf_middleware.rb +11 -1
  92. data/lib/otto/security/middleware/ip_privacy_middleware.rb +231 -0
  93. data/lib/otto/security/middleware/rate_limit_middleware.rb +2 -0
  94. data/lib/otto/security/middleware/validation_middleware.rb +15 -0
  95. data/lib/otto/security/rate_limiter.rb +2 -2
  96. data/lib/otto/security/rate_limiting.rb +2 -2
  97. data/lib/otto/security/validator.rb +2 -2
  98. data/lib/otto/security.rb +12 -0
  99. data/lib/otto/static.rb +2 -2
  100. data/lib/otto/utils.rb +27 -2
  101. data/lib/otto/version.rb +3 -3
  102. data/lib/otto.rb +344 -89
  103. data/otto.gemspec +9 -2
  104. metadata +72 -8
  105. 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.pre7
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: concurrent-ruby
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '1.3'
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.3'
29
+ - - "<"
30
+ - !ruby/object:Gem::Version
31
+ version: '2.0'
32
+ - !ruby/object:Gem::Dependency
33
+ name: ipaddr
34
+ requirement: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - "~>"
37
+ - !ruby/object:Gem::Version
38
+ version: '1'
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'
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
@@ -67,16 +107,16 @@ dependencies:
67
107
  name: rexml
68
108
  requirement: !ruby/object:Gem::Requirement
69
109
  requirements:
70
- - - ">="
110
+ - - "~>"
71
111
  - !ruby/object:Gem::Version
72
- version: 3.3.6
112
+ version: '3.4'
73
113
  type: :runtime
74
114
  prerelease: false
75
115
  version_requirements: !ruby/object:Gem::Requirement
76
116
  requirements:
77
- - - ">="
117
+ - - "~>"
78
118
  - !ruby/object:Gem::Version
79
- version: 3.3.6
119
+ version: '3.4'
80
120
  - !ruby/object:Gem::Dependency
81
121
  name: facets
82
122
  requirement: !ruby/object:Gem::Requirement
@@ -115,9 +155,11 @@ files:
115
155
  - ".github/workflows/ci.yml"
116
156
  - ".github/workflows/claude-code-review.yml"
117
157
  - ".github/workflows/claude.yml"
158
+ - ".github/workflows/code-smells.yml"
118
159
  - ".gitignore"
119
160
  - ".pre-commit-config.yaml"
120
161
  - ".pre-push-config.yaml"
162
+ - ".reek.yml"
121
163
  - ".rspec"
122
164
  - ".rubocop.yml"
123
165
  - CHANGELOG.rst
@@ -127,9 +169,12 @@ files:
127
169
  - LICENSE.txt
128
170
  - README.md
129
171
  - bin/rspec
172
+ - changelog.d/20251103_235431_delano_86_improve_error_logging.rst
173
+ - changelog.d/20251109_025012_claude_fix_backtrace_sanitization.rst
130
174
  - changelog.d/README.md
131
175
  - changelog.d/scriv.ini
132
176
  - docs/.gitignore
177
+ - docs/ipaddr-encoding-quirk.md
133
178
  - docs/migrating/v2.0.0-pre1.md
134
179
  - docs/migrating/v2.0.0-pre2.md
135
180
  - examples/.gitignore
@@ -173,10 +218,13 @@ files:
173
218
  - examples/authentication_strategies/app/controllers/main_controller.rb
174
219
  - examples/authentication_strategies/config.ru
175
220
  - examples/authentication_strategies/routes
221
+ - examples/backtrace_sanitization_demo.rb
176
222
  - examples/basic/README.md
177
223
  - examples/basic/app.rb
178
224
  - examples/basic/config.ru
179
225
  - examples/basic/routes
226
+ - examples/error_handler_registration.rb
227
+ - examples/logging_improvements.rb
180
228
  - examples/mcp_demo/README.md
181
229
  - examples/mcp_demo/app.rb
182
230
  - examples/mcp_demo/config.ru
@@ -185,19 +233,28 @@ files:
185
233
  - examples/security_features/app.rb
186
234
  - examples/security_features/config.ru
187
235
  - examples/security_features/routes
236
+ - examples/simple_geo_resolver.rb
188
237
  - lib/otto.rb
238
+ - lib/otto/core.rb
189
239
  - lib/otto/core/configuration.rb
190
240
  - lib/otto/core/error_handler.rb
191
241
  - lib/otto/core/file_safety.rb
242
+ - lib/otto/core/freezable.rb
192
243
  - lib/otto/core/middleware_stack.rb
193
244
  - lib/otto/core/router.rb
194
245
  - lib/otto/core/uri_generator.rb
195
246
  - lib/otto/design_system.rb
196
247
  - lib/otto/env_keys.rb
248
+ - lib/otto/helpers.rb
197
249
  - lib/otto/helpers/base.rb
198
250
  - lib/otto/helpers/request.rb
199
251
  - lib/otto/helpers/response.rb
200
252
  - lib/otto/helpers/validation.rb
253
+ - lib/otto/locale.rb
254
+ - lib/otto/locale/config.rb
255
+ - lib/otto/locale/middleware.rb
256
+ - lib/otto/logging_helpers.rb
257
+ - lib/otto/mcp.rb
201
258
  - lib/otto/mcp/auth/token.rb
202
259
  - lib/otto/mcp/protocol.rb
203
260
  - lib/otto/mcp/rate_limiting.rb
@@ -205,6 +262,11 @@ files:
205
262
  - lib/otto/mcp/route_parser.rb
206
263
  - lib/otto/mcp/schema_validation.rb
207
264
  - lib/otto/mcp/server.rb
265
+ - lib/otto/privacy.rb
266
+ - lib/otto/privacy/config.rb
267
+ - lib/otto/privacy/geo_resolver.rb
268
+ - lib/otto/privacy/ip_privacy.rb
269
+ - lib/otto/privacy/redacted_fingerprint.rb
208
270
  - lib/otto/response_handlers.rb
209
271
  - lib/otto/response_handlers/auto.rb
210
272
  - lib/otto/response_handlers/base.rb
@@ -222,10 +284,10 @@ files:
222
284
  - lib/otto/route_handlers/instance_method.rb
223
285
  - lib/otto/route_handlers/lambda.rb
224
286
  - lib/otto/route_handlers/logic_class.rb
287
+ - lib/otto/security.rb
225
288
  - lib/otto/security/authentication.rb
289
+ - lib/otto/security/authentication/auth_failure.rb
226
290
  - lib/otto/security/authentication/auth_strategy.rb
227
- - lib/otto/security/authentication/authentication_middleware.rb
228
- - lib/otto/security/authentication/failure_result.rb
229
291
  - lib/otto/security/authentication/route_auth_wrapper.rb
230
292
  - lib/otto/security/authentication/strategies/api_key_strategy.rb
231
293
  - lib/otto/security/authentication/strategies/noauth_strategy.rb
@@ -233,10 +295,12 @@ files:
233
295
  - lib/otto/security/authentication/strategies/role_strategy.rb
234
296
  - lib/otto/security/authentication/strategies/session_strategy.rb
235
297
  - lib/otto/security/authentication/strategy_result.rb
298
+ - lib/otto/security/authorization_error.rb
236
299
  - lib/otto/security/config.rb
237
300
  - lib/otto/security/configurator.rb
238
301
  - lib/otto/security/csrf.rb
239
302
  - lib/otto/security/middleware/csrf_middleware.rb
303
+ - lib/otto/security/middleware/ip_privacy_middleware.rb
240
304
  - lib/otto/security/middleware/rate_limit_middleware.rb
241
305
  - lib/otto/security/middleware/validation_middleware.rb
242
306
  - lib/otto/security/rate_limiter.rb
@@ -270,7 +334,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
270
334
  - !ruby/object:Gem::Version
271
335
  version: '0'
272
336
  requirements: []
273
- rubygems_version: 3.6.9
337
+ rubygems_version: 3.7.2
274
338
  specification_version: 4
275
339
  summary: Auto-define your rack-apps in plaintext.
276
340
  test_files: []
@@ -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