otto 1.5.0 → 2.0.0.pre1
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 +44 -5
- data/.github/workflows/claude-code-review.yml +53 -0
- data/.github/workflows/claude.yml +49 -0
- data/.gitignore +3 -0
- data/.rubocop.yml +24 -345
- data/CHANGELOG.rst +83 -0
- data/CLAUDE.md +56 -0
- data/Gemfile +21 -5
- data/Gemfile.lock +69 -31
- data/README.md +2 -0
- data/bin/rspec +16 -0
- data/changelog.d/20250911_235619_delano_next.rst +28 -0
- data/changelog.d/20250912_123055_delano_remove_ostruct.rst +21 -0
- data/changelog.d/20250912_175625_claude_delano_remove_ostruct.rst +21 -0
- data/changelog.d/README.md +120 -0
- data/changelog.d/scriv.ini +5 -0
- data/docs/.gitignore +1 -0
- data/docs/migrating/v2.0.0-pre1.md +276 -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 +51 -0
- data/examples/mcp_demo/config.ru +17 -0
- data/examples/mcp_demo/routes +9 -0
- 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 +157 -0
- data/lib/otto/core/router.rb +183 -0
- data/lib/otto/core/uri_generator.rb +44 -0
- data/lib/otto/design_system.rb +7 -5
- data/lib/otto/helpers/base.rb +3 -0
- data/lib/otto/helpers/request.rb +10 -8
- data/lib/otto/helpers/response.rb +5 -4
- data/lib/otto/helpers/validation.rb +85 -0
- data/lib/otto/mcp/auth/token.rb +77 -0
- data/lib/otto/mcp/protocol.rb +164 -0
- data/lib/otto/mcp/rate_limiting.rb +155 -0
- data/lib/otto/mcp/registry.rb +100 -0
- data/lib/otto/mcp/route_parser.rb +77 -0
- data/lib/otto/mcp/server.rb +206 -0
- data/lib/otto/mcp/validation.rb +123 -0
- 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 +28 -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 +9 -9
- data/lib/otto/route_definition.rb +30 -33
- 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 +29 -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 -376
- data/lib/otto/security/authentication/auth_strategy.rb +44 -0
- data/lib/otto/security/authentication/authentication_middleware.rb +123 -0
- data/lib/otto/security/authentication/failure_result.rb +36 -0
- data/lib/otto/security/authentication/strategies/api_key_strategy.rb +40 -0
- data/lib/otto/security/authentication/strategies/permission_strategy.rb +47 -0
- data/lib/otto/security/authentication/strategies/public_strategy.rb +19 -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 +223 -0
- data/lib/otto/security/authentication.rb +28 -282
- data/lib/otto/security/config.rb +15 -11
- 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 +38 -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 +16 -0
- data/lib/otto/security/validator.rb +8 -292
- 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 +184 -414
- data/otto.gemspec +11 -6
- metadata +134 -25
- 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/otto/route_handlers.rb
CHANGED
@@ -1,383 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
# lib/otto/route_handlers.rb
|
2
4
|
|
3
5
|
class Otto
|
4
|
-
# Pluggable Route Handler Factory
|
6
|
+
# Pluggable Route Handler Factory
|
7
|
+
#
|
5
8
|
# Enables different execution patterns while maintaining backward compatibility
|
6
9
|
module RouteHandlers
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
# @return [BaseHandler] Appropriate handler for the route
|
14
|
-
def self.create_handler(route_definition, otto_instance = nil)
|
15
|
-
case route_definition.kind
|
16
|
-
when :logic
|
17
|
-
LogicClassHandler.new(route_definition, otto_instance)
|
18
|
-
when :instance
|
19
|
-
InstanceMethodHandler.new(route_definition, otto_instance)
|
20
|
-
when :class
|
21
|
-
ClassMethodHandler.new(route_definition, otto_instance)
|
22
|
-
else
|
23
|
-
raise ArgumentError, "Unknown handler kind: #{route_definition.kind}"
|
24
|
-
end
|
25
|
-
end
|
26
|
-
end
|
27
|
-
|
28
|
-
# Base class for all route handlers
|
29
|
-
# Provides common functionality and interface
|
30
|
-
class BaseHandler
|
31
|
-
attr_reader :route_definition, :otto_instance
|
32
|
-
|
33
|
-
def initialize(route_definition, otto_instance = nil)
|
34
|
-
@route_definition = route_definition
|
35
|
-
@otto_instance = otto_instance
|
36
|
-
end
|
37
|
-
|
38
|
-
# Execute the route handler
|
39
|
-
# @param env [Hash] Rack environment
|
40
|
-
# @param extra_params [Hash] Additional parameters
|
41
|
-
# @return [Array] Rack response array
|
42
|
-
def call(env, extra_params = {})
|
43
|
-
raise NotImplementedError, "Subclasses must implement #call"
|
44
|
-
end
|
45
|
-
|
46
|
-
protected
|
47
|
-
|
48
|
-
# Get the target class, loading it safely
|
49
|
-
# @return [Class] The target class
|
50
|
-
def target_class
|
51
|
-
@target_class ||= safe_const_get(route_definition.klass_name)
|
52
|
-
end
|
53
|
-
|
54
|
-
# Setup request and response with the same extensions and processing as Route#call
|
55
|
-
# @param req [Rack::Request] Request object
|
56
|
-
# @param res [Rack::Response] Response object
|
57
|
-
# @param env [Hash] Rack environment
|
58
|
-
# @param extra_params [Hash] Additional parameters
|
59
|
-
def setup_request_response(req, res, env, extra_params)
|
60
|
-
# Apply the same extensions as original Route#call
|
61
|
-
req.extend Otto::RequestHelpers
|
62
|
-
res.extend Otto::ResponseHelpers
|
63
|
-
res.request = req
|
64
|
-
|
65
|
-
# Make security config available to response helpers
|
66
|
-
if otto_instance&.respond_to?(:security_config) && otto_instance.security_config
|
67
|
-
env['otto.security_config'] = otto_instance.security_config
|
68
|
-
end
|
69
|
-
|
70
|
-
# Make route definition and options available to middleware and handlers
|
71
|
-
env['otto.route_definition'] = route_definition
|
72
|
-
env['otto.route_options'] = route_definition.options
|
73
|
-
|
74
|
-
# Process parameters through security layer
|
75
|
-
req.params.merge! extra_params
|
76
|
-
req.params.replace Otto::Static.indifferent_params(req.params)
|
77
|
-
|
78
|
-
# Add security headers
|
79
|
-
if otto_instance&.respond_to?(:security_config) && otto_instance.security_config
|
80
|
-
otto_instance.security_config.security_headers.each do |header, value|
|
81
|
-
res.headers[header] = value
|
82
|
-
end
|
83
|
-
end
|
84
|
-
|
85
|
-
# Setup class extensions if target_class is available
|
86
|
-
if target_class
|
87
|
-
target_class.extend Otto::Route::ClassMethods
|
88
|
-
target_class.otto = otto_instance if otto_instance
|
89
|
-
end
|
90
|
-
|
91
|
-
# Add security helpers if CSRF is enabled
|
92
|
-
if otto_instance&.respond_to?(:security_config) && otto_instance.security_config&.csrf_enabled?
|
93
|
-
res.extend Otto::Security::CSRFHelpers
|
94
|
-
end
|
95
|
-
|
96
|
-
# Add validation helpers
|
97
|
-
res.extend Otto::Security::ValidationHelpers
|
98
|
-
end
|
99
|
-
|
100
|
-
# Finalize response with the same processing as Route#call
|
101
|
-
# @param res [Rack::Response] Response object
|
102
|
-
# @return [Array] Rack response array
|
103
|
-
def finalize_response(res)
|
104
|
-
res.body = [res.body] unless res.body.respond_to?(:each)
|
105
|
-
res.finish
|
106
|
-
end
|
107
|
-
|
108
|
-
# Handle response using appropriate response handler
|
109
|
-
# @param result [Object] Result from route execution
|
110
|
-
# @param response [Rack::Response] Response object
|
111
|
-
# @param context [Hash] Additional context for response handling
|
112
|
-
def handle_response(result, response, context = {})
|
113
|
-
response_type = route_definition.response_type
|
114
|
-
|
115
|
-
# Get the appropriate response handler
|
116
|
-
handler_class = case response_type
|
117
|
-
when 'json' then Otto::ResponseHandlers::JSONHandler
|
118
|
-
when 'redirect' then Otto::ResponseHandlers::RedirectHandler
|
119
|
-
when 'view' then Otto::ResponseHandlers::ViewHandler
|
120
|
-
when 'auto' then Otto::ResponseHandlers::AutoHandler
|
121
|
-
else Otto::ResponseHandlers::DefaultHandler
|
122
|
-
end
|
123
|
-
|
124
|
-
handler_class.handle(result, response, context)
|
125
|
-
end
|
126
|
-
|
127
|
-
private
|
128
|
-
|
129
|
-
# Safely get a constant from a string name
|
130
|
-
# @param name [String] Class name
|
131
|
-
# @return [Class] The class
|
132
|
-
def safe_const_get(name)
|
133
|
-
name.split('::').inject(Object) do |scope, const_name|
|
134
|
-
scope.const_get(const_name)
|
135
|
-
end
|
136
|
-
rescue NameError => e
|
137
|
-
raise NameError, "Unknown class: #{name} (#{e})"
|
138
|
-
end
|
139
|
-
end
|
140
|
-
|
141
|
-
# Handler for Logic classes (new in Otto Framework Enhancement)
|
142
|
-
# Supports the OneTime Secret Logic class pattern
|
143
|
-
class LogicClassHandler < BaseHandler
|
144
|
-
def call(env, extra_params = {})
|
145
|
-
req = Rack::Request.new(env)
|
146
|
-
res = Rack::Response.new
|
147
|
-
|
148
|
-
begin
|
149
|
-
# Get authentication context if available
|
150
|
-
auth_result = env['otto.auth_result']
|
151
|
-
|
152
|
-
# Initialize Logic class with standard parameters
|
153
|
-
# Logic classes expect: session, user, params, locale
|
154
|
-
logic_params = req.params.merge(extra_params)
|
155
|
-
locale = env['otto.locale'] || 'en'
|
156
|
-
|
157
|
-
logic = if target_class.instance_method(:initialize).arity == 4
|
158
|
-
# Standard Logic class constructor
|
159
|
-
target_class.new(
|
160
|
-
auth_result&.session,
|
161
|
-
auth_result&.user,
|
162
|
-
logic_params,
|
163
|
-
locale
|
164
|
-
)
|
165
|
-
else
|
166
|
-
# Fallback for custom constructors
|
167
|
-
target_class.new(req, res)
|
168
|
-
end
|
169
|
-
|
170
|
-
# Execute standard Logic class lifecycle
|
171
|
-
if logic.respond_to?(:raise_concerns)
|
172
|
-
logic.raise_concerns
|
173
|
-
end
|
174
|
-
|
175
|
-
result = if logic.respond_to?(:process)
|
176
|
-
logic.process
|
177
|
-
else
|
178
|
-
logic.call || logic
|
179
|
-
end
|
180
|
-
|
181
|
-
# Handle response with Logic instance context
|
182
|
-
handle_response(result, res, {
|
183
|
-
logic_instance: logic,
|
184
|
-
request: req,
|
185
|
-
status_code: logic.respond_to?(:status_code) ? logic.status_code : nil
|
186
|
-
})
|
187
|
-
|
188
|
-
rescue => e
|
189
|
-
# Error handling - return 500 with proper headers like main Otto error handler
|
190
|
-
error_id = SecureRandom.hex(8)
|
191
|
-
Otto.logger.error "[#{error_id}] #{e.class}: #{e.message}"
|
192
|
-
Otto.logger.debug "[#{error_id}] Backtrace: #{e.backtrace.join("\n")}" if Otto.debug
|
193
|
-
|
194
|
-
res.status = 500
|
195
|
-
res.headers['content-type'] = 'text/plain'
|
196
|
-
|
197
|
-
if Otto.env?(:dev, :development)
|
198
|
-
res.write "Server error (ID: #{error_id}). Check logs for details."
|
199
|
-
else
|
200
|
-
res.write "An error occurred. Please try again later."
|
201
|
-
end
|
202
|
-
|
203
|
-
# Add security headers if available
|
204
|
-
if otto_instance&.respond_to?(:security_config) && otto_instance.security_config
|
205
|
-
otto_instance.security_config.security_headers.each do |header, value|
|
206
|
-
res.headers[header] = value
|
207
|
-
end
|
208
|
-
end
|
209
|
-
end
|
210
|
-
|
211
|
-
res.finish
|
212
|
-
end
|
213
|
-
end
|
214
|
-
|
215
|
-
# Handler for instance methods (existing Otto pattern)
|
216
|
-
# Maintains backward compatibility for Controller#action patterns
|
217
|
-
class InstanceMethodHandler < BaseHandler
|
218
|
-
def call(env, extra_params = {})
|
219
|
-
req = Rack::Request.new(env)
|
220
|
-
res = Rack::Response.new
|
221
|
-
|
222
|
-
begin
|
223
|
-
# Apply the same extensions and processing as original Route#call
|
224
|
-
setup_request_response(req, res, env, extra_params)
|
225
|
-
|
226
|
-
# Create instance and call method (existing Otto behavior)
|
227
|
-
instance = target_class.new(req, res)
|
228
|
-
result = instance.send(route_definition.method_name)
|
229
|
-
|
230
|
-
# Only handle response if response_type is not default
|
231
|
-
if route_definition.response_type != 'default'
|
232
|
-
handle_response(result, res, {
|
233
|
-
instance: instance,
|
234
|
-
request: req
|
235
|
-
})
|
236
|
-
end
|
237
|
-
|
238
|
-
rescue => e
|
239
|
-
# Error handling - return 500 with proper headers like main Otto error handler
|
240
|
-
error_id = SecureRandom.hex(8)
|
241
|
-
Otto.logger.error "[#{error_id}] #{e.class}: #{e.message}"
|
242
|
-
Otto.logger.debug "[#{error_id}] Backtrace: #{e.backtrace.join("\n")}" if Otto.debug
|
243
|
-
|
244
|
-
res.status = 500
|
245
|
-
res.headers['content-type'] = 'text/plain'
|
246
|
-
|
247
|
-
if Otto.env?(:dev, :development)
|
248
|
-
res.write "Server error (ID: #{error_id}). Check logs for details."
|
249
|
-
else
|
250
|
-
res.write "An error occurred. Please try again later."
|
251
|
-
end
|
252
|
-
|
253
|
-
# Add security headers if available
|
254
|
-
if otto_instance&.respond_to?(:security_config) && otto_instance.security_config
|
255
|
-
otto_instance.security_config.security_headers.each do |header, value|
|
256
|
-
res.headers[header] = value
|
257
|
-
end
|
258
|
-
end
|
259
|
-
end
|
260
|
-
|
261
|
-
finalize_response(res)
|
262
|
-
end
|
263
|
-
end
|
264
|
-
|
265
|
-
# Handler for class methods (existing Otto pattern)
|
266
|
-
# Maintains backward compatibility for Controller.action patterns
|
267
|
-
class ClassMethodHandler < BaseHandler
|
268
|
-
def call(env, extra_params = {})
|
269
|
-
req = Rack::Request.new(env)
|
270
|
-
res = Rack::Response.new
|
271
|
-
|
272
|
-
begin
|
273
|
-
# Apply the same extensions and processing as original Route#call
|
274
|
-
setup_request_response(req, res, env, extra_params)
|
275
|
-
|
276
|
-
# Call class method directly (existing Otto behavior)
|
277
|
-
result = target_class.send(route_definition.method_name, req, res)
|
278
|
-
|
279
|
-
# Only handle response if response_type is not default
|
280
|
-
if route_definition.response_type != 'default'
|
281
|
-
handle_response(result, res, {
|
282
|
-
class: target_class,
|
283
|
-
request: req
|
284
|
-
})
|
285
|
-
end
|
286
|
-
|
287
|
-
rescue => e
|
288
|
-
# Error handling - return 500 with proper headers like main Otto error handler
|
289
|
-
error_id = SecureRandom.hex(8)
|
290
|
-
Otto.logger.error "[#{error_id}] #{e.class}: #{e.message}"
|
291
|
-
Otto.logger.debug "[#{error_id}] Backtrace: #{e.backtrace.join("\n")}" if Otto.debug
|
292
|
-
|
293
|
-
res.status = 500
|
294
|
-
|
295
|
-
# Content negotiation for error response
|
296
|
-
accept_header = env['HTTP_ACCEPT'].to_s
|
297
|
-
if accept_header.include?('application/json')
|
298
|
-
res.headers['content-type'] = 'application/json'
|
299
|
-
error_data = if Otto.env?(:dev, :development)
|
300
|
-
{
|
301
|
-
error: 'Internal Server Error',
|
302
|
-
message: 'Server error occurred. Check logs for details.',
|
303
|
-
error_id: error_id,
|
304
|
-
}
|
305
|
-
else
|
306
|
-
{
|
307
|
-
error: 'Internal Server Error',
|
308
|
-
message: 'An error occurred. Please try again later.',
|
309
|
-
}
|
310
|
-
end
|
311
|
-
res.write JSON.generate(error_data)
|
312
|
-
else
|
313
|
-
res.headers['content-type'] = 'text/plain'
|
314
|
-
if Otto.env?(:dev, :development)
|
315
|
-
res.write "Server error (ID: #{error_id}). Check logs for details."
|
316
|
-
else
|
317
|
-
res.write "An error occurred. Please try again later."
|
318
|
-
end
|
319
|
-
end
|
320
|
-
|
321
|
-
# Add security headers if available
|
322
|
-
if otto_instance&.respond_to?(:security_config) && otto_instance.security_config
|
323
|
-
otto_instance.security_config.security_headers.each do |header, value|
|
324
|
-
res.headers[header] = value
|
325
|
-
end
|
326
|
-
end
|
327
|
-
end
|
328
|
-
|
329
|
-
finalize_response(res)
|
330
|
-
end
|
331
|
-
end
|
332
|
-
|
333
|
-
# Custom handler for lambda/proc definitions (future extension)
|
334
|
-
class LambdaHandler < BaseHandler
|
335
|
-
def call(env, extra_params = {})
|
336
|
-
req = Rack::Request.new(env)
|
337
|
-
res = Rack::Response.new
|
338
|
-
|
339
|
-
begin
|
340
|
-
# Security: Lambda handlers require pre-configured procs from Otto instance
|
341
|
-
# This prevents code injection via eval and maintains security
|
342
|
-
handler_name = route_definition.klass_name
|
343
|
-
lambda_registry = otto_instance&.config&.dig(:lambda_handlers) || {}
|
344
|
-
|
345
|
-
lambda_proc = lambda_registry[handler_name]
|
346
|
-
unless lambda_proc.respond_to?(:call)
|
347
|
-
raise ArgumentError, "Lambda handler '#{handler_name}' not found in registry or not callable"
|
348
|
-
end
|
349
|
-
|
350
|
-
result = lambda_proc.call(req, res, extra_params)
|
351
|
-
|
352
|
-
handle_response(result, res, {
|
353
|
-
lambda: lambda_proc,
|
354
|
-
request: req
|
355
|
-
})
|
356
|
-
|
357
|
-
rescue => e
|
358
|
-
error_id = SecureRandom.hex(8)
|
359
|
-
Otto.logger.error "[#{error_id}] #{e.class}: #{e.message}"
|
360
|
-
Otto.logger.debug "[#{error_id}] Backtrace: #{e.backtrace.join("\n")}" if Otto.debug
|
361
|
-
|
362
|
-
res.status = 500
|
363
|
-
res.headers['content-type'] = 'text/plain'
|
364
|
-
|
365
|
-
if Otto.env?(:dev, :development)
|
366
|
-
res.write "Lambda handler error (ID: #{error_id}). Check logs for details."
|
367
|
-
else
|
368
|
-
res.write "An error occurred. Please try again later."
|
369
|
-
end
|
370
|
-
|
371
|
-
# Add security headers if available
|
372
|
-
if otto_instance&.respond_to?(:security_config) && otto_instance.security_config
|
373
|
-
otto_instance.security_config.security_headers.each do |header, value|
|
374
|
-
res.headers[header] = value
|
375
|
-
end
|
376
|
-
end
|
377
|
-
end
|
378
|
-
|
379
|
-
res.finish
|
380
|
-
end
|
381
|
-
end
|
10
|
+
require_relative 'route_handlers/base'
|
11
|
+
require_relative 'route_handlers/factory'
|
12
|
+
require_relative 'route_handlers/logic_class'
|
13
|
+
require_relative 'route_handlers/instance_method'
|
14
|
+
require_relative 'route_handlers/class_method'
|
15
|
+
require_relative 'route_handlers/lambda'
|
382
16
|
end
|
383
17
|
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# lib/otto/security/authentication/auth_strategy.rb
|
4
|
+
#
|
5
|
+
# Base class for all authentication strategies in Otto framework
|
6
|
+
# Provides pluggable authentication patterns that can be customized per application
|
7
|
+
|
8
|
+
class Otto
|
9
|
+
module Security
|
10
|
+
module Authentication
|
11
|
+
# Base class for all authentication strategies
|
12
|
+
class AuthStrategy
|
13
|
+
# Check if the request meets the authentication requirements
|
14
|
+
# @param env [Hash] Rack environment
|
15
|
+
# @param requirement [String] Authentication requirement string
|
16
|
+
# @return [Otto::Security::Authentication::StrategyResult, nil] StrategyResult for success, nil for failure
|
17
|
+
def authenticate(env, requirement)
|
18
|
+
raise NotImplementedError, 'Subclasses must implement #authenticate'
|
19
|
+
end
|
20
|
+
|
21
|
+
protected
|
22
|
+
|
23
|
+
# Helper to create successful strategy result
|
24
|
+
def success(user:, session: {}, auth_method: nil, **metadata)
|
25
|
+
Otto::Security::Authentication::StrategyResult.new(
|
26
|
+
session: session,
|
27
|
+
user: user,
|
28
|
+
auth_method: auth_method || self.class.name.split('::').last,
|
29
|
+
metadata: metadata
|
30
|
+
)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Helper for authentication failure - return FailureResult
|
34
|
+
def failure(reason = nil)
|
35
|
+
Otto.logger.debug "[#{self.class}] Authentication failed: #{reason}" if reason
|
36
|
+
Otto::Security::Authentication::FailureResult.new(
|
37
|
+
failure_reason: reason || 'Authentication failed',
|
38
|
+
auth_method: self.class.name.split('::').last
|
39
|
+
)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'strategy_result'
|
4
|
+
require_relative 'failure_result'
|
5
|
+
require_relative 'strategies/public_strategy'
|
6
|
+
require_relative 'strategies/role_strategy'
|
7
|
+
require_relative 'strategies/permission_strategy'
|
8
|
+
|
9
|
+
class Otto
|
10
|
+
module Security
|
11
|
+
module Authentication
|
12
|
+
# Authentication middleware that enforces route-level auth requirements
|
13
|
+
class AuthenticationMiddleware
|
14
|
+
def initialize(app, security_config = {}, config = {})
|
15
|
+
@app = app
|
16
|
+
@security_config = security_config
|
17
|
+
@config = config
|
18
|
+
@strategies = config[:auth_strategies] || {}
|
19
|
+
@default_strategy = config[:default_auth_strategy] || 'publicly'
|
20
|
+
|
21
|
+
# Add default public strategy if not provided
|
22
|
+
@strategies['publicly'] ||= Strategies::PublicStrategy.new
|
23
|
+
end
|
24
|
+
|
25
|
+
def call(env)
|
26
|
+
# Check if this route has auth requirements
|
27
|
+
route_definition = env['otto.route_definition']
|
28
|
+
|
29
|
+
# If no route definition, create anonymous result and continue
|
30
|
+
unless route_definition
|
31
|
+
env['otto.strategy_result'] = Otto::Security::Authentication::StrategyResult.anonymous(
|
32
|
+
metadata: { ip: env['REMOTE_ADDR'] }
|
33
|
+
)
|
34
|
+
return @app.call(env)
|
35
|
+
end
|
36
|
+
|
37
|
+
auth_requirement = route_definition.auth_requirement
|
38
|
+
|
39
|
+
# If no auth requirement, create anonymous result and continue
|
40
|
+
unless auth_requirement
|
41
|
+
env['otto.strategy_result'] = Otto::Security::Authentication::StrategyResult.anonymous(
|
42
|
+
metadata: { ip: env['REMOTE_ADDR'] }
|
43
|
+
)
|
44
|
+
return @app.call(env)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Find appropriate strategy
|
48
|
+
strategy = find_strategy(auth_requirement)
|
49
|
+
return auth_error_response("Unknown authentication strategy: #{auth_requirement}") unless strategy
|
50
|
+
|
51
|
+
# Perform authentication
|
52
|
+
strategy_result = strategy.authenticate(env, auth_requirement)
|
53
|
+
|
54
|
+
if strategy_result&.success?
|
55
|
+
# Success - store the strategy result directly
|
56
|
+
env['otto.strategy_result'] = strategy_result
|
57
|
+
env['otto.user'] = strategy_result.user # For convenience
|
58
|
+
env['otto.user_context'] = strategy_result.user_context # For convenience
|
59
|
+
@app.call(env)
|
60
|
+
else
|
61
|
+
# Failure - create anonymous result with failure info
|
62
|
+
failure_reason = strategy_result&.failure_reason || 'Authentication failed'
|
63
|
+
env['otto.strategy_result'] = Otto::Security::Authentication::StrategyResult.anonymous(
|
64
|
+
metadata: {
|
65
|
+
ip: env['REMOTE_ADDR'],
|
66
|
+
auth_failure: failure_reason,
|
67
|
+
attempted_strategy: auth_requirement,
|
68
|
+
}
|
69
|
+
)
|
70
|
+
auth_error_response(failure_reason)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
def find_strategy(requirement)
|
77
|
+
# Try exact match first - this has highest priority
|
78
|
+
return @strategies[requirement] if @strategies[requirement]
|
79
|
+
|
80
|
+
# For colon-separated requirements like "role:admin", try prefix match
|
81
|
+
if requirement.include?(':')
|
82
|
+
prefix = requirement.split(':', 2).first
|
83
|
+
|
84
|
+
# Check if we have a strategy registered for the prefix
|
85
|
+
prefix_strategy = @strategies[prefix]
|
86
|
+
return prefix_strategy if prefix_strategy
|
87
|
+
|
88
|
+
# Try fallback patterns for role: and permission: requirements
|
89
|
+
if requirement.start_with?('role:')
|
90
|
+
return @strategies['role'] || Strategies::RoleStrategy.new([])
|
91
|
+
elsif requirement.start_with?('permission:')
|
92
|
+
return @strategies['permission'] || Strategies::PermissionStrategy.new([])
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
nil
|
97
|
+
end
|
98
|
+
|
99
|
+
def auth_error_response(message)
|
100
|
+
body = JSON.generate({
|
101
|
+
error: 'Authentication Required',
|
102
|
+
message: message,
|
103
|
+
timestamp: Time.now.to_i,
|
104
|
+
})
|
105
|
+
|
106
|
+
headers = {
|
107
|
+
'Content-Type' => 'application/json',
|
108
|
+
'Content-Length' => body.bytesize.to_s,
|
109
|
+
}
|
110
|
+
|
111
|
+
# Add security headers if available from config hash or Otto instance
|
112
|
+
if @config.is_a?(Hash) && @config[:security_headers]
|
113
|
+
headers.merge!(@config[:security_headers])
|
114
|
+
elsif @config.respond_to?(:security_config) && @config.security_config
|
115
|
+
headers.merge!(@config.security_config.security_headers)
|
116
|
+
end
|
117
|
+
|
118
|
+
[401, headers, [body]]
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# lib/otto/security/authentication/failure_result.rb
|
4
|
+
|
5
|
+
class Otto
|
6
|
+
module Security
|
7
|
+
module Authentication
|
8
|
+
# Failure result for authentication failures
|
9
|
+
FailureResult = Data.define(:failure_reason, :auth_method) do
|
10
|
+
def success?
|
11
|
+
false
|
12
|
+
end
|
13
|
+
|
14
|
+
def failure?
|
15
|
+
true
|
16
|
+
end
|
17
|
+
|
18
|
+
def authenticated?
|
19
|
+
false
|
20
|
+
end
|
21
|
+
|
22
|
+
def anonymous?
|
23
|
+
true
|
24
|
+
end
|
25
|
+
|
26
|
+
def user_context
|
27
|
+
{}
|
28
|
+
end
|
29
|
+
|
30
|
+
def inspect
|
31
|
+
"#<FailureResult reason=#{failure_reason.inspect} method=#{auth_method}>"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../auth_strategy'
|
4
|
+
|
5
|
+
class Otto
|
6
|
+
module Security
|
7
|
+
module Authentication
|
8
|
+
module Strategies
|
9
|
+
# API key authentication strategy
|
10
|
+
class APIKeyStrategy < AuthStrategy
|
11
|
+
def initialize(api_keys: [], header_name: 'X-API-Key', param_name: 'api_key')
|
12
|
+
@api_keys = Array(api_keys)
|
13
|
+
@header_name = header_name
|
14
|
+
@param_name = param_name
|
15
|
+
end
|
16
|
+
|
17
|
+
def authenticate(env, _requirement)
|
18
|
+
# Try header first, then query parameter
|
19
|
+
api_key = env["HTTP_#{@header_name.upcase.tr('-', '_')}"]
|
20
|
+
|
21
|
+
if api_key.nil?
|
22
|
+
request = Rack::Request.new(env)
|
23
|
+
api_key = request.params[@param_name]
|
24
|
+
end
|
25
|
+
|
26
|
+
return failure('No API key provided') unless api_key
|
27
|
+
|
28
|
+
if @api_keys.empty? || @api_keys.include?(api_key)
|
29
|
+
# Create a simple user hash for API key authentication
|
30
|
+
user_data = { api_key: api_key }
|
31
|
+
success(user: user_data, api_key: api_key)
|
32
|
+
else
|
33
|
+
failure('Invalid API key')
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../auth_strategy'
|
4
|
+
|
5
|
+
class Otto
|
6
|
+
module Security
|
7
|
+
module Authentication
|
8
|
+
module Strategies
|
9
|
+
# Permission-based authentication strategy
|
10
|
+
class PermissionStrategy < AuthStrategy
|
11
|
+
def initialize(required_permissions, session_key: 'user_permissions')
|
12
|
+
@required_permissions = Array(required_permissions)
|
13
|
+
@session_key = session_key
|
14
|
+
end
|
15
|
+
|
16
|
+
def authenticate(env, requirement)
|
17
|
+
session = env['rack.session']
|
18
|
+
return failure('No session available') unless session
|
19
|
+
|
20
|
+
user_permissions = session[@session_key] || []
|
21
|
+
user_permissions = Array(user_permissions)
|
22
|
+
|
23
|
+
# Create user data from session
|
24
|
+
user_data = { user_permissions: user_permissions, session: session }
|
25
|
+
|
26
|
+
# Extract permission from requirement (e.g., "permission:write" -> "write")
|
27
|
+
required_permission = requirement.split(':', 2).last
|
28
|
+
|
29
|
+
if user_permissions.include?(required_permission)
|
30
|
+
success(user: user_data, user_permissions: user_permissions, required_permission: required_permission)
|
31
|
+
else
|
32
|
+
failure("Insufficient privileges - requires permission: #{required_permission}")
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def user_context(env)
|
37
|
+
session = env['rack.session']
|
38
|
+
return {} unless session
|
39
|
+
|
40
|
+
user_permissions = session[@session_key] || []
|
41
|
+
{ user_permissions: Array(user_permissions) }
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|