rack_jwt_aegis 0.0.0 → 1.0.1
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/.rubocop.yml +9 -0
- data/.yard/yard_gfm_config.rb +21 -0
- data/.yardopts +16 -0
- data/CHANGELOG.md +243 -0
- data/README.md +408 -53
- data/Rakefile +52 -0
- data/bin/console +11 -0
- data/bin/docs +20 -0
- data/bin/setup +8 -0
- data/exe/rack_jwt_aegis +235 -0
- data/lib/rack_jwt_aegis/configuration.rb +225 -44
- data/lib/rack_jwt_aegis/debug_logger.rb +51 -0
- data/lib/rack_jwt_aegis/jwt_validator.rb +56 -14
- data/lib/rack_jwt_aegis/middleware.rb +75 -8
- data/lib/rack_jwt_aegis/multi_tenant_validator.rb +43 -18
- data/lib/rack_jwt_aegis/rbac_manager.rb +320 -80
- data/lib/rack_jwt_aegis/request_context.rb +64 -23
- data/lib/rack_jwt_aegis/version.rb +1 -1
- data/lib/rack_jwt_aegis.rb +37 -1
- metadata +25 -13
- data/examples/basic_usage.rb +0 -85
- /data/sig/{rack_jwt_bastion.rbs → rack_jwt_aegis.rbs} +0 -0
@@ -1,15 +1,37 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module RackJwtAegis
|
4
|
+
# Role-Based Access Control (RBAC) manager
|
5
|
+
#
|
6
|
+
# Handles authorization by checking user permissions against cached RBAC data.
|
7
|
+
# Supports both simple boolean permissions and complex permission structures.
|
8
|
+
# Uses a two-tier caching system for performance optimization.
|
9
|
+
#
|
10
|
+
# @author Ken Camajalan Demanawa
|
11
|
+
# @since 0.1.0
|
12
|
+
#
|
13
|
+
# @example Basic usage
|
14
|
+
# config = Configuration.new(jwt_secret: 'secret', rbac_enabled: true, rbac_cache_store: :memory)
|
15
|
+
# manager = RbacManager.new(config)
|
16
|
+
# manager.authorize(request, jwt_payload)
|
4
17
|
class RbacManager
|
18
|
+
include DebugLogger
|
19
|
+
|
5
20
|
CACHE_TTL = 300 # 5 minutes default cache TTL
|
6
|
-
LAST_UPDATE_KEY = 'last-update'
|
7
21
|
|
22
|
+
# Initialize the RBAC manager
|
23
|
+
#
|
24
|
+
# @param config [Configuration] the configuration instance
|
8
25
|
def initialize(config)
|
9
26
|
@config = config
|
10
27
|
setup_cache_adapters
|
11
28
|
end
|
12
29
|
|
30
|
+
# Authorize a request against RBAC permissions
|
31
|
+
#
|
32
|
+
# @param request [Rack::Request] the incoming request
|
33
|
+
# @param payload [Hash] the JWT payload containing user information
|
34
|
+
# @raise [AuthorizationError] if user lacks sufficient permissions
|
13
35
|
def authorize(request, payload)
|
14
36
|
user_id = payload[@config.payload_key(:user_id).to_s]
|
15
37
|
raise AuthorizationError, 'User ID missing from JWT payload' if user_id.nil?
|
@@ -61,137 +83,355 @@ module RackJwtAegis
|
|
61
83
|
end
|
62
84
|
|
63
85
|
def build_permission_key(user_id, request)
|
64
|
-
"#{
|
86
|
+
full_url = "#{request.host}#{request.path}"
|
87
|
+
"#{user_id}:#{full_url}:#{request.request_method.downcase}"
|
65
88
|
end
|
66
89
|
|
67
90
|
def check_cached_permission(permission_key)
|
68
91
|
return nil unless @permission_cache
|
69
92
|
|
70
93
|
begin
|
71
|
-
# Get
|
72
|
-
|
73
|
-
return nil if
|
94
|
+
# Get the user permissions cache using new format
|
95
|
+
user_permissions = @permission_cache.read('user_permissions')
|
96
|
+
return nil if user_permissions.nil? || !user_permissions.is_a?(Hash)
|
97
|
+
|
98
|
+
# First check: If RBAC permissions were updated recently, nuke ALL cached permissions
|
99
|
+
rbac_last_update = rbac_last_update_timestamp
|
100
|
+
if rbac_last_update
|
101
|
+
current_time = Time.now.to_i
|
102
|
+
rbac_update_age = current_time - rbac_last_update
|
103
|
+
|
104
|
+
# If RBAC was updated within the TTL period, all cached permissions are invalid
|
105
|
+
if rbac_update_age <= @config.user_permissions_ttl
|
106
|
+
nuke_user_permissions_cache("RBAC permissions updated recently (#{rbac_update_age}s ago, within TTL)")
|
107
|
+
return nil
|
108
|
+
end
|
109
|
+
end
|
74
110
|
|
75
|
-
# Check if
|
76
|
-
|
77
|
-
|
111
|
+
# Check if permission exists in new format: {"user_id:full_url:method" => timestamp}
|
112
|
+
cached_timestamp = user_permissions[permission_key]
|
113
|
+
return nil unless cached_timestamp.is_a?(Integer)
|
78
114
|
|
79
|
-
|
115
|
+
current_time = Time.now.to_i
|
116
|
+
permission_age = current_time - cached_timestamp
|
80
117
|
|
81
|
-
|
82
|
-
|
118
|
+
# Second check: TTL expiration
|
119
|
+
if permission_age > @config.user_permissions_ttl
|
120
|
+
# This specific permission expired due to TTL
|
121
|
+
remove_stale_permission(permission_key,
|
122
|
+
"TTL expired (#{permission_age}s > #{@config.user_permissions_ttl}s)")
|
83
123
|
return nil
|
84
|
-
|
85
124
|
end
|
86
125
|
|
87
|
-
#
|
88
|
-
|
89
|
-
|
126
|
+
# Permission is fresh
|
127
|
+
debug_log("Cache hit: #{permission_key} (permission age: \
|
128
|
+
#{permission_age}s, RBAC age: #{rbac_update_age || 'unknown'}s)".squeeze)
|
129
|
+
true
|
90
130
|
rescue CacheError => e
|
91
131
|
# Log cache error but don't fail the request
|
92
|
-
|
132
|
+
debug_log("RbacManager cache read error: #{e.message}", :warn)
|
93
133
|
nil
|
94
134
|
end
|
95
135
|
end
|
96
136
|
|
97
137
|
def check_rbac_permission(user_id, request)
|
98
|
-
|
99
|
-
rbac_key = build_rbac_key(user_id, request.host, request.path, request.request_method)
|
100
|
-
|
101
|
-
# Check RBAC cache store for permission
|
102
|
-
permission_data = @rbac_cache.read(rbac_key)
|
138
|
+
rbac_data = @rbac_cache.read('permissions')
|
103
139
|
|
104
|
-
if
|
105
|
-
|
106
|
-
|
107
|
-
else
|
108
|
-
# Permission data found - check if it grants access
|
109
|
-
case permission_data
|
110
|
-
when true, 'true', 1, '1'
|
111
|
-
true
|
112
|
-
when false, 'false', 0, '0'
|
113
|
-
false
|
114
|
-
else
|
115
|
-
# Complex permission data - delegate to custom logic if available
|
116
|
-
evaluate_complex_permission?(permission_data, user_id, request)
|
117
|
-
end
|
140
|
+
# Check if RBAC data exists and is valid
|
141
|
+
if rbac_data.is_a?(Hash) && validate_rbac_cache_format(rbac_data)
|
142
|
+
return check_rbac_format?(user_id, request, rbac_data)
|
118
143
|
end
|
144
|
+
|
145
|
+
# No valid RBAC data found
|
146
|
+
false
|
119
147
|
rescue CacheError => e
|
120
148
|
# Cache error - fail secure (deny access)
|
121
|
-
|
149
|
+
debug_log("RbacManager RBAC cache error: #{e.message}", :warn)
|
122
150
|
false
|
123
151
|
end
|
124
152
|
|
125
153
|
def cache_permission_result(permission_key, has_permission)
|
126
154
|
return unless @permission_cache
|
155
|
+
return unless has_permission # Only cache positive permissions
|
127
156
|
|
128
157
|
begin
|
129
158
|
current_time = Time.now.to_i
|
130
|
-
cache_entry = {
|
131
|
-
'permission' => has_permission,
|
132
|
-
'timestamp' => current_time,
|
133
|
-
}
|
134
159
|
|
135
|
-
|
160
|
+
# Get existing user permissions cache or create new one
|
161
|
+
user_permissions = @permission_cache.read('user_permissions') || {}
|
162
|
+
|
163
|
+
# Store permission with new format: {"user_id:full_url:method" => timestamp}
|
164
|
+
user_permissions[permission_key] = current_time
|
165
|
+
|
166
|
+
# Write back to cache
|
167
|
+
@permission_cache.write('user_permissions', user_permissions, expires_in: CACHE_TTL)
|
168
|
+
|
169
|
+
debug_log("Cached permission: #{permission_key} => #{current_time}")
|
136
170
|
rescue CacheError => e
|
137
171
|
# Log cache error but don't fail the request
|
138
|
-
|
172
|
+
debug_log("RbacManager permission cache write error: #{e.message}", :warn)
|
139
173
|
end
|
140
174
|
end
|
141
175
|
|
142
|
-
def
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
176
|
+
def check_rbac_format?(user_id, request, rbac_data)
|
177
|
+
# Extract user roles from JWT payload
|
178
|
+
user_roles = extract_user_roles_from_request(request)
|
179
|
+
if user_roles.nil? || user_roles.empty?
|
180
|
+
debug_log('RbacManager: No user roles found in request context', :warn)
|
181
|
+
return false
|
182
|
+
end
|
183
|
+
|
184
|
+
# Check permissions for each user role
|
185
|
+
rbac_data['permissions'].each do |role_permissions|
|
186
|
+
user_roles.each do |role_id|
|
187
|
+
next unless role_permissions.key?(role_id.to_s) || role_permissions.key?(role_id.to_i)
|
188
|
+
|
189
|
+
permissions = role_permissions[role_id.to_s] || role_permissions[role_id.to_i]
|
190
|
+
matched_permission = find_matching_permission(permissions, request)
|
191
|
+
|
192
|
+
next unless matched_permission
|
193
|
+
|
194
|
+
# Cache this specific permission match for faster future lookups
|
195
|
+
if @permission_cache && @config.cache_write_enabled?
|
196
|
+
cache_permission_match(user_id, request, role_id, matched_permission)
|
197
|
+
end
|
198
|
+
return true
|
199
|
+
end
|
200
|
+
end
|
148
201
|
|
149
|
-
|
150
|
-
# Standard RBAC key format as defined in architecture
|
151
|
-
"#{user_id}:#{host}:#{path}:#{method}"
|
202
|
+
false
|
152
203
|
end
|
153
204
|
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
205
|
+
# Get RBAC permissions collection last_update timestamp
|
206
|
+
def rbac_last_update_timestamp
|
207
|
+
return nil unless @rbac_cache
|
208
|
+
|
209
|
+
begin
|
210
|
+
rbac_data = @rbac_cache.read('permissions')
|
211
|
+
if rbac_data.is_a?(Hash) && (rbac_data['last_update'] || rbac_data[:last_update])
|
212
|
+
return rbac_data['last_update'] || rbac_data[:last_update]
|
213
|
+
end
|
214
|
+
|
215
|
+
nil
|
216
|
+
rescue CacheError => e
|
217
|
+
debug_log("RbacManager RBAC last-update read error: #{e.message}", :warn)
|
218
|
+
nil
|
166
219
|
end
|
167
220
|
end
|
168
221
|
|
169
|
-
|
170
|
-
|
222
|
+
# Remove a specific stale permission
|
223
|
+
def remove_stale_permission(permission_key, reason)
|
224
|
+
return unless @permission_cache
|
225
|
+
|
226
|
+
begin
|
227
|
+
user_permissions = @permission_cache.read('user_permissions')
|
228
|
+
return unless user_permissions.is_a?(Hash)
|
229
|
+
|
230
|
+
# Remove the specific permission key
|
231
|
+
user_permissions.delete(permission_key)
|
171
232
|
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
233
|
+
# If no permissions remain, remove the entire cache
|
234
|
+
if user_permissions.empty?
|
235
|
+
@permission_cache.delete('user_permissions')
|
236
|
+
debug_log("Removed last permission, cleared entire cache: #{reason}")
|
237
|
+
else
|
238
|
+
# Update the cache with the modified permissions
|
239
|
+
@permission_cache.write('user_permissions', user_permissions, expires_in: CACHE_TTL)
|
240
|
+
debug_log("Removed stale permission #{permission_key}: #{reason}")
|
241
|
+
end
|
242
|
+
rescue CacheError => e
|
243
|
+
debug_log("RbacManager stale permission removal error: #{e.message}", :warn)
|
176
244
|
end
|
245
|
+
end
|
246
|
+
|
247
|
+
# Nuke (delete) the entire user permissions cache
|
248
|
+
def nuke_user_permissions_cache(reason)
|
249
|
+
return unless @permission_cache
|
177
250
|
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
251
|
+
begin
|
252
|
+
@permission_cache.delete('user_permissions')
|
253
|
+
debug_log("Nuked user permissions cache: #{reason}")
|
254
|
+
rescue CacheError => e
|
255
|
+
debug_log("RbacManager cache nuke error: #{e.message}", :warn)
|
183
256
|
end
|
257
|
+
end
|
258
|
+
|
259
|
+
# Extract user roles from request context (stored by middleware)
|
260
|
+
def extract_user_roles_from_request(request)
|
261
|
+
# Check if roles are stored in request environment by middleware
|
262
|
+
request.env['rack_jwt_aegis.user_roles']
|
263
|
+
end
|
184
264
|
|
185
|
-
|
186
|
-
|
265
|
+
# Check if any role permission matches the request
|
266
|
+
def check_role_permissions?(permissions, request)
|
267
|
+
return false unless permissions.is_a?(Array)
|
268
|
+
|
269
|
+
request_path = extract_api_path_from_request(request)
|
270
|
+
request_method = request.request_method.downcase
|
271
|
+
|
272
|
+
permissions.each do |permission|
|
273
|
+
return true if permission_matches?(permission, request_path, request_method)
|
274
|
+
end
|
187
275
|
|
188
|
-
# Default deny for unknown hash structure
|
189
276
|
false
|
190
277
|
end
|
191
278
|
|
192
|
-
|
193
|
-
|
194
|
-
|
279
|
+
# Find the first matching permission for the request (returns the permission string or nil)
|
280
|
+
def find_matching_permission(permissions, request)
|
281
|
+
return nil unless permissions.is_a?(Array)
|
282
|
+
|
283
|
+
request_path = extract_api_path_from_request(request)
|
284
|
+
request_method = request.request_method.downcase
|
285
|
+
|
286
|
+
permissions.each do |permission|
|
287
|
+
return permission if permission_matches?(permission, request_path, request_method)
|
288
|
+
end
|
289
|
+
|
290
|
+
nil
|
291
|
+
end
|
292
|
+
|
293
|
+
# Cache the specific permission match for faster future lookups
|
294
|
+
# Format: {"user_id:full_url:method" => timestamp}
|
295
|
+
def cache_permission_match(user_id, request, _role_id, _matched_permission)
|
296
|
+
return unless @permission_cache
|
297
|
+
|
298
|
+
begin
|
299
|
+
current_time = Time.now.to_i
|
300
|
+
|
301
|
+
# Build the permission key in new format
|
302
|
+
host = request.host || 'localhost'
|
303
|
+
full_url = "#{host}#{request.path}"
|
304
|
+
method = request.request_method.downcase
|
305
|
+
permission_key = "#{user_id}:#{full_url}:#{method}"
|
306
|
+
|
307
|
+
# Get existing user permissions cache or create new one
|
308
|
+
user_permissions = @permission_cache.read('user_permissions') || {}
|
309
|
+
|
310
|
+
# Store permission with new format
|
311
|
+
user_permissions[permission_key] = current_time
|
312
|
+
|
313
|
+
# Write back to cache
|
314
|
+
@permission_cache.write('user_permissions', user_permissions, expires_in: CACHE_TTL)
|
315
|
+
|
316
|
+
debug_log("Cached user permission: #{permission_key} => #{current_time}")
|
317
|
+
rescue CacheError => e
|
318
|
+
# Log cache error but don't fail the request
|
319
|
+
debug_log("RbacManager permission cache write error: #{e.message}", :warn)
|
320
|
+
end
|
321
|
+
end
|
322
|
+
|
323
|
+
# Extract the API path portion from the full request path
|
324
|
+
# Removes subdomain and pathname slug parts to get the resource endpoint
|
325
|
+
def extract_api_path_from_request(request)
|
326
|
+
path = request.path
|
327
|
+
|
328
|
+
# Remove API prefix and pathname slug pattern if configured
|
329
|
+
if @config.pathname_slug_pattern
|
330
|
+
# Extract the resource path after the pathname slug
|
331
|
+
match = path.match(@config.pathname_slug_pattern)
|
332
|
+
if match&.captures&.any?
|
333
|
+
# Get everything after the slug pattern
|
334
|
+
slug_part = match[0]
|
335
|
+
resource_path = path.sub(slug_part, '')
|
336
|
+
return resource_path.start_with?('/') ? resource_path[1..] : resource_path
|
337
|
+
end
|
338
|
+
end
|
339
|
+
|
340
|
+
# Fallback: remove common API prefixes
|
341
|
+
path = path.sub(%r{^/api/v\d+/}, '')
|
342
|
+
path = path.sub(%r{^/api/}, '')
|
343
|
+
path.sub(%r{^/}, '')
|
344
|
+
end
|
345
|
+
|
346
|
+
# Check if a permission string matches the request
|
347
|
+
def permission_matches?(permission, resource_path, request_method)
|
348
|
+
return false unless permission.is_a?(String)
|
349
|
+
|
350
|
+
# Parse permission format: "resource-endpoint:http-method"
|
351
|
+
parts = permission.split(':')
|
352
|
+
return false unless parts.length == 2
|
353
|
+
|
354
|
+
permission_path, permission_method = parts
|
355
|
+
|
356
|
+
# Check if method matches
|
357
|
+
return false unless method_matches?(permission_method, request_method)
|
358
|
+
|
359
|
+
# Check if path matches (handle both literal and regex patterns)
|
360
|
+
path_matches?(permission_path, resource_path)
|
361
|
+
end
|
362
|
+
|
363
|
+
# Check if HTTP method matches
|
364
|
+
def method_matches?(permission_method, request_method)
|
365
|
+
permission_method = permission_method.downcase
|
366
|
+
|
367
|
+
# Wildcard method matches all
|
368
|
+
return true if permission_method == '*'
|
369
|
+
|
370
|
+
# Exact method match
|
371
|
+
permission_method == request_method
|
372
|
+
end
|
373
|
+
|
374
|
+
# Check if path matches (handles both literal strings and regex patterns)
|
375
|
+
def path_matches?(permission_path, resource_path)
|
376
|
+
# Handle regex pattern format: "%r{pattern}"
|
377
|
+
if permission_path.start_with?('%r{') && permission_path.end_with?('}')
|
378
|
+
regex_pattern = permission_path[3..-2] # Remove %r{ and }
|
379
|
+
begin
|
380
|
+
regex = Regexp.new(regex_pattern)
|
381
|
+
return regex.match?(resource_path)
|
382
|
+
rescue RegexpError => e
|
383
|
+
debug_log("RbacManager: Invalid regex pattern '#{regex_pattern}': #{e.message}", :warn)
|
384
|
+
return false
|
385
|
+
end
|
386
|
+
end
|
387
|
+
|
388
|
+
# Exact string match
|
389
|
+
permission_path == resource_path
|
390
|
+
end
|
391
|
+
|
392
|
+
# Validate RBAC cache format according to specification
|
393
|
+
# Expected format:
|
394
|
+
# {
|
395
|
+
# last_update: timestamp,
|
396
|
+
# permissions: [
|
397
|
+
# {role-id: ["{resource-endpoint}:{http-method}"]}
|
398
|
+
# ]
|
399
|
+
# }
|
400
|
+
def validate_rbac_cache_format(rbac_data)
|
401
|
+
return false unless rbac_data.is_a?(Hash)
|
402
|
+
|
403
|
+
# Check required fields
|
404
|
+
return false unless rbac_data.key?('last_update') || rbac_data.key?(:last_update)
|
405
|
+
return false unless rbac_data.key?('permissions') || rbac_data.key?(:permissions)
|
406
|
+
|
407
|
+
# Get permissions array
|
408
|
+
permissions = rbac_data['permissions'] || rbac_data[:permissions]
|
409
|
+
return false unless permissions.is_a?(Array)
|
410
|
+
|
411
|
+
# Validate each permission entry
|
412
|
+
permissions.each do |permission_entry|
|
413
|
+
return false unless permission_entry.is_a?(Hash)
|
414
|
+
|
415
|
+
# Each entry should have at least one role-id key
|
416
|
+
return false if permission_entry.empty?
|
417
|
+
|
418
|
+
# Validate permission values are arrays of strings
|
419
|
+
permission_entry.each_value do |role_permissions|
|
420
|
+
return false unless role_permissions.is_a?(Array)
|
421
|
+
|
422
|
+
# Each permission should be a string in format "endpoint:method"
|
423
|
+
role_permissions.each do |permission|
|
424
|
+
return false unless permission.is_a?(String)
|
425
|
+
# Permission must include ':' (resource:method format)
|
426
|
+
return false unless permission.include?(':')
|
427
|
+
end
|
428
|
+
end
|
429
|
+
end
|
430
|
+
|
431
|
+
true
|
432
|
+
rescue StandardError => e
|
433
|
+
debug_log("RbacManager: Cache format validation error: #{e.message}", :warn)
|
434
|
+
false
|
195
435
|
end
|
196
436
|
end
|
197
437
|
end
|
@@ -1,19 +1,43 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module RackJwtAegis
|
4
|
+
# Request context manager for storing JWT authentication data in Rack env
|
5
|
+
#
|
6
|
+
# Stores authenticated user and tenant information in the Rack environment
|
7
|
+
# hash for easy access by downstream application code. Provides both
|
8
|
+
# instance methods for setting context and class methods for reading.
|
9
|
+
#
|
10
|
+
# @author Ken Camajalan Demanawa
|
11
|
+
# @since 0.1.0
|
12
|
+
#
|
13
|
+
# @example Setting context (done by middleware)
|
14
|
+
# context = RequestContext.new(config)
|
15
|
+
# context.set_context(env, jwt_payload)
|
16
|
+
#
|
17
|
+
# @example Reading context in application
|
18
|
+
# user_id = RequestContext.user_id(request.env)
|
19
|
+
# tenant_id = RequestContext.tenant_id(request.env)
|
20
|
+
# authenticated = RequestContext.authenticated?(request.env)
|
4
21
|
class RequestContext
|
5
22
|
# Standard environment keys for JWT data
|
6
23
|
JWT_PAYLOAD_KEY = 'rack_jwt_aegis.payload'
|
7
24
|
USER_ID_KEY = 'rack_jwt_aegis.user_id'
|
8
|
-
|
9
|
-
|
10
|
-
|
25
|
+
TENANT_ID_KEY = 'rack_jwt_aegis.tenant_id'
|
26
|
+
SUBDOMAIN_KEY = 'rack_jwt_aegis.subdomain'
|
27
|
+
PATHNAME_SLUGS_KEY = 'rack_jwt_aegis.pathname_slugs'
|
11
28
|
AUTHENTICATED_KEY = 'rack_jwt_aegis.authenticated'
|
12
29
|
|
30
|
+
# Initialize the request context manager
|
31
|
+
#
|
32
|
+
# @param config [Configuration] the configuration instance
|
13
33
|
def initialize(config)
|
14
34
|
@config = config
|
15
35
|
end
|
16
36
|
|
37
|
+
# Set JWT authentication context in the Rack environment
|
38
|
+
#
|
39
|
+
# @param env [Hash] the Rack environment hash
|
40
|
+
# @param payload [Hash] the validated JWT payload
|
17
41
|
def set_context(env, payload)
|
18
42
|
# Set the full payload
|
19
43
|
env[JWT_PAYLOAD_KEY] = payload
|
@@ -27,40 +51,57 @@ module RackJwtAegis
|
|
27
51
|
end
|
28
52
|
|
29
53
|
# Class methods for easy access from application code
|
54
|
+
|
55
|
+
# Check if the request is authenticated
|
56
|
+
#
|
57
|
+
# @param env [Hash] the Rack environment hash
|
58
|
+
# @return [Boolean] true if request is authenticated
|
30
59
|
def self.authenticated?(env)
|
31
60
|
!!env[AUTHENTICATED_KEY]
|
32
61
|
end
|
33
62
|
|
63
|
+
# Get the full JWT payload from the request
|
64
|
+
#
|
65
|
+
# @param env [Hash] the Rack environment hash
|
66
|
+
# @return [Hash, nil] the JWT payload or nil if not authenticated
|
34
67
|
def self.payload(env)
|
35
68
|
env[JWT_PAYLOAD_KEY]
|
36
69
|
end
|
37
70
|
|
71
|
+
# Get the authenticated user ID
|
72
|
+
#
|
73
|
+
# @param env [Hash] the Rack environment hash
|
74
|
+
# @return [String, Integer, nil] the user ID or nil if not available
|
38
75
|
def self.user_id(env)
|
39
76
|
env[USER_ID_KEY]
|
40
77
|
end
|
41
78
|
|
42
|
-
|
43
|
-
|
79
|
+
# Get the tenant ID
|
80
|
+
#
|
81
|
+
# @param env [Hash] the Rack environment hash
|
82
|
+
# @return [String, Integer, nil] the tenant ID or nil if not available
|
83
|
+
def self.tenant_id(env)
|
84
|
+
env[TENANT_ID_KEY]
|
44
85
|
end
|
45
86
|
|
46
|
-
def self.
|
47
|
-
env[
|
87
|
+
def self.subdomain(env)
|
88
|
+
env[SUBDOMAIN_KEY]
|
48
89
|
end
|
49
90
|
|
50
|
-
def self.
|
51
|
-
env[
|
91
|
+
def self.pathname_slugs(env)
|
92
|
+
env[PATHNAME_SLUGS_KEY] || []
|
52
93
|
end
|
53
94
|
|
54
95
|
def self.current_user_id(request)
|
55
96
|
user_id(request.env)
|
56
97
|
end
|
57
98
|
|
58
|
-
def self.
|
59
|
-
|
99
|
+
def self.current_tenant_id(request)
|
100
|
+
tenant_id(request.env)
|
60
101
|
end
|
61
102
|
|
62
103
|
def self.has_company_access?(env, company_slug)
|
63
|
-
|
104
|
+
pathname_slugs(env).include?(company_slug)
|
64
105
|
end
|
65
106
|
|
66
107
|
private
|
@@ -74,27 +115,27 @@ module RackJwtAegis
|
|
74
115
|
|
75
116
|
def set_tenant_context(env, payload)
|
76
117
|
# Set company group information
|
77
|
-
if @config.validate_subdomain? || @config.payload_mapping.key?(:
|
78
|
-
|
79
|
-
|
80
|
-
env[
|
118
|
+
if @config.validate_subdomain? || @config.payload_mapping.key?(:tenant_id)
|
119
|
+
tenant_id_key = @config.payload_key(:tenant_id).to_s
|
120
|
+
tenant_id = payload[tenant_id_key]
|
121
|
+
env[TENANT_ID_KEY] = tenant_id
|
81
122
|
end
|
82
123
|
|
83
124
|
if @config.validate_subdomain?
|
84
|
-
company_domain_key = @config.payload_key(:
|
125
|
+
company_domain_key = @config.payload_key(:subdomain).to_s
|
85
126
|
company_domain = payload[company_domain_key]
|
86
|
-
env[
|
127
|
+
env[SUBDOMAIN_KEY] = company_domain
|
87
128
|
end
|
88
129
|
|
89
130
|
# Set company slugs for sub-level tenant access
|
90
|
-
return unless @config.
|
131
|
+
return unless @config.validate_pathname_slug? || @config.payload_mapping.key?(:pathname_slugs)
|
91
132
|
|
92
|
-
|
93
|
-
|
133
|
+
pathname_slugs_key = @config.payload_key(:pathname_slugs).to_s
|
134
|
+
pathname_slugs = payload[pathname_slugs_key]
|
94
135
|
|
95
136
|
# Ensure it's an array
|
96
|
-
|
97
|
-
env[
|
137
|
+
pathname_slugs = Array(pathname_slugs) if pathname_slugs
|
138
|
+
env[PATHNAME_SLUGS_KEY] = pathname_slugs || []
|
98
139
|
end
|
99
140
|
end
|
100
141
|
end
|