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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +2 -3
- data/.github/workflows/claude-code-review.yml +30 -14
- data/.github/workflows/claude.yml +1 -1
- data/.rubocop.yml +4 -1
- data/CHANGELOG.rst +54 -6
- data/CLAUDE.md +537 -0
- data/Gemfile +3 -2
- data/Gemfile.lock +34 -26
- 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 +2 -0
- data/docs/ipaddr-encoding-quirk.md +34 -0
- data/docs/migrating/v2.0.0-pre2.md +338 -0
- data/examples/authentication_strategies/config.ru +0 -1
- data/lib/otto/core/configuration.rb +91 -41
- data/lib/otto/core/freezable.rb +93 -0
- data/lib/otto/core/middleware_stack.rb +103 -16
- data/lib/otto/core/router.rb +8 -7
- data/lib/otto/core.rb +8 -0
- data/lib/otto/env_keys.rb +118 -0
- data/lib/otto/helpers/base.rb +2 -21
- data/lib/otto/helpers/request.rb +80 -2
- data/lib/otto/helpers/response.rb +25 -3
- data/lib/otto/helpers.rb +4 -0
- data/lib/otto/locale/config.rb +56 -0
- data/lib/otto/mcp/{validation.rb → schema_validation.rb} +3 -2
- data/lib/otto/mcp/server.rb +26 -13
- 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/response_handlers/json.rb +6 -0
- data/lib/otto/route.rb +44 -48
- data/lib/otto/route_handlers/base.rb +1 -2
- data/lib/otto/route_handlers/factory.rb +24 -9
- data/lib/otto/route_handlers/logic_class.rb +2 -2
- data/lib/otto/security/authentication/auth_failure.rb +44 -0
- data/lib/otto/security/authentication/auth_strategy.rb +3 -3
- data/lib/otto/security/authentication/route_auth_wrapper.rb +260 -0
- data/lib/otto/security/authentication/strategies/{public_strategy.rb → noauth_strategy.rb} +6 -2
- data/lib/otto/security/authentication/strategy_result.rb +129 -15
- data/lib/otto/security/authentication.rb +5 -6
- data/lib/otto/security/config.rb +51 -18
- data/lib/otto/security/configurator.rb +2 -15
- data/lib/otto/security/middleware/ip_privacy_middleware.rb +211 -0
- data/lib/otto/security/middleware/rate_limit_middleware.rb +19 -3
- data/lib/otto/security.rb +9 -0
- data/lib/otto/version.rb +1 -1
- data/lib/otto.rb +183 -89
- data/otto.gemspec +5 -0
- metadata +83 -8
- data/changelog.d/20250911_235619_delano_next.rst +0 -28
- data/changelog.d/20250912_123055_delano_remove_ostruct.rst +0 -21
- data/changelog.d/20250912_175625_claude_delano_remove_ostruct.rst +0 -21
- data/lib/otto/security/authentication/authentication_middleware.rb +0 -123
- data/lib/otto/security/authentication/failure_result.rb +0 -36
data/lib/otto.rb
CHANGED
|
@@ -11,29 +11,19 @@ require 'rack/request'
|
|
|
11
11
|
require 'rack/response'
|
|
12
12
|
require 'rack/utils'
|
|
13
13
|
|
|
14
|
-
require_relative 'otto/security/authentication/strategy_result'
|
|
15
14
|
require_relative 'otto/route_definition'
|
|
16
15
|
require_relative 'otto/route'
|
|
17
16
|
require_relative 'otto/static'
|
|
18
|
-
require_relative 'otto/helpers
|
|
19
|
-
require_relative 'otto/helpers/response'
|
|
17
|
+
require_relative 'otto/helpers'
|
|
20
18
|
require_relative 'otto/response_handlers'
|
|
21
19
|
require_relative 'otto/route_handlers'
|
|
22
|
-
require_relative 'otto/
|
|
23
|
-
require_relative 'otto/
|
|
24
|
-
require_relative 'otto/
|
|
25
|
-
require_relative 'otto/
|
|
26
|
-
require_relative 'otto/security
|
|
27
|
-
require_relative 'otto/security/middleware/rate_limit_middleware'
|
|
28
|
-
require_relative 'otto/mcp/server'
|
|
29
|
-
require_relative 'otto/core/router'
|
|
30
|
-
require_relative 'otto/core/file_safety'
|
|
31
|
-
require_relative 'otto/core/configuration'
|
|
32
|
-
require_relative 'otto/core/error_handler'
|
|
33
|
-
require_relative 'otto/core/uri_generator'
|
|
34
|
-
require_relative 'otto/core/middleware_stack'
|
|
35
|
-
require_relative 'otto/security/configurator'
|
|
20
|
+
require_relative 'otto/locale/config'
|
|
21
|
+
require_relative 'otto/mcp'
|
|
22
|
+
require_relative 'otto/core'
|
|
23
|
+
require_relative 'otto/privacy'
|
|
24
|
+
require_relative 'otto/security'
|
|
36
25
|
require_relative 'otto/utils'
|
|
26
|
+
require_relative 'otto/version'
|
|
37
27
|
|
|
38
28
|
# Otto is a simple Rack router that allows you to define routes in a file
|
|
39
29
|
# with built-in security features including CSRF protection, input validation,
|
|
@@ -56,38 +46,6 @@ require_relative 'otto/utils'
|
|
|
56
46
|
# otto.enable_csp!
|
|
57
47
|
# otto.enable_frame_protection!
|
|
58
48
|
#
|
|
59
|
-
# Configuration Data class to replace OpenStruct
|
|
60
|
-
# Configuration Data class to replace OpenStruct
|
|
61
|
-
# Configuration class to replace OpenStruct
|
|
62
|
-
class ConfigData
|
|
63
|
-
def initialize(**kwargs)
|
|
64
|
-
@data = kwargs
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
# Dynamic attribute accessors
|
|
68
|
-
def method_missing(method_name, *args)
|
|
69
|
-
if method_name.to_s.end_with?('=')
|
|
70
|
-
# Setter
|
|
71
|
-
attr_name = method_name.to_s.chomp('=').to_sym
|
|
72
|
-
@data[attr_name] = args.first
|
|
73
|
-
elsif @data.key?(method_name)
|
|
74
|
-
# Getter
|
|
75
|
-
@data[method_name]
|
|
76
|
-
else
|
|
77
|
-
super
|
|
78
|
-
end
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
def respond_to_missing?(method_name, include_private = false)
|
|
82
|
-
method_name.to_s.end_with?('=') || @data.key?(method_name) || super
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
# Convert to hash for compatibility
|
|
86
|
-
def to_h
|
|
87
|
-
@data.dup
|
|
88
|
-
end
|
|
89
|
-
end
|
|
90
|
-
|
|
91
49
|
class Otto
|
|
92
50
|
include Otto::Core::Router
|
|
93
51
|
include Otto::Core::FileSafety
|
|
@@ -103,25 +61,11 @@ class Otto
|
|
|
103
61
|
else
|
|
104
62
|
defined?(Otto::Utils) ? Otto::Utils.yes?(ENV.fetch('OTTO_DEBUG', nil)) : false
|
|
105
63
|
end
|
|
106
|
-
@logger
|
|
107
|
-
@global_config = nil
|
|
108
|
-
|
|
109
|
-
# Global configuration for all Otto instances (Ruby 3.2+ pattern matching)
|
|
110
|
-
def self.configure
|
|
111
|
-
config = case @global_config
|
|
112
|
-
in Hash => h
|
|
113
|
-
# Transform string keys to symbol keys for ConfigData compatibility
|
|
114
|
-
symbol_hash = h.transform_keys(&:to_sym)
|
|
115
|
-
ConfigData.new(**symbol_hash)
|
|
116
|
-
else
|
|
117
|
-
ConfigData.new
|
|
118
|
-
end
|
|
119
|
-
yield config
|
|
120
|
-
@global_config = config.to_h
|
|
121
|
-
end
|
|
64
|
+
@logger = Logger.new($stdout, Logger::INFO)
|
|
122
65
|
|
|
123
|
-
attr_reader :routes, :routes_literal, :routes_static, :route_definitions, :option,
|
|
124
|
-
:security_config, :locale_config, :auth_config,
|
|
66
|
+
attr_reader :routes, :routes_literal, :routes_static, :route_definitions, :option,
|
|
67
|
+
:static_route, :security_config, :locale_config, :auth_config,
|
|
68
|
+
:route_handler_factory, :mcp_server, :security, :middleware
|
|
125
69
|
attr_accessor :not_found, :server_error
|
|
126
70
|
|
|
127
71
|
def initialize(path = nil, opts = {})
|
|
@@ -132,27 +76,76 @@ class Otto
|
|
|
132
76
|
Otto.logger.debug "new Otto: #{opts}" if Otto.debug
|
|
133
77
|
load(path) unless path.nil?
|
|
134
78
|
super()
|
|
79
|
+
|
|
80
|
+
# Build the middleware app once after all initialization is complete
|
|
81
|
+
build_app!
|
|
82
|
+
|
|
83
|
+
# Configuration freezing is deferred until first request to support
|
|
84
|
+
# multi-step initialization (e.g., multi-app architectures).
|
|
85
|
+
# This allows adding auth strategies, middleware, etc. after Otto.new
|
|
86
|
+
# but before processing requests.
|
|
87
|
+
@freeze_mutex = Mutex.new
|
|
88
|
+
@configuration_frozen = false
|
|
135
89
|
end
|
|
136
90
|
alias options option
|
|
137
91
|
|
|
138
92
|
# Main Rack application interface
|
|
139
93
|
def call(env)
|
|
140
|
-
#
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
94
|
+
# Freeze configuration on first request (thread-safe)
|
|
95
|
+
# Skip in test environment to allow test flexibility
|
|
96
|
+
unless defined?(RSpec) || @configuration_frozen
|
|
97
|
+
Otto.logger.debug '[Otto] Lazy freezing check: configuration not yet frozen' if Otto.debug
|
|
98
|
+
|
|
99
|
+
@freeze_mutex.synchronize do
|
|
100
|
+
unless @configuration_frozen
|
|
101
|
+
Otto.logger.info '[Otto] Freezing configuration on first request (lazy freeze)'
|
|
102
|
+
freeze_configuration!
|
|
103
|
+
@configuration_frozen = true
|
|
104
|
+
Otto.logger.debug '[Otto] Configuration frozen successfully' if Otto.debug
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
145
108
|
|
|
146
109
|
begin
|
|
147
|
-
app
|
|
110
|
+
# Use pre-built middleware app (built once at initialization)
|
|
111
|
+
@app.call(env)
|
|
148
112
|
rescue StandardError => e
|
|
149
113
|
handle_error(e, env)
|
|
150
114
|
end
|
|
151
115
|
end
|
|
152
116
|
|
|
117
|
+
# Builds the middleware application chain
|
|
118
|
+
# Called once at initialization and whenever middleware stack changes
|
|
119
|
+
#
|
|
120
|
+
# IMPORTANT: If you have routes with auth requirements, you MUST add session
|
|
121
|
+
# middleware to your middleware stack BEFORE Otto processes requests.
|
|
122
|
+
#
|
|
123
|
+
# Session middleware is required for RouteAuthWrapper to correctly persist
|
|
124
|
+
# session changes during authentication. Common options include:
|
|
125
|
+
# - Rack::Session::Cookie (requires rack-session gem)
|
|
126
|
+
# - Rack::Session::Pool
|
|
127
|
+
# - Rack::Session::Memcache
|
|
128
|
+
# - Any Rack-compatible session middleware
|
|
129
|
+
#
|
|
130
|
+
# Example:
|
|
131
|
+
# use Rack::Session::Cookie, secret: ENV['SESSION_SECRET']
|
|
132
|
+
# otto = Otto.new('routes.txt')
|
|
133
|
+
#
|
|
134
|
+
def build_app!
|
|
135
|
+
base_app = method(:handle_request)
|
|
136
|
+
@app = @middleware.wrap(base_app, @security_config)
|
|
137
|
+
end
|
|
138
|
+
|
|
153
139
|
# Middleware Management
|
|
154
140
|
def use(middleware, ...)
|
|
141
|
+
ensure_not_frozen!
|
|
155
142
|
@middleware.add(middleware, ...)
|
|
143
|
+
|
|
144
|
+
# NOTE: If build_app! is triggered during a request (via use() or
|
|
145
|
+
# middleware_stack=), the @app instance variable could be swapped
|
|
146
|
+
# mid-request in a multi-threaded environment.
|
|
147
|
+
|
|
148
|
+
build_app! if @app # Rebuild app if already initialized
|
|
156
149
|
end
|
|
157
150
|
|
|
158
151
|
# Compatibility method for existing tests
|
|
@@ -164,6 +157,7 @@ class Otto
|
|
|
164
157
|
def middleware_stack=(stack)
|
|
165
158
|
@middleware.clear!
|
|
166
159
|
Array(stack).each { |middleware| @middleware.add(middleware) }
|
|
160
|
+
build_app! if @app # Rebuild app if already initialized
|
|
167
161
|
end
|
|
168
162
|
|
|
169
163
|
# Compatibility method for middleware detection
|
|
@@ -180,6 +174,7 @@ class Otto
|
|
|
180
174
|
# @example
|
|
181
175
|
# otto.enable_csrf_protection!
|
|
182
176
|
def enable_csrf_protection!
|
|
177
|
+
ensure_not_frozen!
|
|
183
178
|
return if @middleware.includes?(Otto::Security::Middleware::CSRFMiddleware)
|
|
184
179
|
|
|
185
180
|
@security_config.enable_csrf_protection!
|
|
@@ -192,6 +187,7 @@ class Otto
|
|
|
192
187
|
# @example
|
|
193
188
|
# otto.enable_request_validation!
|
|
194
189
|
def enable_request_validation!
|
|
190
|
+
ensure_not_frozen!
|
|
195
191
|
return if @middleware.includes?(Otto::Security::Middleware::ValidationMiddleware)
|
|
196
192
|
|
|
197
193
|
@security_config.input_validation = true
|
|
@@ -207,6 +203,7 @@ class Otto
|
|
|
207
203
|
# @example
|
|
208
204
|
# otto.enable_rate_limiting!(requests_per_minute: 50)
|
|
209
205
|
def enable_rate_limiting!(options = {})
|
|
206
|
+
ensure_not_frozen!
|
|
210
207
|
return if @middleware.includes?(Otto::Security::Middleware::RateLimitMiddleware)
|
|
211
208
|
|
|
212
209
|
@security.configure_rate_limiting(options)
|
|
@@ -223,7 +220,7 @@ class Otto
|
|
|
223
220
|
# @example
|
|
224
221
|
# otto.add_rate_limit_rule('uploads', limit: 5, period: 300, condition: ->(req) { req.post? && req.path.include?('upload') })
|
|
225
222
|
def add_rate_limit_rule(name, options)
|
|
226
|
-
|
|
223
|
+
ensure_not_frozen!
|
|
227
224
|
@security_config.rate_limiting_config[:custom_rules][name.to_s] = options
|
|
228
225
|
end
|
|
229
226
|
|
|
@@ -235,6 +232,7 @@ class Otto
|
|
|
235
232
|
# otto.add_trusted_proxy('10.0.0.0/8')
|
|
236
233
|
# otto.add_trusted_proxy(/^172\.16\./)
|
|
237
234
|
def add_trusted_proxy(proxy)
|
|
235
|
+
ensure_not_frozen!
|
|
238
236
|
@security_config.add_trusted_proxy(proxy)
|
|
239
237
|
end
|
|
240
238
|
|
|
@@ -248,6 +246,7 @@ class Otto
|
|
|
248
246
|
# 'strict-transport-security' => 'max-age=31536000'
|
|
249
247
|
# })
|
|
250
248
|
def set_security_headers(headers)
|
|
249
|
+
ensure_not_frozen!
|
|
251
250
|
@security_config.security_headers.merge!(headers)
|
|
252
251
|
end
|
|
253
252
|
|
|
@@ -260,6 +259,7 @@ class Otto
|
|
|
260
259
|
# @example
|
|
261
260
|
# otto.enable_hsts!(max_age: 86400, include_subdomains: false)
|
|
262
261
|
def enable_hsts!(max_age: 31_536_000, include_subdomains: true)
|
|
262
|
+
ensure_not_frozen!
|
|
263
263
|
@security_config.enable_hsts!(max_age: max_age, include_subdomains: include_subdomains)
|
|
264
264
|
end
|
|
265
265
|
|
|
@@ -270,6 +270,7 @@ class Otto
|
|
|
270
270
|
# @example
|
|
271
271
|
# otto.enable_csp!("default-src 'self'; script-src 'self' 'unsafe-inline'")
|
|
272
272
|
def enable_csp!(policy = "default-src 'self'")
|
|
273
|
+
ensure_not_frozen!
|
|
273
274
|
@security_config.enable_csp!(policy)
|
|
274
275
|
end
|
|
275
276
|
|
|
@@ -279,6 +280,7 @@ class Otto
|
|
|
279
280
|
# @example
|
|
280
281
|
# otto.enable_frame_protection!('DENY')
|
|
281
282
|
def enable_frame_protection!(option = 'SAMEORIGIN')
|
|
283
|
+
ensure_not_frozen!
|
|
282
284
|
@security_config.enable_frame_protection!(option)
|
|
283
285
|
end
|
|
284
286
|
|
|
@@ -289,20 +291,10 @@ class Otto
|
|
|
289
291
|
# @example
|
|
290
292
|
# otto.enable_csp_with_nonce!(debug: true)
|
|
291
293
|
def enable_csp_with_nonce!(debug: false)
|
|
294
|
+
ensure_not_frozen!
|
|
292
295
|
@security_config.enable_csp_with_nonce!(debug: debug)
|
|
293
296
|
end
|
|
294
297
|
|
|
295
|
-
# Enable authentication middleware for route-level access control.
|
|
296
|
-
# This will automatically check route auth parameters and enforce authentication.
|
|
297
|
-
#
|
|
298
|
-
# @example
|
|
299
|
-
# otto.enable_authentication!
|
|
300
|
-
def enable_authentication!
|
|
301
|
-
return if @middleware.includes?(Otto::Security::Authentication::AuthenticationMiddleware)
|
|
302
|
-
|
|
303
|
-
use Otto::Security::Authentication::AuthenticationMiddleware, @auth_config
|
|
304
|
-
end
|
|
305
|
-
|
|
306
298
|
# Add a single authentication strategy
|
|
307
299
|
#
|
|
308
300
|
# @param name [String] Strategy name
|
|
@@ -310,12 +302,83 @@ class Otto
|
|
|
310
302
|
# @example
|
|
311
303
|
# otto.add_auth_strategy('custom', MyCustomStrategy.new)
|
|
312
304
|
def add_auth_strategy(name, strategy)
|
|
305
|
+
ensure_not_frozen!
|
|
313
306
|
# Ensure auth_config is initialized (handles edge case where it might be nil)
|
|
314
|
-
@auth_config = { auth_strategies: {}, default_auth_strategy: '
|
|
307
|
+
@auth_config = { auth_strategies: {}, default_auth_strategy: 'noauth' } if @auth_config.nil?
|
|
315
308
|
|
|
316
309
|
@auth_config[:auth_strategies][name] = strategy
|
|
310
|
+
end
|
|
317
311
|
|
|
318
|
-
|
|
312
|
+
# Disable IP privacy to access original IP addresses
|
|
313
|
+
#
|
|
314
|
+
# IMPORTANT: By default, Otto masks public IP addresses for privacy.
|
|
315
|
+
# Private/localhost IPs (127.0.0.0/8, 10.0.0.0/8, etc.) are never masked.
|
|
316
|
+
# Only disable this if you need access to original public IPs.
|
|
317
|
+
#
|
|
318
|
+
# When disabled:
|
|
319
|
+
# - env['REMOTE_ADDR'] contains the real IP address
|
|
320
|
+
# - env['otto.original_ip'] also contains the real IP
|
|
321
|
+
# - No PrivateFingerprint is created
|
|
322
|
+
#
|
|
323
|
+
# @example
|
|
324
|
+
# otto.disable_ip_privacy!
|
|
325
|
+
def disable_ip_privacy!
|
|
326
|
+
ensure_not_frozen!
|
|
327
|
+
@security_config.ip_privacy_config.disable!
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
# Enable full IP privacy (mask ALL IPs including private/localhost)
|
|
332
|
+
#
|
|
333
|
+
# By default, Otto exempts private and localhost IPs from masking for
|
|
334
|
+
# better development experience. Call this method to mask ALL IPs
|
|
335
|
+
# regardless of type.
|
|
336
|
+
#
|
|
337
|
+
# @example Enable full privacy (mask all IPs)
|
|
338
|
+
# otto = Otto.new(routes_file)
|
|
339
|
+
# otto.enable_full_ip_privacy!
|
|
340
|
+
# # Now 127.0.0.1 → 127.0.0.0, 192.168.1.100 → 192.168.1.0
|
|
341
|
+
#
|
|
342
|
+
# @return [void]
|
|
343
|
+
# @raise [FrozenError] if called after configuration is frozen
|
|
344
|
+
def enable_full_ip_privacy!
|
|
345
|
+
ensure_not_frozen!
|
|
346
|
+
@security_config.ip_privacy_config.mask_private_ips = true
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
# Configure IP privacy settings
|
|
350
|
+
#
|
|
351
|
+
# Privacy is enabled by default. Use this method to customize privacy
|
|
352
|
+
# behavior without disabling it entirely.
|
|
353
|
+
#
|
|
354
|
+
# @param octet_precision [Integer] Number of octets to mask (1 or 2, default: 1)
|
|
355
|
+
# @param hash_rotation [Integer] Seconds between key rotation (default: 86400)
|
|
356
|
+
# @param geo [Boolean] Enable geo-location resolution (default: true)
|
|
357
|
+
# @param redis [Redis] Redis connection for multi-server atomic key generation
|
|
358
|
+
#
|
|
359
|
+
# @example Mask 2 octets instead of 1
|
|
360
|
+
# otto.configure_ip_privacy(octet_precision: 2)
|
|
361
|
+
#
|
|
362
|
+
# @example Disable geo-location
|
|
363
|
+
# otto.configure_ip_privacy(geo: false)
|
|
364
|
+
#
|
|
365
|
+
# @example Custom hash rotation
|
|
366
|
+
# otto.configure_ip_privacy(hash_rotation: 24.hours)
|
|
367
|
+
#
|
|
368
|
+
# @example Multi-server with Redis
|
|
369
|
+
# redis = Redis.new(url: ENV['REDIS_URL'])
|
|
370
|
+
# otto.configure_ip_privacy(redis: redis)
|
|
371
|
+
def configure_ip_privacy(octet_precision: nil, hash_rotation: nil, geo: nil, redis: nil)
|
|
372
|
+
ensure_not_frozen!
|
|
373
|
+
config = @security_config.ip_privacy_config
|
|
374
|
+
|
|
375
|
+
config.octet_precision = octet_precision if octet_precision
|
|
376
|
+
config.hash_rotation_period = hash_rotation if hash_rotation
|
|
377
|
+
config.geo_enabled = geo unless geo.nil?
|
|
378
|
+
config.instance_variable_set(:@redis, redis) if redis
|
|
379
|
+
|
|
380
|
+
# Validate configuration
|
|
381
|
+
config.validate!
|
|
319
382
|
end
|
|
320
383
|
|
|
321
384
|
# Enable MCP (Model Context Protocol) server support
|
|
@@ -327,6 +390,7 @@ class Otto
|
|
|
327
390
|
# @example
|
|
328
391
|
# otto.enable_mcp!(http: true, endpoint: '/api/mcp')
|
|
329
392
|
def enable_mcp!(options = {})
|
|
393
|
+
ensure_not_frozen!
|
|
330
394
|
@mcp_server ||= Otto::MCP::Server.new(self)
|
|
331
395
|
|
|
332
396
|
@mcp_server.enable!(options)
|
|
@@ -349,8 +413,16 @@ class Otto
|
|
|
349
413
|
@security_config = Otto::Security::Config.new
|
|
350
414
|
@middleware = Otto::Core::MiddlewareStack.new
|
|
351
415
|
# Initialize @auth_config first so it can be shared with the configurator
|
|
352
|
-
@auth_config = { auth_strategies: {}, default_auth_strategy: '
|
|
416
|
+
@auth_config = { auth_strategies: {}, default_auth_strategy: 'noauth' }
|
|
353
417
|
@security = Otto::Security::Configurator.new(@security_config, @middleware, @auth_config)
|
|
418
|
+
@app = nil # Pre-built middleware app (built after initialization)
|
|
419
|
+
|
|
420
|
+
# Add IP Privacy middleware first in stack (privacy by default for public IPs)
|
|
421
|
+
# Private/localhost IPs are automatically exempted from masking
|
|
422
|
+
@middleware.add_with_position(
|
|
423
|
+
Otto::Security::Middleware::IPPrivacyMiddleware,
|
|
424
|
+
position: :first
|
|
425
|
+
)
|
|
354
426
|
end
|
|
355
427
|
|
|
356
428
|
def initialize_options(_path, opts)
|
|
@@ -376,7 +448,7 @@ class Otto
|
|
|
376
448
|
end
|
|
377
449
|
|
|
378
450
|
class << self
|
|
379
|
-
attr_accessor :debug, :logger
|
|
451
|
+
attr_accessor :debug, :logger # rubocop:disable ThreadSafety/ClassAndModuleAttributes
|
|
380
452
|
end
|
|
381
453
|
|
|
382
454
|
# Class methods for Otto framework providing singleton access and configuration
|
|
@@ -401,6 +473,28 @@ class Otto
|
|
|
401
473
|
def env? *guesses
|
|
402
474
|
!guesses.flatten.select { |n| ENV['RACK_ENV'].to_s == n.to_s }.empty?
|
|
403
475
|
end
|
|
476
|
+
|
|
477
|
+
# Test-only method to unfreeze Otto configuration
|
|
478
|
+
#
|
|
479
|
+
# This method resets the @configuration_frozen flag, allowing tests
|
|
480
|
+
# to bypass the ensure_not_frozen! check. It does NOT actually unfreeze
|
|
481
|
+
# Ruby objects (which is impossible once frozen).
|
|
482
|
+
#
|
|
483
|
+
# IMPORTANT: Only works when RSpec is defined. Raises an error otherwise
|
|
484
|
+
# to prevent accidental use in production.
|
|
485
|
+
#
|
|
486
|
+
# @param otto [Otto] The Otto instance to unfreeze
|
|
487
|
+
# @return [Otto] The unfrozen Otto instance
|
|
488
|
+
# @raise [RuntimeError] if RSpec is not defined (not in test environment)
|
|
489
|
+
# @api private
|
|
490
|
+
def unfreeze_for_testing(otto)
|
|
491
|
+
unless defined?(RSpec)
|
|
492
|
+
raise 'Otto.unfreeze_for_testing is only available in RSpec test environment'
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
otto.instance_variable_set(:@configuration_frozen, false)
|
|
496
|
+
otto
|
|
497
|
+
end
|
|
404
498
|
end
|
|
405
499
|
extend ClassMethods
|
|
406
500
|
end
|
data/otto.gemspec
CHANGED
|
@@ -16,6 +16,11 @@ Gem::Specification.new do |spec|
|
|
|
16
16
|
|
|
17
17
|
spec.required_ruby_version = ['>= 3.2', '< 4.0']
|
|
18
18
|
|
|
19
|
+
spec.add_dependency 'ipaddr', '~> 1', '< 2.0'
|
|
20
|
+
spec.add_dependency 'concurrent-ruby', '~> 1.3', '< 2.0'
|
|
21
|
+
|
|
22
|
+
# Logger is not part of the default gems as of Ruby 3.5.0
|
|
23
|
+
spec.add_dependency 'logger', '~> 1', '< 2.0'
|
|
19
24
|
|
|
20
25
|
spec.add_dependency 'rack', '~> 3.1', '< 4.0'
|
|
21
26
|
spec.add_dependency 'rack-parser', '~> 0.7'
|
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,66 @@ 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'
|
|
52
|
+
- !ruby/object:Gem::Dependency
|
|
53
|
+
name: logger
|
|
54
|
+
requirement: !ruby/object:Gem::Requirement
|
|
55
|
+
requirements:
|
|
56
|
+
- - "~>"
|
|
57
|
+
- !ruby/object:Gem::Version
|
|
58
|
+
version: '1'
|
|
59
|
+
- - "<"
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '2.0'
|
|
62
|
+
type: :runtime
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - "~>"
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '1'
|
|
69
|
+
- - "<"
|
|
70
|
+
- !ruby/object:Gem::Version
|
|
71
|
+
version: '2.0'
|
|
12
72
|
- !ruby/object:Gem::Dependency
|
|
13
73
|
name: rack
|
|
14
74
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -106,14 +166,16 @@ files:
|
|
|
106
166
|
- Gemfile.lock
|
|
107
167
|
- LICENSE.txt
|
|
108
168
|
- README.md
|
|
169
|
+
- benchmark_middleware_wrap.rb
|
|
109
170
|
- bin/rspec
|
|
110
|
-
- changelog.d/
|
|
111
|
-
- changelog.d/
|
|
112
|
-
- changelog.d/20250912_175625_claude_delano_remove_ostruct.rst
|
|
171
|
+
- changelog.d/20251014_144317_delano_54_thats_a_wrapper.rst
|
|
172
|
+
- changelog.d/20251014_161526_delano_54_thats_a_wrapper.rst
|
|
113
173
|
- changelog.d/README.md
|
|
114
174
|
- changelog.d/scriv.ini
|
|
115
175
|
- docs/.gitignore
|
|
176
|
+
- docs/ipaddr-encoding-quirk.md
|
|
116
177
|
- docs/migrating/v2.0.0-pre1.md
|
|
178
|
+
- docs/migrating/v2.0.0-pre2.md
|
|
117
179
|
- examples/.gitignore
|
|
118
180
|
- examples/advanced_routes/README.md
|
|
119
181
|
- examples/advanced_routes/app.rb
|
|
@@ -168,24 +230,35 @@ files:
|
|
|
168
230
|
- examples/security_features/config.ru
|
|
169
231
|
- examples/security_features/routes
|
|
170
232
|
- lib/otto.rb
|
|
233
|
+
- lib/otto/core.rb
|
|
171
234
|
- lib/otto/core/configuration.rb
|
|
172
235
|
- lib/otto/core/error_handler.rb
|
|
173
236
|
- lib/otto/core/file_safety.rb
|
|
237
|
+
- lib/otto/core/freezable.rb
|
|
174
238
|
- lib/otto/core/middleware_stack.rb
|
|
175
239
|
- lib/otto/core/router.rb
|
|
176
240
|
- lib/otto/core/uri_generator.rb
|
|
177
241
|
- lib/otto/design_system.rb
|
|
242
|
+
- lib/otto/env_keys.rb
|
|
243
|
+
- lib/otto/helpers.rb
|
|
178
244
|
- lib/otto/helpers/base.rb
|
|
179
245
|
- lib/otto/helpers/request.rb
|
|
180
246
|
- lib/otto/helpers/response.rb
|
|
181
247
|
- lib/otto/helpers/validation.rb
|
|
248
|
+
- lib/otto/locale/config.rb
|
|
249
|
+
- lib/otto/mcp.rb
|
|
182
250
|
- lib/otto/mcp/auth/token.rb
|
|
183
251
|
- lib/otto/mcp/protocol.rb
|
|
184
252
|
- lib/otto/mcp/rate_limiting.rb
|
|
185
253
|
- lib/otto/mcp/registry.rb
|
|
186
254
|
- lib/otto/mcp/route_parser.rb
|
|
255
|
+
- lib/otto/mcp/schema_validation.rb
|
|
187
256
|
- lib/otto/mcp/server.rb
|
|
188
|
-
- lib/otto/
|
|
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
|
|
189
262
|
- lib/otto/response_handlers.rb
|
|
190
263
|
- lib/otto/response_handlers/auto.rb
|
|
191
264
|
- lib/otto/response_handlers/base.rb
|
|
@@ -203,13 +276,14 @@ files:
|
|
|
203
276
|
- lib/otto/route_handlers/instance_method.rb
|
|
204
277
|
- lib/otto/route_handlers/lambda.rb
|
|
205
278
|
- lib/otto/route_handlers/logic_class.rb
|
|
279
|
+
- lib/otto/security.rb
|
|
206
280
|
- lib/otto/security/authentication.rb
|
|
281
|
+
- lib/otto/security/authentication/auth_failure.rb
|
|
207
282
|
- lib/otto/security/authentication/auth_strategy.rb
|
|
208
|
-
- lib/otto/security/authentication/
|
|
209
|
-
- lib/otto/security/authentication/failure_result.rb
|
|
283
|
+
- lib/otto/security/authentication/route_auth_wrapper.rb
|
|
210
284
|
- lib/otto/security/authentication/strategies/api_key_strategy.rb
|
|
285
|
+
- lib/otto/security/authentication/strategies/noauth_strategy.rb
|
|
211
286
|
- lib/otto/security/authentication/strategies/permission_strategy.rb
|
|
212
|
-
- lib/otto/security/authentication/strategies/public_strategy.rb
|
|
213
287
|
- lib/otto/security/authentication/strategies/role_strategy.rb
|
|
214
288
|
- lib/otto/security/authentication/strategies/session_strategy.rb
|
|
215
289
|
- lib/otto/security/authentication/strategy_result.rb
|
|
@@ -217,6 +291,7 @@ files:
|
|
|
217
291
|
- lib/otto/security/configurator.rb
|
|
218
292
|
- lib/otto/security/csrf.rb
|
|
219
293
|
- lib/otto/security/middleware/csrf_middleware.rb
|
|
294
|
+
- lib/otto/security/middleware/ip_privacy_middleware.rb
|
|
220
295
|
- lib/otto/security/middleware/rate_limit_middleware.rb
|
|
221
296
|
- lib/otto/security/middleware/validation_middleware.rb
|
|
222
297
|
- lib/otto/security/rate_limiter.rb
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
Added
|
|
2
|
-
-----
|
|
3
|
-
|
|
4
|
-
- ``Otto::RequestContext`` Data class providing immutable, structured authentication context for Logic classes
|
|
5
|
-
- Helper methods ``authenticated?``, ``has_role?``, ``has_permission?``, ``user_name``, ``session_id`` for cleaner Logic class implementation
|
|
6
|
-
- Factory methods for creating RequestContext from AuthResult or anonymous contexts
|
|
7
|
-
|
|
8
|
-
Changed
|
|
9
|
-
-------
|
|
10
|
-
|
|
11
|
-
- **BREAKING**: Logic class constructor signature changed from ``initialize(session, user, params, locale)`` to ``initialize(context, params, locale)``
|
|
12
|
-
- Logic classes now receive immutable RequestContext instead of separate session/user parameters
|
|
13
|
-
- LogicClassHandler simplified to single arity pattern, removing backward compatibility code
|
|
14
|
-
- Authentication middleware now creates RequestContext instances for all requests
|
|
15
|
-
|
|
16
|
-
Documentation
|
|
17
|
-
-------------
|
|
18
|
-
|
|
19
|
-
- Updated migration guide with comprehensive RequestContext examples and step-by-step conversion instructions
|
|
20
|
-
- Updated Logic class examples in advanced_routes and authentication_strategies to demonstrate new pattern
|
|
21
|
-
- Enhanced documentation with RequestContext API reference and helper method examples
|
|
22
|
-
|
|
23
|
-
AI Assistance
|
|
24
|
-
-------------
|
|
25
|
-
|
|
26
|
-
- RequestContext Data class design developed with AI architectural guidance for immutability and clean API
|
|
27
|
-
- Comprehensive migration of all example Logic classes with AI assistance for consistency and best practices
|
|
28
|
-
- Documentation improvements ensuring clarity of breaking changes and migration path
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
Changed
|
|
2
|
-
-------
|
|
3
|
-
|
|
4
|
-
- Replaced `RequestContext` with `StrategyResult` class for better authentication handling
|
|
5
|
-
- Simplified authentication strategy API to return `StrategyResult` or `nil` for success/failure
|
|
6
|
-
- Enhanced route handlers to support JSON request body parsing
|
|
7
|
-
- Updated authentication middleware to use `StrategyResult` throughout
|
|
8
|
-
|
|
9
|
-
Added
|
|
10
|
-
-----
|
|
11
|
-
|
|
12
|
-
- Added `StrategyResult` class with improved user model compatibility and cleaner API
|
|
13
|
-
- Added JSON request body parsing support in Logic class handlers
|
|
14
|
-
|
|
15
|
-
Removed
|
|
16
|
-
-------
|
|
17
|
-
|
|
18
|
-
- Removed `RequestContext` class (replaced by `StrategyResult`)
|
|
19
|
-
- Removed `AuthResult` class from authentication system
|
|
20
|
-
- Removed OpenStruct dependency across the framework
|
|
21
|
-
- Removed `ConcurrentCacheStore` example class for an ActiveSupport::Cache::MemoryStore-compatible interface with Rack::Attack
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
Changed
|
|
2
|
-
-------
|
|
3
|
-
|
|
4
|
-
- Reorganized Otto security module structure for better maintainability and separation of concerns
|
|
5
|
-
- Moved authentication strategies to ``Otto::Security::Authentication::Strategies`` namespace
|
|
6
|
-
- Moved security middleware to ``Otto::Security::Middleware`` namespace
|
|
7
|
-
- Moved ``StrategyResult`` and ``FailureResult`` to ``Otto::Security::Authentication`` namespace
|
|
8
|
-
|
|
9
|
-
Added
|
|
10
|
-
-----
|
|
11
|
-
|
|
12
|
-
- Added new modular directory structure under ``lib/otto/security/``
|
|
13
|
-
- Added backward compatibility aliases to maintain existing API compatibility
|
|
14
|
-
- Added proper namespacing for authentication components and middleware classes
|
|
15
|
-
|
|
16
|
-
AI Assistance
|
|
17
|
-
-------------
|
|
18
|
-
|
|
19
|
-
- Comprehensive security module reorganization with systematic namespace restructuring
|
|
20
|
-
- Automated test validation to ensure backward compatibility during refactoring
|
|
21
|
-
- Intelligent file organization following Ruby conventions and single responsibility principles
|