otto 1.6.0 → 2.0.0.pre2
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 +3 -2
- data/.github/workflows/claude-code-review.yml +53 -0
- data/.github/workflows/claude.yml +49 -0
- data/.gitignore +3 -0
- data/.rubocop.yml +26 -344
- data/CHANGELOG.rst +131 -0
- data/CLAUDE.md +56 -0
- data/Gemfile +11 -4
- data/Gemfile.lock +38 -42
- data/README.md +2 -0
- data/bin/rspec +4 -4
- data/changelog.d/README.md +120 -0
- data/changelog.d/scriv.ini +5 -0
- data/docs/.gitignore +2 -0
- data/docs/migrating/v2.0.0-pre1.md +276 -0
- data/docs/migrating/v2.0.0-pre2.md +345 -0
- data/examples/.gitignore +1 -0
- data/examples/advanced_routes/README.md +33 -0
- data/examples/advanced_routes/app/controllers/handlers/async.rb +9 -0
- data/examples/advanced_routes/app/controllers/handlers/dynamic.rb +9 -0
- data/examples/advanced_routes/app/controllers/handlers/static.rb +9 -0
- data/examples/advanced_routes/app/controllers/modules/auth.rb +9 -0
- data/examples/advanced_routes/app/controllers/modules/transformer.rb +9 -0
- data/examples/advanced_routes/app/controllers/modules/validator.rb +9 -0
- data/examples/advanced_routes/app/controllers/routes_app.rb +232 -0
- data/examples/advanced_routes/app/controllers/v2/admin.rb +9 -0
- data/examples/advanced_routes/app/controllers/v2/config.rb +9 -0
- data/examples/advanced_routes/app/controllers/v2/settings.rb +9 -0
- data/examples/advanced_routes/app/logic/admin/logic/manager.rb +27 -0
- data/examples/advanced_routes/app/logic/admin/panel.rb +27 -0
- data/examples/advanced_routes/app/logic/analytics_processor.rb +25 -0
- data/examples/advanced_routes/app/logic/complex/business/handler.rb +27 -0
- data/examples/advanced_routes/app/logic/data_logic.rb +23 -0
- data/examples/advanced_routes/app/logic/data_processor.rb +25 -0
- data/examples/advanced_routes/app/logic/input_validator.rb +24 -0
- data/examples/advanced_routes/app/logic/nested/feature/logic.rb +27 -0
- data/examples/advanced_routes/app/logic/reports_generator.rb +27 -0
- data/examples/advanced_routes/app/logic/simple_logic.rb +25 -0
- data/examples/advanced_routes/app/logic/system/config/manager.rb +27 -0
- data/examples/advanced_routes/app/logic/test_logic.rb +23 -0
- data/examples/advanced_routes/app/logic/transform_logic.rb +23 -0
- data/examples/advanced_routes/app/logic/upload_logic.rb +23 -0
- data/examples/advanced_routes/app/logic/v2/logic/dashboard.rb +27 -0
- data/examples/advanced_routes/app/logic/v2/logic/processor.rb +27 -0
- data/examples/advanced_routes/app.rb +33 -0
- data/examples/advanced_routes/config.rb +23 -0
- data/examples/advanced_routes/config.ru +7 -0
- data/examples/advanced_routes/puma.rb +20 -0
- data/examples/advanced_routes/routes +167 -0
- data/examples/advanced_routes/run.rb +39 -0
- data/examples/advanced_routes/test.rb +58 -0
- data/examples/authentication_strategies/README.md +32 -0
- data/examples/authentication_strategies/app/auth.rb +68 -0
- data/examples/authentication_strategies/app/controllers/auth_controller.rb +29 -0
- data/examples/authentication_strategies/app/controllers/main_controller.rb +28 -0
- data/examples/authentication_strategies/config.ru +24 -0
- data/examples/authentication_strategies/routes +37 -0
- data/examples/basic/README.md +29 -0
- data/examples/basic/app.rb +7 -35
- data/examples/basic/routes +0 -9
- data/examples/mcp_demo/README.md +87 -0
- data/examples/mcp_demo/app.rb +29 -34
- data/examples/mcp_demo/config.ru +9 -60
- data/examples/security_features/README.md +46 -0
- data/examples/security_features/app.rb +23 -24
- data/examples/security_features/config.ru +8 -10
- data/lib/otto/core/configuration.rb +167 -0
- data/lib/otto/core/error_handler.rb +86 -0
- data/lib/otto/core/file_safety.rb +61 -0
- data/lib/otto/core/middleware_stack.rb +237 -0
- data/lib/otto/core/router.rb +184 -0
- data/lib/otto/core/uri_generator.rb +44 -0
- data/lib/otto/design_system.rb +7 -5
- data/lib/otto/env_keys.rb +114 -0
- data/lib/otto/helpers/base.rb +5 -21
- data/lib/otto/helpers/request.rb +10 -8
- data/lib/otto/helpers/response.rb +27 -4
- data/lib/otto/helpers/validation.rb +9 -7
- data/lib/otto/mcp/auth/token.rb +10 -9
- data/lib/otto/mcp/protocol.rb +24 -27
- data/lib/otto/mcp/rate_limiting.rb +8 -3
- data/lib/otto/mcp/registry.rb +7 -2
- data/lib/otto/mcp/route_parser.rb +10 -15
- data/lib/otto/mcp/{validation.rb → schema_validation.rb} +16 -11
- data/lib/otto/mcp/server.rb +45 -22
- data/lib/otto/response_handlers/auto.rb +39 -0
- data/lib/otto/response_handlers/base.rb +16 -0
- data/lib/otto/response_handlers/default.rb +16 -0
- data/lib/otto/response_handlers/factory.rb +39 -0
- data/lib/otto/response_handlers/json.rb +34 -0
- data/lib/otto/response_handlers/redirect.rb +25 -0
- data/lib/otto/response_handlers/view.rb +24 -0
- data/lib/otto/response_handlers.rb +9 -135
- data/lib/otto/route.rb +51 -55
- data/lib/otto/route_definition.rb +15 -18
- data/lib/otto/route_handlers/base.rb +121 -0
- data/lib/otto/route_handlers/class_method.rb +89 -0
- data/lib/otto/route_handlers/factory.rb +42 -0
- data/lib/otto/route_handlers/instance_method.rb +69 -0
- data/lib/otto/route_handlers/lambda.rb +59 -0
- data/lib/otto/route_handlers/logic_class.rb +93 -0
- data/lib/otto/route_handlers.rb +10 -405
- data/lib/otto/security/authentication/auth_strategy.rb +44 -0
- data/lib/otto/security/authentication/authentication_middleware.rb +140 -0
- data/lib/otto/security/authentication/failure_result.rb +44 -0
- data/lib/otto/security/authentication/route_auth_wrapper.rb +149 -0
- data/lib/otto/security/authentication/strategies/api_key_strategy.rb +40 -0
- data/lib/otto/security/authentication/strategies/noauth_strategy.rb +19 -0
- data/lib/otto/security/authentication/strategies/permission_strategy.rb +47 -0
- data/lib/otto/security/authentication/strategies/role_strategy.rb +57 -0
- data/lib/otto/security/authentication/strategies/session_strategy.rb +41 -0
- data/lib/otto/security/authentication/strategy_result.rb +337 -0
- data/lib/otto/security/authentication.rb +28 -282
- data/lib/otto/security/config.rb +14 -23
- data/lib/otto/security/configurator.rb +219 -0
- data/lib/otto/security/csrf.rb +8 -143
- data/lib/otto/security/middleware/csrf_middleware.rb +151 -0
- data/lib/otto/security/middleware/rate_limit_middleware.rb +54 -0
- data/lib/otto/security/middleware/validation_middleware.rb +252 -0
- data/lib/otto/security/rate_limiter.rb +86 -0
- data/lib/otto/security/rate_limiting.rb +10 -105
- data/lib/otto/security/validator.rb +8 -253
- data/lib/otto/static.rb +3 -0
- data/lib/otto/utils.rb +14 -0
- data/lib/otto/version.rb +3 -1
- data/lib/otto.rb +141 -498
- data/otto.gemspec +4 -2
- metadata +99 -18
- data/examples/dynamic_pages/app.rb +0 -115
- data/examples/dynamic_pages/config.ru +0 -30
- data/examples/dynamic_pages/routes +0 -21
- data/examples/helpers_demo/app.rb +0 -244
- data/examples/helpers_demo/config.ru +0 -26
- data/examples/helpers_demo/routes +0 -7
- data/lib/concurrent_cache_store.rb +0 -68
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
# Migrating to Otto v2.0.0-pre1
|
|
2
|
+
|
|
3
|
+
## What's New in v2.0.0-pre1
|
|
4
|
+
|
|
5
|
+
This pre-release includes extensive test coverage improvements (76 new test cases), core module refactoring, middleware stack unification, and a major update to Logic class authentication patterns. See the [full changelog](../../CHANGELOG.rst#changelog-2.0.0-pre1) for complete details.
|
|
6
|
+
|
|
7
|
+
The main breaking changes affect:
|
|
8
|
+
1. Applications that directly manipulate the middleware stack
|
|
9
|
+
2. Logic classes using the old authentication signature
|
|
10
|
+
|
|
11
|
+
## Middleware Stack Unified API
|
|
12
|
+
|
|
13
|
+
Otto v2.0.0-pre1 introduces a significant refactoring of the middleware stack management, providing a more consistent and efficient approach to middleware configuration.
|
|
14
|
+
|
|
15
|
+
### Key Changes
|
|
16
|
+
|
|
17
|
+
#### Unified Middleware Registration
|
|
18
|
+
|
|
19
|
+
Previous versions had separate legacy and new middleware stacks. In v2.0.0-pre1, we've consolidated these into a single, more powerful middleware stack.
|
|
20
|
+
|
|
21
|
+
**Before:**
|
|
22
|
+
```ruby
|
|
23
|
+
# Old approach with separate stacks
|
|
24
|
+
otto.middleware_stack << SomeMiddleware
|
|
25
|
+
otto.middleware.add(AnotherMiddleware)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
**After:**
|
|
29
|
+
```ruby
|
|
30
|
+
# Unified middleware registration
|
|
31
|
+
otto.use(SomeMiddleware)
|
|
32
|
+
otto.use(AnotherMiddleware)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
#### Performance Improvements
|
|
36
|
+
|
|
37
|
+
- Middleware lookup is now O(1) using a Set
|
|
38
|
+
- Memoized middleware list reduces repeated array creation
|
|
39
|
+
- Prevent duplicate middleware registrations with identical configurations
|
|
40
|
+
|
|
41
|
+
### Migration Steps
|
|
42
|
+
|
|
43
|
+
1. Replace all `otto.middleware_stack <<` calls with `otto.use()`
|
|
44
|
+
2. Remove any direct references to `otto.middleware_stack`
|
|
45
|
+
3. Use `otto.middleware.includes?()` instead of manual stack checks
|
|
46
|
+
|
|
47
|
+
### Authentication and Security Methods
|
|
48
|
+
|
|
49
|
+
Authentication and security methods now consistently use the new unified middleware stack:
|
|
50
|
+
|
|
51
|
+
```ruby
|
|
52
|
+
# Before
|
|
53
|
+
otto.middleware_stack << Otto::Security::CSRFMiddleware
|
|
54
|
+
|
|
55
|
+
# After (no change needed)
|
|
56
|
+
otto.enable_csrf_protection!
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Performance Considerations
|
|
60
|
+
|
|
61
|
+
The new implementation is more memory-efficient and provides faster middleware lookups, especially for applications with many middleware components.
|
|
62
|
+
|
|
63
|
+
### Potential Breaking Changes
|
|
64
|
+
|
|
65
|
+
- Code relying on direct manipulation of `middleware_stack` will need updates
|
|
66
|
+
- Method signatures for middleware configuration remain the same
|
|
67
|
+
|
|
68
|
+
### Example Migration
|
|
69
|
+
|
|
70
|
+
```ruby
|
|
71
|
+
# Old code
|
|
72
|
+
class MyApp
|
|
73
|
+
def initialize
|
|
74
|
+
@otto = Otto.new
|
|
75
|
+
@otto.middleware_stack << CustomMiddleware
|
|
76
|
+
@otto.middleware.add(AnotherMiddleware)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# New code
|
|
81
|
+
class MyApp
|
|
82
|
+
def initialize
|
|
83
|
+
@otto = Otto.new
|
|
84
|
+
@otto.use(CustomMiddleware)
|
|
85
|
+
@otto.use(AnotherMiddleware)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Troubleshooting
|
|
91
|
+
|
|
92
|
+
If you encounter any issues with middleware registration or configuration, please file an issue at [Otto GitHub Repository](https://github.com/delano/otto/issues).
|
|
93
|
+
|
|
94
|
+
## Logic Class RequestContext Pattern
|
|
95
|
+
|
|
96
|
+
Otto v2.0.0-pre1 introduces a major improvement to Logic class authentication with the new RequestContext pattern.
|
|
97
|
+
|
|
98
|
+
### Key Changes
|
|
99
|
+
|
|
100
|
+
#### New Constructor Signature
|
|
101
|
+
|
|
102
|
+
Logic classes now use a cleaner, more powerful constructor signature that provides immutable context.
|
|
103
|
+
|
|
104
|
+
**Before:**
|
|
105
|
+
```ruby
|
|
106
|
+
class MyLogic
|
|
107
|
+
attr_reader :session, :user, :params, :locale
|
|
108
|
+
|
|
109
|
+
def initialize(session, user, params, locale)
|
|
110
|
+
@session = session
|
|
111
|
+
@user = user
|
|
112
|
+
@params = params
|
|
113
|
+
@locale = locale
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def process
|
|
117
|
+
return { error: 'not authenticated' } unless @user
|
|
118
|
+
{ result: 'success', user_name: @user['name'] }
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
**After:**
|
|
124
|
+
```ruby
|
|
125
|
+
class MyLogic
|
|
126
|
+
attr_reader :context, :params, :locale
|
|
127
|
+
|
|
128
|
+
def initialize(context, params, locale)
|
|
129
|
+
@context = context
|
|
130
|
+
@params = params
|
|
131
|
+
@locale = locale
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def process
|
|
135
|
+
return { error: 'not authenticated' } unless @context.authenticated?
|
|
136
|
+
{
|
|
137
|
+
result: 'success',
|
|
138
|
+
user_name: @context.user_name,
|
|
139
|
+
roles: @context.roles,
|
|
140
|
+
permissions: @context.permissions
|
|
141
|
+
}
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
#### RequestContext Benefits
|
|
147
|
+
|
|
148
|
+
1. **Immutable Structure** - RequestContext is a Ruby Data class that can't be accidentally modified
|
|
149
|
+
2. **Helper Methods** - Built-in methods like `authenticated?`, `has_role?`, `has_permission?`
|
|
150
|
+
3. **Cleaner Interface** - Single context parameter instead of separate session/user parameters
|
|
151
|
+
4. **Type Safety** - Better IDE support and documentation
|
|
152
|
+
5. **Future-proof** - New authentication data automatically available
|
|
153
|
+
|
|
154
|
+
#### RequestContext API
|
|
155
|
+
|
|
156
|
+
```ruby
|
|
157
|
+
# Authentication status
|
|
158
|
+
context.authenticated? # Boolean: has non-empty user data
|
|
159
|
+
context.anonymous? # Boolean: not authenticated
|
|
160
|
+
|
|
161
|
+
# User information
|
|
162
|
+
context.user_id # User ID from various possible locations
|
|
163
|
+
context.user_name # User name/username
|
|
164
|
+
context.session_id # Session identifier
|
|
165
|
+
|
|
166
|
+
# Role and permission checks
|
|
167
|
+
context.has_role?('admin') # Single role check
|
|
168
|
+
context.has_permission?('write') # Single permission check
|
|
169
|
+
context.has_any_role?('admin', 'mod') # Multiple role check
|
|
170
|
+
context.roles # Array of all user roles
|
|
171
|
+
context.permissions # Array of all user permissions
|
|
172
|
+
|
|
173
|
+
# Raw data access
|
|
174
|
+
context.session # Session hash
|
|
175
|
+
context.user # User hash
|
|
176
|
+
context.auth_method # Authentication method used
|
|
177
|
+
context.metadata # Additional context data
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Migration Steps
|
|
181
|
+
|
|
182
|
+
1. **Update Logic class constructor signatures** from 4 parameters to 3:
|
|
183
|
+
```ruby
|
|
184
|
+
# Change this:
|
|
185
|
+
def initialize(session, user, params, locale)
|
|
186
|
+
|
|
187
|
+
# To this:
|
|
188
|
+
def initialize(context, params, locale)
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
2. **Update instance variables**:
|
|
192
|
+
```ruby
|
|
193
|
+
# Change this:
|
|
194
|
+
@session = session
|
|
195
|
+
@user = user
|
|
196
|
+
|
|
197
|
+
# To this:
|
|
198
|
+
@context = context
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
3. **Update authentication checks**:
|
|
202
|
+
```ruby
|
|
203
|
+
# Change this:
|
|
204
|
+
return error unless @user
|
|
205
|
+
|
|
206
|
+
# To this:
|
|
207
|
+
return error unless @context.authenticated?
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
4. **Update data access**:
|
|
211
|
+
```ruby
|
|
212
|
+
# Change this:
|
|
213
|
+
user_name = @user&.dig('name')
|
|
214
|
+
user_role = @user&.dig('role')
|
|
215
|
+
|
|
216
|
+
# To this:
|
|
217
|
+
user_name = @context.user_name
|
|
218
|
+
user_role = @context.roles.first
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### Example Migration
|
|
222
|
+
|
|
223
|
+
**Before (v1.x):**
|
|
224
|
+
```ruby
|
|
225
|
+
class AdminPanel
|
|
226
|
+
attr_reader :session, :user, :params, :locale
|
|
227
|
+
|
|
228
|
+
def initialize(session, user, params, locale)
|
|
229
|
+
@session = session
|
|
230
|
+
@user = user
|
|
231
|
+
@params = params
|
|
232
|
+
@locale = locale
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def raise_concerns
|
|
236
|
+
raise 'Access denied' unless @user&.dig('role') == 'admin'
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def process
|
|
240
|
+
{
|
|
241
|
+
panel: 'admin',
|
|
242
|
+
user: @user&.dig('name') || 'unknown',
|
|
243
|
+
session_id: @session&.dig('id')
|
|
244
|
+
}
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
**After (v2.0):**
|
|
250
|
+
```ruby
|
|
251
|
+
class AdminPanel
|
|
252
|
+
attr_reader :context, :params, :locale
|
|
253
|
+
|
|
254
|
+
def initialize(context, params, locale)
|
|
255
|
+
@context = context
|
|
256
|
+
@params = params
|
|
257
|
+
@locale = locale
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def raise_concerns
|
|
261
|
+
raise 'Access denied' unless @context.has_role?('admin')
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def process
|
|
265
|
+
{
|
|
266
|
+
panel: 'admin',
|
|
267
|
+
user: @context.user_name || 'unknown',
|
|
268
|
+
session_id: @context.session_id,
|
|
269
|
+
authenticated: @context.authenticated?,
|
|
270
|
+
permissions: @context.permissions
|
|
271
|
+
}
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
This provides a cleaner, more maintainable interface while giving Logic classes access to rich authentication context.
|
|
@@ -0,0 +1,345 @@
|
|
|
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
|
+
# Or for middleware - FailureResult indicates failure
|
|
31
|
+
if strategy_result.is_a?(Otto::Security::Authentication::FailureResult)
|
|
32
|
+
# handle failure
|
|
33
|
+
else
|
|
34
|
+
# handle success
|
|
35
|
+
end
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### 2. New Semantic Distinction
|
|
39
|
+
|
|
40
|
+
**New Method:**
|
|
41
|
+
- `StrategyResult#auth_attempt_succeeded?` - Returns `true` only when auth strategy just executed successfully
|
|
42
|
+
|
|
43
|
+
**Key Semantic Difference:**
|
|
44
|
+
|
|
45
|
+
| Method | Meaning | Use Case |
|
|
46
|
+
|--------|---------|----------|
|
|
47
|
+
| `authenticated?` | User in session (request state) | Check if session has user |
|
|
48
|
+
| `auth_attempt_succeeded?` | Auth strategy just succeeded (auth outcome) | Post-login redirects, analytics |
|
|
49
|
+
|
|
50
|
+
**Migration Examples:**
|
|
51
|
+
|
|
52
|
+
#### Registration Flow (IMPORTANT)
|
|
53
|
+
|
|
54
|
+
```ruby
|
|
55
|
+
# Before - BROKEN - blocks legitimate registration
|
|
56
|
+
class CreateAccount < Logic::Base
|
|
57
|
+
def raise_concerns
|
|
58
|
+
# This was always true if user in session, blocking registration
|
|
59
|
+
raise OT::FormError, "Already signed up" if @strategy_result.success?
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# After - CORRECT
|
|
64
|
+
class CreateAccount < Logic::Base
|
|
65
|
+
def raise_concerns
|
|
66
|
+
# Check if user already in session
|
|
67
|
+
raise OT::FormError, "Already signed up" if @strategy_result.authenticated?
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
#### Post-Login Redirect
|
|
73
|
+
|
|
74
|
+
```ruby
|
|
75
|
+
# Before - unreliable
|
|
76
|
+
class AuthController
|
|
77
|
+
def authenticate
|
|
78
|
+
if @strategy_result.success? # Always true, not helpful
|
|
79
|
+
redirect_to dashboard_path
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# After - correct semantic
|
|
85
|
+
class AuthController
|
|
86
|
+
def authenticate
|
|
87
|
+
if @strategy_result.auth_attempt_succeeded?
|
|
88
|
+
# Only redirect when auth route just succeeded
|
|
89
|
+
redirect_to dashboard_path
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Non-Breaking Enhancements
|
|
96
|
+
|
|
97
|
+
### 1. Comprehensive Documentation
|
|
98
|
+
|
|
99
|
+
`StrategyResult` now includes extensive inline documentation:
|
|
100
|
+
- Usage patterns and creation guidelines
|
|
101
|
+
- Session contract for multi-app architectures
|
|
102
|
+
- Examples for common scenarios
|
|
103
|
+
- Clear distinction between request state and auth outcomes
|
|
104
|
+
|
|
105
|
+
### 2. Session Contract (Multi-App Architectures)
|
|
106
|
+
|
|
107
|
+
For shared session architectures (Auth app + Core app):
|
|
108
|
+
|
|
109
|
+
**Required session keys for authenticated state:**
|
|
110
|
+
```ruby
|
|
111
|
+
session['authenticated'] # Boolean flag
|
|
112
|
+
session['identity_id'] # User/customer ID
|
|
113
|
+
session['authenticated_at'] # Timestamp
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**Optional session keys:**
|
|
117
|
+
```ruby
|
|
118
|
+
session['email'] # User email
|
|
119
|
+
session['ip_address'] # Client IP
|
|
120
|
+
session['user_agent'] # Client UA
|
|
121
|
+
session['locale'] # User locale
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
**Advanced mode adds:**
|
|
125
|
+
```ruby
|
|
126
|
+
session['account_external_id'] # Rodauth external_id
|
|
127
|
+
session['advanced_account_id'] # Rodauth account ID
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Application Code Updates Required
|
|
131
|
+
|
|
132
|
+
### 1. Remove Manual StrategyResult Creation
|
|
133
|
+
|
|
134
|
+
**Anti-pattern identified:**
|
|
135
|
+
```ruby
|
|
136
|
+
# BAD - Bypasses Otto's auth_method tracking
|
|
137
|
+
class Controller::Base
|
|
138
|
+
def _strategy_result
|
|
139
|
+
Otto::Security::Authentication::StrategyResult.new(
|
|
140
|
+
session: session,
|
|
141
|
+
user: cust,
|
|
142
|
+
auth_method: 'session', # Hardcoded - loses semantic meaning
|
|
143
|
+
metadata: { ip: req.client_ipaddress }
|
|
144
|
+
)
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
**Correct approach:**
|
|
150
|
+
```ruby
|
|
151
|
+
# GOOD - Use middleware-provided result
|
|
152
|
+
class Controller::Base
|
|
153
|
+
def strategy_result
|
|
154
|
+
req.env['otto.strategy_result'] # Created by AuthenticationMiddleware
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Or for non-auth checks, use session directly
|
|
159
|
+
class Controller::Base
|
|
160
|
+
def current_user
|
|
161
|
+
return nil unless session['authenticated']
|
|
162
|
+
Customer.find(session['identity_id'])
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### 2. Update Logic Classes
|
|
168
|
+
|
|
169
|
+
**Pattern to check for:**
|
|
170
|
+
```ruby
|
|
171
|
+
# Search your codebase for these patterns:
|
|
172
|
+
grep -r "@strategy_result.success?" apps/
|
|
173
|
+
grep -r "@context.success?" apps/
|
|
174
|
+
grep -r "strategy_result&.success?" apps/
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
**Update to:**
|
|
178
|
+
- Use `authenticated?` for "user in session" checks (registration, profile access, etc.)
|
|
179
|
+
- Use `auth_attempt_succeeded?` for "just logged in" checks (redirects, welcome messages, etc.)
|
|
180
|
+
|
|
181
|
+
### 3. Test Updates
|
|
182
|
+
|
|
183
|
+
**RSpec matchers:**
|
|
184
|
+
```ruby
|
|
185
|
+
# Before
|
|
186
|
+
expect(result).to be_success
|
|
187
|
+
expect(result).to be_failure
|
|
188
|
+
|
|
189
|
+
# After
|
|
190
|
+
expect(result).to be_a(Otto::Security::Authentication::StrategyResult)
|
|
191
|
+
expect(result).to be_a(Otto::Security::Authentication::FailureResult)
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## Architecture Clarifications
|
|
195
|
+
|
|
196
|
+
### When StrategyResult is Created
|
|
197
|
+
|
|
198
|
+
1. **Routes WITH `auth=...` requirement:**
|
|
199
|
+
- Strategy executes
|
|
200
|
+
- Returns `StrategyResult` (success) or `FailureResult` (failure)
|
|
201
|
+
- Middleware converts `FailureResult` to anonymous `StrategyResult` + 401 response
|
|
202
|
+
|
|
203
|
+
2. **Routes WITHOUT `auth=...` requirement:**
|
|
204
|
+
- Middleware creates anonymous `StrategyResult`
|
|
205
|
+
- Sets `auth_method: 'anonymous'`
|
|
206
|
+
|
|
207
|
+
3. **Auth app (Roda) routes:**
|
|
208
|
+
- Manually creates `StrategyResult` for Logic class compatibility
|
|
209
|
+
- Same interface as Otto controllers
|
|
210
|
+
|
|
211
|
+
### Integration Boundaries
|
|
212
|
+
|
|
213
|
+
**Multi-app setup (Auth + Core + API):**
|
|
214
|
+
- **Shared:** Session middleware, Redis session, Logic classes, Customer model
|
|
215
|
+
- **Auth app:** Creates StrategyResult manually, uses Roda routing
|
|
216
|
+
- **Core/API apps:** StrategyResult from AuthenticationMiddleware
|
|
217
|
+
- **Integration:** Pure session-based, no direct code calls between apps
|
|
218
|
+
|
|
219
|
+
## Testing Your Migration
|
|
220
|
+
|
|
221
|
+
### 1. Registration Flow Test
|
|
222
|
+
|
|
223
|
+
```ruby
|
|
224
|
+
describe "CreateAccount" do
|
|
225
|
+
it "blocks registration when user already authenticated" do
|
|
226
|
+
strategy_result = Otto::Security::Authentication::StrategyResult.new(
|
|
227
|
+
session: { user_id: 123 },
|
|
228
|
+
user: { id: 123 },
|
|
229
|
+
auth_method: 'anonymous', # No auth route, but user in session
|
|
230
|
+
metadata: {}
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
logic = CreateAccount.new(strategy_result, params, 'en')
|
|
234
|
+
|
|
235
|
+
expect { logic.raise_concerns }.to raise_error(OT::FormError, /Already signed up/)
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### 2. Auth Attempt Test
|
|
241
|
+
|
|
242
|
+
```ruby
|
|
243
|
+
describe "LoginHandler" do
|
|
244
|
+
it "redirects after successful authentication" do
|
|
245
|
+
strategy_result = Otto::Security::Authentication::StrategyResult.new(
|
|
246
|
+
session: { user_id: 123 },
|
|
247
|
+
user: { id: 123 },
|
|
248
|
+
auth_method: 'session', # Auth route succeeded
|
|
249
|
+
metadata: {}
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
expect(strategy_result.authenticated?).to be true
|
|
253
|
+
expect(strategy_result.auth_attempt_succeeded?).to be true
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
## Checklist
|
|
259
|
+
|
|
260
|
+
- [ ] Remove all usage of `success?` and `failure?` methods
|
|
261
|
+
- [ ] Update registration flows to use `authenticated?`
|
|
262
|
+
- [ ] Update post-login flows to use `auth_attempt_succeeded?` if needed
|
|
263
|
+
- [ ] Remove manual `StrategyResult` creation in controllers
|
|
264
|
+
- [ ] Update test matchers from `be_success`/`be_failure` to type checks
|
|
265
|
+
- [ ] Verify session contract keys match across apps
|
|
266
|
+
- [ ] Run full test suite: `bundle exec rspec`
|
|
267
|
+
- [ ] Test registration while logged in (should be blocked)
|
|
268
|
+
- [ ] Test login redirect flow (should work correctly)
|
|
269
|
+
|
|
270
|
+
## Configuration Updates
|
|
271
|
+
|
|
272
|
+
### Authentication Login Path Configuration
|
|
273
|
+
|
|
274
|
+
When authentication fails for HTML requests, Otto redirects to a login page. You can configure this path:
|
|
275
|
+
|
|
276
|
+
```ruby
|
|
277
|
+
# Initialize with login_path configuration
|
|
278
|
+
otto = Otto.new do
|
|
279
|
+
auth_config[:login_path] = '/auth/login' # Default: '/signin'
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# Or configure after initialization
|
|
283
|
+
otto.auth_config[:login_path] = '/custom/login'
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
**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.
|
|
287
|
+
|
|
288
|
+
## Additional Improvements in v2.0.0-pre2
|
|
289
|
+
|
|
290
|
+
### Middleware Architecture Enhancements
|
|
291
|
+
|
|
292
|
+
**1. Renamed MCP ValidationMiddleware → SchemaValidationMiddleware**
|
|
293
|
+
- Resolves naming collision with `Otto::Security::ValidationMiddleware`
|
|
294
|
+
- `Otto::MCP::SchemaValidationMiddleware` now clearly indicates JSON schema validation
|
|
295
|
+
- `Otto::Security::ValidationMiddleware` remains for input sanitization
|
|
296
|
+
|
|
297
|
+
**Migration:**
|
|
298
|
+
```ruby
|
|
299
|
+
# File renamed: lib/otto/mcp/validation.rb → lib/otto/mcp/schema_validation.rb
|
|
300
|
+
# Class renamed automatically if using Otto's MCP server
|
|
301
|
+
# No action needed for most users
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
**2. Centralized Env Keys Documentation**
|
|
305
|
+
- New file: `lib/otto/env_keys.rb`
|
|
306
|
+
- Documents all `env['otto.*']` keys with types, setters, and users
|
|
307
|
+
- Includes usage examples and multi-app integration patterns
|
|
308
|
+
- Essential reference for custom middleware development
|
|
309
|
+
|
|
310
|
+
**3. RateLimitMiddleware Clarity**
|
|
311
|
+
- Added documentation clarifying it's a CONFIGURATOR, not enforcer
|
|
312
|
+
- Actual rate limiting happens in Rack::Attack middleware
|
|
313
|
+
- `call` method is explicitly a pass-through
|
|
314
|
+
|
|
315
|
+
**4. Middleware Order Enforcement**
|
|
316
|
+
- New method: `MiddlewareStack#validate_mcp_middleware_order`
|
|
317
|
+
- New method: `MiddlewareStack#add_with_position` for explicit ordering
|
|
318
|
+
- MCP Server uses explicit positioning: `position: :first` and `position: :last`
|
|
319
|
+
- Validates middleware order and warns if suboptimal
|
|
320
|
+
- Optimal: RateLimitMiddleware → TokenMiddleware → SchemaValidationMiddleware
|
|
321
|
+
- Validation runs automatically when MCP is enabled
|
|
322
|
+
|
|
323
|
+
**Usage Example:**
|
|
324
|
+
```ruby
|
|
325
|
+
# Explicit positioning for clarity
|
|
326
|
+
middleware.add_with_position(
|
|
327
|
+
Otto::MCP::RateLimitMiddleware,
|
|
328
|
+
security_config,
|
|
329
|
+
position: :first # Ensures rate limiting runs first
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
middleware.add_with_position(
|
|
333
|
+
Otto::MCP::SchemaValidationMiddleware,
|
|
334
|
+
position: :last # Ensures validation runs last
|
|
335
|
+
)
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
## Questions?
|
|
339
|
+
|
|
340
|
+
Review the comprehensive inline documentation in:
|
|
341
|
+
- `lib/otto/security/authentication/strategy_result.rb` (lines 1-90) - Auth semantics
|
|
342
|
+
- `lib/otto/security/authentication/authentication_middleware.rb` - Auth middleware
|
|
343
|
+
- `lib/otto/env_keys.rb` - Complete env key registry
|
|
344
|
+
|
|
345
|
+
The documentation includes detailed usage patterns, session contracts, and examples for common scenarios.
|
data/examples/.gitignore
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
!**/README.md
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Otto - Advanced Routes Example
|
|
2
|
+
|
|
3
|
+
This example demonstrates the advanced routing syntax features available in Otto, such as response type negotiation, CSRF exemptions, and routing to logic classes.
|
|
4
|
+
|
|
5
|
+
## Project Structure
|
|
6
|
+
|
|
7
|
+
The example is structured to separate concerns, making it easier to navigate:
|
|
8
|
+
|
|
9
|
+
- `config.ru`: The main Rackup file that loads and runs the Otto application.
|
|
10
|
+
- `routes`: A comprehensive file demonstrating various advanced routing syntaxes. This file serves as a good reference.
|
|
11
|
+
- `app.rb`: A simple loader that requires all controller and logic files.
|
|
12
|
+
- `app/controllers/`: Contains the `RoutesApp` class and other namespaced controller modules that handle incoming requests.
|
|
13
|
+
- `app/logic/`: Contains various "Logic Classes". These are special classes that can be routed to directly, encapsulating business logic for a specific route. They are organized into namespaces to show how Otto handles complex class names.
|
|
14
|
+
- `run.rb`, `puma.rb`, `test.rb`: Alternative ways to run or test the application.
|
|
15
|
+
|
|
16
|
+
## Key Features Demonstrated
|
|
17
|
+
|
|
18
|
+
- **Response Types:** Defining `response=json`, `response=view`, etc., directly in the `routes` file.
|
|
19
|
+
- **CSRF Exemption:** Using `csrf=exempt` for APIs or webhooks.
|
|
20
|
+
- **Logic Classes:** Routing directly to a class (e.g., `GET /logic/simple SimpleLogic`). Otto automatically instantiates it and calls its `process` method.
|
|
21
|
+
- **Namespaced Targets:** Routing to deeply namespaced classes and modules (e.g., `GET /logic/v2/dashboard V2::Logic::Dashboard`).
|
|
22
|
+
- **Custom Parameters:** Adding arbitrary key-value parameters to a route for custom logic.
|
|
23
|
+
|
|
24
|
+
## Running the Example
|
|
25
|
+
|
|
26
|
+
1. Make sure you have the necessary gems installed (`bundle install`).
|
|
27
|
+
2. Run the application from the root of the `otto` project:
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
rackup examples/advanced_routes/config.ru
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
3. The application will be running at `http://localhost:9292`. You can use `curl` or your browser to test the various routes defined in the `routes` file.
|