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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +0 -2
- data/.github/workflows/claude-code-review.yml +29 -13
- data/CLAUDE.md +537 -0
- data/Gemfile +2 -1
- data/Gemfile.lock +17 -10
- data/benchmark_middleware_wrap.rb +163 -0
- data/changelog.d/20251014_144317_delano_54_thats_a_wrapper.rst +36 -0
- data/changelog.d/20251014_161526_delano_54_thats_a_wrapper.rst +5 -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/authentication_strategies/config.ru +0 -1
- data/lib/otto/core/configuration.rb +89 -39
- data/lib/otto/core/freezable.rb +93 -0
- data/lib/otto/core/middleware_stack.rb +24 -17
- data/lib/otto/core/router.rb +1 -1
- data/lib/otto/core.rb +8 -0
- data/lib/otto/env_keys.rb +8 -4
- data/lib/otto/helpers/request.rb +80 -2
- data/lib/otto/helpers/response.rb +3 -3
- data/lib/otto/helpers.rb +4 -0
- data/lib/otto/locale/config.rb +56 -0
- data/lib/otto/mcp.rb +3 -0
- data/lib/otto/privacy/config.rb +199 -0
- data/lib/otto/privacy/geo_resolver.rb +115 -0
- data/lib/otto/privacy/ip_privacy.rb +175 -0
- data/lib/otto/privacy/redacted_fingerprint.rb +136 -0
- data/lib/otto/privacy.rb +29 -0
- data/lib/otto/route_handlers/base.rb +1 -2
- data/lib/otto/route_handlers/factory.rb +16 -14
- data/lib/otto/route_handlers/logic_class.rb +2 -2
- data/lib/otto/security/authentication/{failure_result.rb → auth_failure.rb} +3 -3
- data/lib/otto/security/authentication/auth_strategy.rb +3 -3
- data/lib/otto/security/authentication/route_auth_wrapper.rb +137 -26
- data/lib/otto/security/authentication/strategies/noauth_strategy.rb +5 -1
- data/lib/otto/security/authentication.rb +3 -4
- data/lib/otto/security/config.rb +51 -7
- data/lib/otto/security/configurator.rb +0 -13
- data/lib/otto/security/middleware/ip_privacy_middleware.rb +211 -0
- data/lib/otto/security.rb +9 -0
- data/lib/otto/version.rb +1 -1
- data/lib/otto.rb +181 -86
- data/otto.gemspec +3 -0
- metadata +58 -3
- 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.
|
|
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
|