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
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
# Otto v2.0.0-pre2 Migration Guide
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
This release resolves critical architectural issues with `StrategyResult` semantics and removes deprecated methods. The changes clarify the distinction between "request state" (user in session) and "authentication outcomes" (auth attempt just succeeded).
|
|
6
|
+
|
|
7
|
+
## Breaking Changes
|
|
8
|
+
|
|
9
|
+
### 1. StrategyResult Methods Removed
|
|
10
|
+
|
|
11
|
+
**Removed Methods:**
|
|
12
|
+
- `StrategyResult#success?` - Always returned `true`, meaningless
|
|
13
|
+
- `StrategyResult#failure?` - Always returned `false`, meaningless
|
|
14
|
+
- `FailureResult#success?` - Always returned `false`
|
|
15
|
+
- `FailureResult#failure?` - Always returned `true`
|
|
16
|
+
|
|
17
|
+
**Migration:**
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
# Before - checking success/failure
|
|
21
|
+
if strategy_result&.success?
|
|
22
|
+
# handle success
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# After - type checking
|
|
26
|
+
if strategy_result.is_a?(Otto::Security::Authentication::StrategyResult)
|
|
27
|
+
# handle success
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### 2. New Semantic Distinction
|
|
33
|
+
|
|
34
|
+
**New Method:**
|
|
35
|
+
- `StrategyResult#auth_attempt_succeeded?` - Returns `true` only when auth strategy just executed successfully
|
|
36
|
+
|
|
37
|
+
**Key Semantic Difference:**
|
|
38
|
+
|
|
39
|
+
| Method | Meaning | Use Case |
|
|
40
|
+
|--------|---------|----------|
|
|
41
|
+
| `authenticated?` | User in session (request state) | Check if session has user |
|
|
42
|
+
| `auth_attempt_succeeded?` | Auth strategy just succeeded (auth outcome) | Post-login redirects, analytics |
|
|
43
|
+
|
|
44
|
+
**Migration Examples:**
|
|
45
|
+
|
|
46
|
+
#### Registration Flow (IMPORTANT)
|
|
47
|
+
|
|
48
|
+
```ruby
|
|
49
|
+
# Before - BROKEN - blocks legitimate registration
|
|
50
|
+
class CreateAccount < Logic::Base
|
|
51
|
+
def raise_concerns
|
|
52
|
+
# This was always true if user in session, blocking registration
|
|
53
|
+
raise OT::FormError, "Already signed up" if @strategy_result.success?
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# After - CORRECT
|
|
58
|
+
class CreateAccount < Logic::Base
|
|
59
|
+
def raise_concerns
|
|
60
|
+
# Check if user already in session
|
|
61
|
+
raise OT::FormError, "Already signed up" if @strategy_result.authenticated?
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
#### Post-Login Redirect
|
|
67
|
+
|
|
68
|
+
```ruby
|
|
69
|
+
# Before - unreliable
|
|
70
|
+
class AuthController
|
|
71
|
+
def authenticate
|
|
72
|
+
if @strategy_result.success? # Always true, not helpful
|
|
73
|
+
redirect_to dashboard_path
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# After - correct semantic
|
|
79
|
+
class AuthController
|
|
80
|
+
def authenticate
|
|
81
|
+
if @strategy_result.auth_attempt_succeeded?
|
|
82
|
+
# Only redirect when auth route just succeeded
|
|
83
|
+
redirect_to dashboard_path
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Non-Breaking Enhancements
|
|
90
|
+
|
|
91
|
+
### 1. Comprehensive Documentation
|
|
92
|
+
|
|
93
|
+
`StrategyResult` now includes extensive inline documentation:
|
|
94
|
+
- Usage patterns and creation guidelines
|
|
95
|
+
- Session contract for multi-app architectures
|
|
96
|
+
- Examples for common scenarios
|
|
97
|
+
- Clear distinction between request state and auth outcomes
|
|
98
|
+
|
|
99
|
+
### 2. Session Contract (Multi-App Architectures)
|
|
100
|
+
|
|
101
|
+
For shared session architectures (Auth app + Core app):
|
|
102
|
+
|
|
103
|
+
**Required session keys for authenticated state:**
|
|
104
|
+
```ruby
|
|
105
|
+
session['authenticated'] # Boolean flag
|
|
106
|
+
session['identity_id'] # User/customer ID
|
|
107
|
+
session['authenticated_at'] # Timestamp
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
**Optional session keys:**
|
|
111
|
+
```ruby
|
|
112
|
+
session['email'] # User email
|
|
113
|
+
session['ip_address'] # Client IP (masked by default via IPPrivacyMiddleware)
|
|
114
|
+
session['user_agent'] # Client UA
|
|
115
|
+
session['locale'] # User locale
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
**Advanced mode adds:**
|
|
119
|
+
```ruby
|
|
120
|
+
session['account_external_id'] # Rodauth external_id
|
|
121
|
+
session['advanced_account_id'] # Rodauth account ID
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Application Code Updates Required
|
|
125
|
+
|
|
126
|
+
### 1. Remove Manual StrategyResult Creation
|
|
127
|
+
|
|
128
|
+
**Anti-pattern identified:**
|
|
129
|
+
```ruby
|
|
130
|
+
# BAD - Bypasses Otto's auth_method tracking
|
|
131
|
+
class Controller::Base
|
|
132
|
+
def _strategy_result
|
|
133
|
+
Otto::Security::Authentication::StrategyResult.new(
|
|
134
|
+
session: session,
|
|
135
|
+
user: cust,
|
|
136
|
+
auth_method: 'session', # Hardcoded - loses semantic meaning
|
|
137
|
+
metadata: { ip: req.masked_ip } # Uses masked IP (privacy by default)
|
|
138
|
+
)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
**Correct approach:**
|
|
144
|
+
```ruby
|
|
145
|
+
# GOOD - Use RouteAuthWrapper-provided result
|
|
146
|
+
class Controller::Base
|
|
147
|
+
def strategy_result
|
|
148
|
+
req.env['otto.strategy_result'] # Created by RouteAuthWrapper
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Or for non-auth checks, use session directly
|
|
153
|
+
class Controller::Base
|
|
154
|
+
def current_user
|
|
155
|
+
return nil unless session['authenticated']
|
|
156
|
+
Customer.find(session['identity_id'])
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### 2. Update Logic Classes
|
|
162
|
+
|
|
163
|
+
**Pattern to check for:**
|
|
164
|
+
```ruby
|
|
165
|
+
# Search your codebase for these patterns:
|
|
166
|
+
grep -r "@strategy_result.success?" apps/
|
|
167
|
+
grep -r "@context.success?" apps/
|
|
168
|
+
grep -r "strategy_result&.success?" apps/
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
**Update to:**
|
|
172
|
+
- Use `authenticated?` for "user in session" checks (registration, profile access, etc.)
|
|
173
|
+
- Use `auth_attempt_succeeded?` for "just logged in" checks (redirects, welcome messages, etc.)
|
|
174
|
+
|
|
175
|
+
### 3. Test Updates
|
|
176
|
+
|
|
177
|
+
**RSpec matchers:**
|
|
178
|
+
```ruby
|
|
179
|
+
# Before
|
|
180
|
+
expect(result).to be_success
|
|
181
|
+
expect(result).to be_failure
|
|
182
|
+
|
|
183
|
+
# After
|
|
184
|
+
expect(result).to be_a(Otto::Security::Authentication::StrategyResult)
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Architecture Clarifications
|
|
188
|
+
|
|
189
|
+
### When StrategyResult is Created
|
|
190
|
+
|
|
191
|
+
1. **Routes WITH `auth=...` requirement:**
|
|
192
|
+
- RouteAuthWrapper executes strategy
|
|
193
|
+
- Always returns `StrategyResult` (success or failure)
|
|
194
|
+
- RouteAuthWrapper returns 401/302 response on `AuthFailure`
|
|
195
|
+
|
|
196
|
+
2. **Routes WITHOUT `auth=...` requirement:**
|
|
197
|
+
- No RouteAuthWrapper wrapping
|
|
198
|
+
- No `StrategyResult` created (routes without auth don't need it)
|
|
199
|
+
|
|
200
|
+
3. **Auth app (Roda) routes:**
|
|
201
|
+
- Manually creates `StrategyResult` for Logic class compatibility
|
|
202
|
+
- Same interface as Otto controllers
|
|
203
|
+
|
|
204
|
+
### Integration Boundaries
|
|
205
|
+
|
|
206
|
+
**Multi-app setup (Auth + Core + API):**
|
|
207
|
+
- **Shared:** Session middleware, Redis session, Logic classes, Customer model
|
|
208
|
+
- **Auth app:** Creates StrategyResult manually, uses Roda routing
|
|
209
|
+
- **Core/API apps:** StrategyResult from RouteAuthWrapper
|
|
210
|
+
- **Integration:** Pure session-based, no direct code calls between apps
|
|
211
|
+
|
|
212
|
+
## Testing Your Migration
|
|
213
|
+
|
|
214
|
+
### 1. Registration Flow Test
|
|
215
|
+
|
|
216
|
+
```ruby
|
|
217
|
+
describe "CreateAccount" do
|
|
218
|
+
it "blocks registration when user already authenticated" do
|
|
219
|
+
strategy_result = Otto::Security::Authentication::StrategyResult.new(
|
|
220
|
+
session: { user_id: 123 },
|
|
221
|
+
user: { id: 123 },
|
|
222
|
+
auth_method: 'anonymous', # No auth route, but user in session
|
|
223
|
+
metadata: {}
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
logic = CreateAccount.new(strategy_result, params, 'en')
|
|
227
|
+
|
|
228
|
+
expect { logic.raise_concerns }.to raise_error(OT::FormError, /Already signed up/)
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### 2. Auth Attempt Test
|
|
234
|
+
|
|
235
|
+
```ruby
|
|
236
|
+
describe "LoginHandler" do
|
|
237
|
+
it "redirects after successful authentication" do
|
|
238
|
+
strategy_result = Otto::Security::Authentication::StrategyResult.new(
|
|
239
|
+
session: { user_id: 123 },
|
|
240
|
+
user: { id: 123 },
|
|
241
|
+
auth_method: 'session', # Auth route succeeded
|
|
242
|
+
metadata: {}
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
expect(strategy_result.authenticated?).to be true
|
|
246
|
+
expect(strategy_result.auth_attempt_succeeded?).to be true
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
## Checklist
|
|
252
|
+
|
|
253
|
+
- [ ] Remove all usage of `success?` and `failure?` methods
|
|
254
|
+
- [ ] Update registration flows to use `authenticated?`
|
|
255
|
+
- [ ] Update post-login flows to use `auth_attempt_succeeded?` if needed
|
|
256
|
+
- [ ] Remove manual `StrategyResult` creation in controllers
|
|
257
|
+
- [ ] Update test matchers from `be_success`/`be_failure` to type checks
|
|
258
|
+
- [ ] Verify session contract keys match across apps
|
|
259
|
+
- [ ] Run full test suite: `bundle exec rspec`
|
|
260
|
+
- [ ] Test registration while logged in (should be blocked)
|
|
261
|
+
- [ ] Test login redirect flow (should work correctly)
|
|
262
|
+
|
|
263
|
+
## Configuration Updates
|
|
264
|
+
|
|
265
|
+
### Authentication Login Path Configuration
|
|
266
|
+
|
|
267
|
+
When authentication fails for HTML requests, Otto redirects to a login page. You can configure this path:
|
|
268
|
+
|
|
269
|
+
```ruby
|
|
270
|
+
# Initialize with login_path configuration
|
|
271
|
+
otto = Otto.new do
|
|
272
|
+
auth_config[:login_path] = '/auth/login' # Default: '/signin'
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# Or configure after initialization
|
|
276
|
+
otto.auth_config[:login_path] = '/custom/login'
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
**Note:** If not configured, the default fallback is `/signin`. Ensure this route exists or configure your actual login path to avoid 404 errors on authentication failures.
|
|
280
|
+
|
|
281
|
+
## Additional Improvements in v2.0.0-pre2
|
|
282
|
+
|
|
283
|
+
### Middleware Architecture Enhancements
|
|
284
|
+
|
|
285
|
+
**1. Renamed MCP ValidationMiddleware → SchemaValidationMiddleware**
|
|
286
|
+
- Resolves naming collision with `Otto::Security::ValidationMiddleware`
|
|
287
|
+
- `Otto::MCP::SchemaValidationMiddleware` now clearly indicates JSON schema validation
|
|
288
|
+
- `Otto::Security::ValidationMiddleware` remains for input sanitization
|
|
289
|
+
|
|
290
|
+
**Migration:**
|
|
291
|
+
```ruby
|
|
292
|
+
# File renamed: lib/otto/mcp/validation.rb → lib/otto/mcp/schema_validation.rb
|
|
293
|
+
# Class renamed automatically if using Otto's MCP server
|
|
294
|
+
# No action needed for most users
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
**2. Centralized Env Keys Documentation**
|
|
298
|
+
- New file: `lib/otto/env_keys.rb`
|
|
299
|
+
- Documents all `env['otto.*']` keys with types, setters, and users
|
|
300
|
+
- Includes usage examples and multi-app integration patterns
|
|
301
|
+
- Essential reference for custom middleware development
|
|
302
|
+
|
|
303
|
+
**3. RateLimitMiddleware Clarity**
|
|
304
|
+
- Added documentation clarifying it's a CONFIGURATOR, not enforcer
|
|
305
|
+
- Actual rate limiting happens in Rack::Attack middleware
|
|
306
|
+
- `call` method is explicitly a pass-through
|
|
307
|
+
|
|
308
|
+
**4. Middleware Order Enforcement**
|
|
309
|
+
- New method: `MiddlewareStack#validate_mcp_middleware_order`
|
|
310
|
+
- New method: `MiddlewareStack#add_with_position` for explicit ordering
|
|
311
|
+
- MCP Server uses explicit positioning: `position: :first` and `position: :last`
|
|
312
|
+
- Validates middleware order and warns if suboptimal
|
|
313
|
+
- Optimal: RateLimitMiddleware → TokenMiddleware → SchemaValidationMiddleware
|
|
314
|
+
- Validation runs automatically when MCP is enabled
|
|
315
|
+
|
|
316
|
+
**Usage Example:**
|
|
317
|
+
```ruby
|
|
318
|
+
# Explicit positioning for clarity
|
|
319
|
+
middleware.add_with_position(
|
|
320
|
+
Otto::MCP::RateLimitMiddleware,
|
|
321
|
+
security_config,
|
|
322
|
+
position: :first # Ensures rate limiting runs first
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
middleware.add_with_position(
|
|
326
|
+
Otto::MCP::SchemaValidationMiddleware,
|
|
327
|
+
position: :last # Ensures validation runs last
|
|
328
|
+
)
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
## Questions?
|
|
332
|
+
|
|
333
|
+
Review the comprehensive inline documentation in:
|
|
334
|
+
- `lib/otto/security/authentication/strategy_result.rb` (lines 1-90) - Auth semantics
|
|
335
|
+
- `lib/otto/security/authentication/route_auth_wrapper.rb` - Auth handler wrapper
|
|
336
|
+
- `lib/otto/env_keys.rb` - Complete env key registry
|
|
337
|
+
|
|
338
|
+
The documentation includes detailed usage patterns, session contracts, and examples for common scenarios.
|
|
@@ -12,7 +12,6 @@ otto = Otto.new('routes')
|
|
|
12
12
|
# Enable security features to demonstrate advanced route parameters
|
|
13
13
|
otto.enable_csrf_protection!
|
|
14
14
|
otto.enable_request_validation!
|
|
15
|
-
otto.enable_authentication!
|
|
16
15
|
|
|
17
16
|
# Load and configure authentication strategies
|
|
18
17
|
AuthenticationSetup.configure(otto)
|
|
@@ -7,52 +7,37 @@ require_relative '../security/validator'
|
|
|
7
7
|
require_relative '../security/authentication'
|
|
8
8
|
require_relative '../security/rate_limiting'
|
|
9
9
|
require_relative '../mcp/server'
|
|
10
|
+
require_relative 'freezable'
|
|
10
11
|
|
|
11
12
|
class Otto
|
|
12
13
|
module Core
|
|
13
14
|
# Configuration module providing locale and application configuration methods
|
|
14
15
|
module Configuration
|
|
16
|
+
include Otto::Core::Freezable
|
|
15
17
|
def configure_locale(opts)
|
|
16
|
-
#
|
|
17
|
-
global_config = self.class.global_config
|
|
18
|
-
@locale_config = nil
|
|
19
|
-
|
|
20
|
-
# Check if we have any locale configuration from any source
|
|
21
|
-
has_global_locale = global_config && (global_config[:available_locales] || global_config[:default_locale])
|
|
18
|
+
# Check if we have any locale configuration
|
|
22
19
|
has_direct_options = opts[:available_locales] || opts[:default_locale]
|
|
23
20
|
has_legacy_config = opts[:locale_config]
|
|
24
21
|
|
|
25
|
-
# Only create locale_config if we have configuration
|
|
26
|
-
return unless
|
|
22
|
+
# Only create locale_config if we have configuration
|
|
23
|
+
return unless has_direct_options || has_legacy_config
|
|
27
24
|
|
|
28
|
-
|
|
25
|
+
# Initialize with direct options
|
|
26
|
+
available_locales = opts[:available_locales]
|
|
27
|
+
default_locale = opts[:default_locale]
|
|
29
28
|
|
|
30
|
-
#
|
|
31
|
-
if
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
if global_config && global_config[:default_locale]
|
|
36
|
-
@locale_config[:default_locale] =
|
|
37
|
-
global_config[:default_locale]
|
|
29
|
+
# Legacy support: Configure locale if provided via locale_config hash
|
|
30
|
+
if opts[:locale_config]
|
|
31
|
+
locale_opts = opts[:locale_config]
|
|
32
|
+
available_locales ||= locale_opts[:available_locales] || locale_opts[:available]
|
|
33
|
+
default_locale ||= locale_opts[:default_locale] || locale_opts[:default]
|
|
38
34
|
end
|
|
39
35
|
|
|
40
|
-
#
|
|
41
|
-
@locale_config
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
return unless opts[:locale_config]
|
|
46
|
-
|
|
47
|
-
locale_opts = opts[:locale_config]
|
|
48
|
-
if locale_opts[:available_locales] || locale_opts[:available]
|
|
49
|
-
@locale_config[:available_locales] =
|
|
50
|
-
locale_opts[:available_locales] || locale_opts[:available]
|
|
51
|
-
end
|
|
52
|
-
return unless locale_opts[:default_locale] || locale_opts[:default]
|
|
53
|
-
|
|
54
|
-
@locale_config[:default_locale] =
|
|
55
|
-
locale_opts[:default_locale] || locale_opts[:default]
|
|
36
|
+
# Create Otto::Locale::Config instance
|
|
37
|
+
@locale_config = Otto::Locale::Config.new(
|
|
38
|
+
available_locales: available_locales,
|
|
39
|
+
default_locale: default_locale
|
|
40
|
+
)
|
|
56
41
|
end
|
|
57
42
|
|
|
58
43
|
def configure_security(opts)
|
|
@@ -86,7 +71,6 @@ class Otto
|
|
|
86
71
|
# Enable authentication middleware if strategies are configured
|
|
87
72
|
return unless opts[:auth_strategies] && !opts[:auth_strategies].empty?
|
|
88
73
|
|
|
89
|
-
enable_authentication!
|
|
90
74
|
end
|
|
91
75
|
|
|
92
76
|
def configure_mcp(opts)
|
|
@@ -115,9 +99,14 @@ class Otto
|
|
|
115
99
|
# default_locale: 'en'
|
|
116
100
|
# )
|
|
117
101
|
def configure(available_locales: nil, default_locale: nil)
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
102
|
+
ensure_not_frozen!
|
|
103
|
+
|
|
104
|
+
# Initialize locale_config if not already set
|
|
105
|
+
@locale_config ||= Otto::Locale::Config.new
|
|
106
|
+
|
|
107
|
+
# Update configuration
|
|
108
|
+
@locale_config.available_locales = available_locales if available_locales
|
|
109
|
+
@locale_config.default_locale = default_locale if default_locale
|
|
121
110
|
end
|
|
122
111
|
|
|
123
112
|
# Configure rate limiting settings.
|
|
@@ -134,6 +123,7 @@ class Otto
|
|
|
134
123
|
# }
|
|
135
124
|
# })
|
|
136
125
|
def configure_rate_limiting(config)
|
|
126
|
+
ensure_not_frozen!
|
|
137
127
|
@security_config.rate_limiting_config.merge!(config)
|
|
138
128
|
end
|
|
139
129
|
|
|
@@ -143,20 +133,80 @@ class Otto
|
|
|
143
133
|
# @param default_strategy [String] Default strategy to use when none specified
|
|
144
134
|
# @example
|
|
145
135
|
# otto.configure_auth_strategies({
|
|
146
|
-
# '
|
|
136
|
+
# 'noauth' => Otto::Security::Authentication::Strategies::NoAuthStrategy.new,
|
|
147
137
|
# 'authenticated' => Otto::Security::Authentication::Strategies::SessionStrategy.new(session_key: 'user_id'),
|
|
148
138
|
# 'role:admin' => Otto::Security::Authentication::Strategies::RoleStrategy.new(['admin']),
|
|
149
139
|
# 'api_key' => Otto::Security::Authentication::Strategies::APIKeyStrategy.new(api_keys: ['secret123'])
|
|
150
140
|
# })
|
|
151
|
-
def configure_auth_strategies(strategies, default_strategy: '
|
|
141
|
+
def configure_auth_strategies(strategies, default_strategy: 'noauth')
|
|
142
|
+
ensure_not_frozen!
|
|
152
143
|
# Update existing @auth_config rather than creating a new one
|
|
153
144
|
@auth_config[:auth_strategies] = strategies
|
|
154
145
|
@auth_config[:default_auth_strategy] = default_strategy
|
|
155
146
|
|
|
156
|
-
enable_authentication! unless strategies.empty?
|
|
157
147
|
end
|
|
158
148
|
|
|
159
|
-
|
|
149
|
+
# Freeze the application configuration to prevent runtime modifications.
|
|
150
|
+
# Called automatically at the end of initialization to ensure immutability.
|
|
151
|
+
#
|
|
152
|
+
# This prevents security-critical configuration from being modified after
|
|
153
|
+
# the application begins handling requests. Uses deep freezing to prevent
|
|
154
|
+
# both direct modification and modification through nested structures.
|
|
155
|
+
#
|
|
156
|
+
# @raise [RuntimeError] if configuration is already frozen
|
|
157
|
+
# @return [self]
|
|
158
|
+
def freeze_configuration!
|
|
159
|
+
if frozen_configuration?
|
|
160
|
+
Otto.logger.debug '[Otto::Configuration] Configuration already frozen, skipping' if Otto.debug
|
|
161
|
+
return self
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
Otto.logger.debug '[Otto::Configuration] Starting configuration freeze process' if Otto.debug
|
|
165
|
+
|
|
166
|
+
# Deep freeze configuration objects with memoization support
|
|
167
|
+
Otto.logger.debug '[Otto::Configuration] Freezing security_config' if Otto.debug
|
|
168
|
+
@security_config.deep_freeze! if @security_config.respond_to?(:deep_freeze!)
|
|
169
|
+
|
|
170
|
+
Otto.logger.debug '[Otto::Configuration] Freezing locale_config' if Otto.debug
|
|
171
|
+
@locale_config.deep_freeze! if @locale_config.respond_to?(:deep_freeze!)
|
|
172
|
+
|
|
173
|
+
Otto.logger.debug '[Otto::Configuration] Freezing middleware stack' if Otto.debug
|
|
174
|
+
@middleware.deep_freeze! if @middleware.respond_to?(:deep_freeze!)
|
|
175
|
+
|
|
176
|
+
# Deep freeze configuration hashes (recursively freezes nested structures)
|
|
177
|
+
Otto.logger.debug '[Otto::Configuration] Freezing auth_config hash' if Otto.debug
|
|
178
|
+
deep_freeze_value(@auth_config) if @auth_config
|
|
179
|
+
|
|
180
|
+
Otto.logger.debug '[Otto::Configuration] Freezing option hash' if Otto.debug
|
|
181
|
+
deep_freeze_value(@option) if @option
|
|
182
|
+
|
|
183
|
+
# Deep freeze route structures (prevent modification of nested hashes/arrays)
|
|
184
|
+
Otto.logger.debug '[Otto::Configuration] Freezing route structures' if Otto.debug
|
|
185
|
+
deep_freeze_value(@routes) if @routes
|
|
186
|
+
deep_freeze_value(@routes_literal) if @routes_literal
|
|
187
|
+
deep_freeze_value(@routes_static) if @routes_static
|
|
188
|
+
deep_freeze_value(@route_definitions) if @route_definitions
|
|
189
|
+
|
|
190
|
+
@configuration_frozen = true
|
|
191
|
+
Otto.logger.info '[Otto::Configuration] Configuration freeze completed successfully'
|
|
192
|
+
|
|
193
|
+
self
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Check if configuration is frozen
|
|
197
|
+
#
|
|
198
|
+
# @return [Boolean] true if configuration is frozen
|
|
199
|
+
def frozen_configuration?
|
|
200
|
+
@configuration_frozen == true
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Ensure configuration is not frozen before allowing mutations
|
|
204
|
+
#
|
|
205
|
+
# @raise [FrozenError] if configuration is frozen
|
|
206
|
+
def ensure_not_frozen!
|
|
207
|
+
raise FrozenError, 'Cannot modify frozen configuration' if frozen_configuration?
|
|
208
|
+
end
|
|
209
|
+
|
|
160
210
|
|
|
161
211
|
def middleware_enabled?(middleware_class)
|
|
162
212
|
# Only check the new middleware stack as the single source of truth
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# lib/otto/core/freezable.rb
|
|
4
|
+
|
|
5
|
+
require 'set'
|
|
6
|
+
|
|
7
|
+
class Otto
|
|
8
|
+
module Core
|
|
9
|
+
# Provides deep freezing capability for configuration objects
|
|
10
|
+
#
|
|
11
|
+
# This module enables objects to be deeply frozen, preventing any
|
|
12
|
+
# modifications to the object itself and all its nested structures.
|
|
13
|
+
# This is critical for security as it prevents runtime tampering with
|
|
14
|
+
# security configurations.
|
|
15
|
+
#
|
|
16
|
+
# @example
|
|
17
|
+
# class MyConfig
|
|
18
|
+
# include Otto::Core::Freezable
|
|
19
|
+
#
|
|
20
|
+
# def initialize
|
|
21
|
+
# @settings = { security: { enabled: true } }
|
|
22
|
+
# end
|
|
23
|
+
# end
|
|
24
|
+
#
|
|
25
|
+
# config = MyConfig.new
|
|
26
|
+
# config.deep_freeze!
|
|
27
|
+
# # Now config and all nested hashes/arrays are frozen
|
|
28
|
+
#
|
|
29
|
+
module Freezable
|
|
30
|
+
# Deeply freeze this object and all its instance variables
|
|
31
|
+
#
|
|
32
|
+
# This method recursively freezes all nested structures including:
|
|
33
|
+
# - Hashes (both keys and values)
|
|
34
|
+
# - Arrays (and all elements)
|
|
35
|
+
# - Sets
|
|
36
|
+
# - Other freezable objects
|
|
37
|
+
#
|
|
38
|
+
# NOTE: This method is idempotent and safe to call multiple times.
|
|
39
|
+
#
|
|
40
|
+
# @return [self] The frozen object
|
|
41
|
+
def deep_freeze!
|
|
42
|
+
return self if frozen?
|
|
43
|
+
|
|
44
|
+
freeze_instance_variables!
|
|
45
|
+
freeze
|
|
46
|
+
self
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
# Freeze all instance variables recursively
|
|
52
|
+
def freeze_instance_variables!
|
|
53
|
+
instance_variables.each do |var|
|
|
54
|
+
value = instance_variable_get(var)
|
|
55
|
+
deep_freeze_value(value)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Recursively freeze a value based on its type
|
|
60
|
+
#
|
|
61
|
+
# @param value [Object] Value to freeze
|
|
62
|
+
# @return [void]
|
|
63
|
+
def deep_freeze_value(value)
|
|
64
|
+
case value
|
|
65
|
+
when Hash
|
|
66
|
+
# Freeze hash keys and values, then freeze the hash itself
|
|
67
|
+
value.each do |k, v|
|
|
68
|
+
k.freeze unless k.frozen?
|
|
69
|
+
deep_freeze_value(v)
|
|
70
|
+
end
|
|
71
|
+
value.freeze
|
|
72
|
+
when Array
|
|
73
|
+
# Freeze all array elements, then freeze the array
|
|
74
|
+
value.each { |item| deep_freeze_value(item) }
|
|
75
|
+
value.freeze
|
|
76
|
+
when Set
|
|
77
|
+
# Sets are immutable once frozen
|
|
78
|
+
value.freeze
|
|
79
|
+
when String, Symbol, Numeric, TrueClass, FalseClass, NilClass
|
|
80
|
+
# These types are either immutable or already frozen
|
|
81
|
+
value.freeze if value.respond_to?(:freeze) && !value.frozen?
|
|
82
|
+
else
|
|
83
|
+
# For other objects, recursively freeze if they support it, otherwise shallow freeze.
|
|
84
|
+
if value.respond_to?(:deep_freeze!)
|
|
85
|
+
value.deep_freeze!
|
|
86
|
+
elsif value.respond_to?(:freeze) && !value.frozen?
|
|
87
|
+
value.freeze
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|