rack_jwt_aegis 1.0.2 โ 1.1.0
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/CHANGELOG.md +122 -0
- data/README.md +18 -21
- data/lib/rack_jwt_aegis/debug_logger.rb +1 -17
- data/lib/rack_jwt_aegis/jwt_validator.rb +16 -23
- data/lib/rack_jwt_aegis/middleware.rb +5 -2
- data/lib/rack_jwt_aegis/rbac_manager.rb +43 -41
- data/lib/rack_jwt_aegis/request_context.rb +7 -26
- data/lib/rack_jwt_aegis/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0e776985ff922d83b9ac75821698125a565dce342be61bd4bf6012fc97dfd82c
|
4
|
+
data.tar.gz: 7bbb4006689d39ea9ec47ad869b01468bbfbddfb0ecbaa430d28ac96f1fd33b7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 64eb785ccd161bf7f7a7e0ce2b9840007885778b47d35a5528fe586ebcc160aa2f3d356e92868f8931c0789603a78fbc1feef56d6141dbeb322611330404b77a
|
7
|
+
data.tar.gz: 84f45c9c58aed4eadbcfd8a1d78d1a3e0cdd47c9f602ae9047e847738c671c0513dacfc9298207456fa969a44045ec6b8b8ff29fea3e0c29f76357aa4933ddaf
|
data/CHANGELOG.md
CHANGED
@@ -5,6 +5,126 @@ All notable changes to this project will be documented in this file.
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
7
7
|
|
8
|
+
## [1.1.0] - 2025-08-14
|
9
|
+
|
10
|
+
### ๐ Added
|
11
|
+
|
12
|
+
#### Enhanced RBAC Cache Format
|
13
|
+
|
14
|
+
- **Improved Performance**: Changed RBAC cache format from array-based to flat object structure for O(1) role lookup
|
15
|
+
- **Before**: `"permissions": [{ "role-id": ["resource:method"] }]` (O(n) array iteration)
|
16
|
+
- **After**: `"permissions": { "role-id": ["resource:method"] }` (O(1) direct access)
|
17
|
+
- **Developer Experience**: Enhanced error detection with descriptive ConfigurationError exceptions
|
18
|
+
- Catches common migration mistakes when upgrading from array to object format
|
19
|
+
- Provides clear error messages with expected format examples
|
20
|
+
- Helps developers quickly identify and fix RBAC configuration issues
|
21
|
+
|
22
|
+
#### Documentation Improvements
|
23
|
+
|
24
|
+
- **Clean Documentation**: Fixed YARD documentation rendering issues with improved Markdown processing
|
25
|
+
- Added Redcarpet gem for better Markdown parsing in YARD
|
26
|
+
- Removed complex Jekyll integration that was causing rendering issues
|
27
|
+
- Maintained YARD for comprehensive API documentation with cleaner output
|
28
|
+
- **API Documentation**: Enhanced code comments for better YARD rendering
|
29
|
+
- Updated key method documentation with clearer parameter descriptions
|
30
|
+
- Improved examples and usage patterns in documentation
|
31
|
+
- Better integration with YARD's HTML generation
|
32
|
+
|
33
|
+
### ๐ง Fixed
|
34
|
+
|
35
|
+
#### RBAC System Improvements
|
36
|
+
|
37
|
+
- **Cache Format Validation**: Added explicit validation for RBAC permissions format
|
38
|
+
- Raises `ConfigurationError` when permissions is not a Hash
|
39
|
+
- Provides helpful error messages for developers
|
40
|
+
- Maintains backward compatibility for other validation scenarios
|
41
|
+
- **Role Lookup Logic**: Optimized role permission checking with direct hash access
|
42
|
+
- Eliminated unnecessary array iteration in permission validation
|
43
|
+
- Improved performance for applications with many roles
|
44
|
+
- Maintains support for both string and integer role keys
|
45
|
+
|
46
|
+
#### Bug Fixes
|
47
|
+
|
48
|
+
- **Test Coverage**: Fixed `test_check_rbac_format?` test that was using old array format
|
49
|
+
- **Key Resolution**: Fixed JWT payload key resolution bug in `rbac_last_update_timestamp` method
|
50
|
+
- **Validation Logic**: Updated all validation tests to use new flat object format
|
51
|
+
|
52
|
+
### ๐๏ธ Technical Details
|
53
|
+
|
54
|
+
#### Architecture Changes
|
55
|
+
|
56
|
+
- **RBAC Manager**: Updated `validate_rbac_cache_format` and `check_rbac_format?` methods
|
57
|
+
- Direct hash lookup: `permissions_data[role_id]` instead of array iteration
|
58
|
+
- Improved error handling with specific ConfigurationError exceptions
|
59
|
+
- Enhanced validation with clear developer feedback
|
60
|
+
- **Exception Handling**: Re-raises ConfigurationError while preserving other error handling
|
61
|
+
- Developer errors bubble up for immediate attention
|
62
|
+
- Runtime errors (cache issues) are still handled gracefully
|
63
|
+
- Maintains existing debug logging for troubleshooting
|
64
|
+
|
65
|
+
#### Performance Improvements
|
66
|
+
|
67
|
+
- **O(1) Role Lookup**: Direct hash access for role permissions
|
68
|
+
- **Reduced Memory**: Eliminated nested array structures
|
69
|
+
- **Faster Validation**: Simplified permission checking logic
|
70
|
+
|
71
|
+
#### Developer Experience
|
72
|
+
|
73
|
+
- **Better Error Messages**: Clear configuration error descriptions
|
74
|
+
```ruby
|
75
|
+
# Example error message:
|
76
|
+
"RBAC permissions must be a Hash with role-id keys, not Array.
|
77
|
+
Expected format: {\"role-id\": [\"resource:method\", ...]}, but got: Array"
|
78
|
+
```
|
79
|
+
- **Migration Support**: Catches common mistakes when upgrading cache format
|
80
|
+
- **Improved Documentation**: Cleaner YARD output with better Markdown rendering
|
81
|
+
|
82
|
+
### ๐ Documentation Updates
|
83
|
+
|
84
|
+
- **README.md**: Updated RBAC cache format examples and specifications
|
85
|
+
- **YARD Integration**: Fixed documentation rendering with Redcarpet Markdown processor
|
86
|
+
- **Code Examples**: Updated all examples to use new flat object format
|
87
|
+
|
88
|
+
### ๐งช Testing
|
89
|
+
|
90
|
+
- **Test Coverage Maintained**: All tests updated to use new cache format
|
91
|
+
- **Enhanced Validation**: Added tests for new configuration error scenarios
|
92
|
+
- **Comprehensive Coverage**: Validated migration scenarios and edge cases
|
93
|
+
|
94
|
+
### โ ๏ธ Migration Guide
|
95
|
+
|
96
|
+
#### Updating RBAC Cache Format
|
97
|
+
|
98
|
+
**Before (v1.0.x):**
|
99
|
+
```ruby
|
100
|
+
Rails.cache.write("permissions", {
|
101
|
+
'last_update' => Time.now.to_i,
|
102
|
+
'permissions' => [
|
103
|
+
{ '123' => ['sales/invoices:get', 'sales/invoices:post'] },
|
104
|
+
{ '456' => ['admin/*:*'] }
|
105
|
+
]
|
106
|
+
})
|
107
|
+
```
|
108
|
+
|
109
|
+
**After (v1.1.0+):**
|
110
|
+
```ruby
|
111
|
+
Rails.cache.write("permissions", {
|
112
|
+
'last_update' => Time.now.to_i,
|
113
|
+
'permissions' => {
|
114
|
+
'123' => ['sales/invoices:get', 'sales/invoices:post'],
|
115
|
+
'456' => ['admin/*:*']
|
116
|
+
}
|
117
|
+
})
|
118
|
+
```
|
119
|
+
|
120
|
+
#### Benefits of Migration
|
121
|
+
|
122
|
+
- **๐ Better Performance**: O(1) role lookup instead of O(n) iteration
|
123
|
+
- **๐ ๏ธ Easier Management**: Direct role access for permission updates
|
124
|
+
- **๐ Cleaner Code**: Simpler data structure that's easier to understand and maintain
|
125
|
+
|
126
|
+
---
|
127
|
+
|
8
128
|
## [1.0.2] - 2025-08-13
|
9
129
|
|
10
130
|
### ๐ง Fixed
|
@@ -249,5 +369,7 @@ This 1.0.0 release represents a production-ready JWT authentication middleware w
|
|
249
369
|
|
250
370
|
**Deprecations**: None (initial release).
|
251
371
|
|
372
|
+
[1.1.0]: https://github.com/kanutocd/rack_jwt_aegis/releases/tag/v1.1.0
|
373
|
+
[1.0.2]: https://github.com/kanutocd/rack_jwt_aegis/releases/tag/v1.0.2
|
252
374
|
[1.0.1]: https://github.com/kanutocd/rack_jwt_aegis/releases/tag/v1.0.1
|
253
375
|
[1.0.0]: https://github.com/kanutocd/rack_jwt_aegis/releases/tag/v1.0.0
|
data/README.md
CHANGED
@@ -273,7 +273,7 @@ accessible_companies = RackJwtAegis::RequestContext.pathname_slugs(request.env)
|
|
273
273
|
# => ["company-a", "company-b"]
|
274
274
|
|
275
275
|
# Check if user has access to specific company
|
276
|
-
has_access = RackJwtAegis::RequestContext.
|
276
|
+
has_access = RackJwtAegis::RequestContext.has_pathname_slug_access?(request.env, "company-a")
|
277
277
|
# => true
|
278
278
|
|
279
279
|
# Helper methods for request objects
|
@@ -291,7 +291,7 @@ tenant_id = RackJwtAegis::RequestContext.current_tenant_id(request)
|
|
291
291
|
- `pathname_slugs(env)` - Get array of accessible company slugs
|
292
292
|
- `current_user_id(request)` - Helper for request objects
|
293
293
|
- `current_tenant_id(request)` - Helper for request objects
|
294
|
-
- `
|
294
|
+
- `has_pathname_slug_access?(env, slug)` - Check company access
|
295
295
|
|
296
296
|
## JWT Payload Structure
|
297
297
|
|
@@ -439,31 +439,28 @@ When RBAC is enabled, the middleware expects permissions to be stored in the cac
|
|
439
439
|
```json
|
440
440
|
{
|
441
441
|
"last_update": 1640995200,
|
442
|
-
"permissions":
|
443
|
-
|
444
|
-
"
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
{
|
453
|
-
"456": ["admin/*:*", "reports:get"]
|
454
|
-
}
|
455
|
-
]
|
442
|
+
"permissions": {
|
443
|
+
"123": [
|
444
|
+
"sales/invoices:get",
|
445
|
+
"sales/invoices:post",
|
446
|
+
"%r{sales/invoices/\\d+}:get",
|
447
|
+
"%r{sales/invoices/\\d+}:put",
|
448
|
+
"users/*:get"
|
449
|
+
],
|
450
|
+
"456": ["admin/*:*", "reports:get"]
|
451
|
+
}
|
456
452
|
}
|
457
453
|
```
|
458
454
|
|
459
455
|
### Format Specification
|
460
456
|
|
461
457
|
- **last_update**: Timestamp for cache invalidation
|
462
|
-
- **permissions**:
|
463
|
-
- **Role ID
|
464
|
-
- **
|
465
|
-
- **resource-endpoint
|
466
|
-
|
458
|
+
- **permissions**: Object containing direct role-to-permissions mapping:
|
459
|
+
- **Role ID** (key): String or numeric identifier
|
460
|
+
- **Permissions** (value): Array of permission strings
|
461
|
+
- **Permission Format**: `"resource-endpoint:http-method"`
|
462
|
+
- **resource-endpoint**: API path (literal string or regex pattern)
|
463
|
+
- **http-method**: `get`, `post`, `put`, `delete`, or `*` (wildcard)
|
467
464
|
|
468
465
|
### Permission Examples
|
469
466
|
|
@@ -20,7 +20,7 @@ module RackJwtAegis
|
|
20
20
|
timestamp = Time.now.strftime('%Y-%m-%d %H:%M:%S.%L')
|
21
21
|
|
22
22
|
# Determine component name for log prefix
|
23
|
-
component_name = component ||
|
23
|
+
component_name = component || self.class.name.split('::').last || 'RackJwtAegis'
|
24
24
|
|
25
25
|
formatted_message = "[#{timestamp}] #{component_name}: #{message}"
|
26
26
|
|
@@ -31,21 +31,5 @@ module RackJwtAegis
|
|
31
31
|
puts formatted_message
|
32
32
|
end
|
33
33
|
end
|
34
|
-
|
35
|
-
private
|
36
|
-
|
37
|
-
# Infer component name from class name
|
38
|
-
#
|
39
|
-
# @return [String] the inferred component name
|
40
|
-
def infer_component_name
|
41
|
-
case self.class.name
|
42
|
-
when /Middleware/
|
43
|
-
'RackJwtAegis'
|
44
|
-
when /RbacManager/
|
45
|
-
'RbacManager'
|
46
|
-
else
|
47
|
-
self.class.name.split('::').last || 'RackJwtAegis'
|
48
|
-
end
|
49
|
-
end
|
50
34
|
end
|
51
35
|
end
|
@@ -19,6 +19,7 @@ module RackJwtAegis
|
|
19
19
|
# @example With multi-tenant validation
|
20
20
|
# config = Configuration.new(
|
21
21
|
# jwt_secret: 'your-secret',
|
22
|
+
# validate_tenant_id: true,
|
22
23
|
# validate_subdomain: true,
|
23
24
|
# validate_pathname_slug: true
|
24
25
|
# )
|
@@ -95,21 +96,14 @@ module RackJwtAegis
|
|
95
96
|
# @param payload [Hash] the JWT payload to validate
|
96
97
|
# @raise [AuthenticationError] if required claims are missing
|
97
98
|
def validate_required_claims(payload)
|
98
|
-
required_claims = []
|
99
|
-
|
100
99
|
# Always require user identification
|
101
|
-
required_claims
|
102
|
-
|
103
|
-
|
104
|
-
if @config.validate_subdomain?
|
105
|
-
required_claims << @config.payload_key(:tenant_id)
|
106
|
-
required_claims << @config.payload_key(:subdomain)
|
107
|
-
end
|
108
|
-
|
100
|
+
required_claims = [@config.payload_key(:user_id)]
|
101
|
+
required_claims << @config.payload_key(:subdomain) if @config.validate_subdomain?
|
102
|
+
required_claims << @config.payload_key(:tenant_id) if @config.validate_tenant_id?
|
109
103
|
required_claims << @config.payload_key(:pathname_slugs) if @config.validate_pathname_slug?
|
104
|
+
required_claims << @config.payload_key(:role_ids) if @config.rbac_enabled?
|
110
105
|
|
111
|
-
missing_claims = required_claims.select { |claim| payload[claim.to_s].
|
112
|
-
|
106
|
+
missing_claims = required_claims.select { |claim| payload[claim.to_s].to_s.empty? }
|
113
107
|
return if missing_claims.empty?
|
114
108
|
|
115
109
|
raise AuthenticationError, "JWT payload missing required claims: #{missing_claims.join(', ')}"
|
@@ -120,25 +114,24 @@ module RackJwtAegis
|
|
120
114
|
# @param payload [Hash] the JWT payload to validate
|
121
115
|
# @raise [AuthenticationError] if claim types are invalid
|
122
116
|
def validate_claim_types(payload)
|
123
|
-
|
124
|
-
|
117
|
+
user_id = payload[@config.payload_key(:user_id).to_s]
|
125
118
|
# User ID should be numeric or string
|
126
|
-
if
|
119
|
+
if user_id.to_s.empty? || (!user_id.is_a?(Numeric) && !user_id.is_a?(String))
|
127
120
|
raise AuthenticationError, 'Invalid user_id format in JWT payload'
|
128
121
|
end
|
129
122
|
|
130
|
-
#
|
131
|
-
if @config.
|
132
|
-
|
133
|
-
if
|
123
|
+
# Tenant ID should be numeric or string (if present)
|
124
|
+
if @config.validate_tenant_id?
|
125
|
+
tenant_id = payload[@config.payload_key(:tenant_id).to_s]
|
126
|
+
if tenant_id.to_s.empty? || (!tenant_id.is_a?(Numeric) && !tenant_id.is_a?(String))
|
134
127
|
raise AuthenticationError, 'Invalid tenant_id format in JWT payload'
|
135
128
|
end
|
136
129
|
end
|
137
130
|
|
138
131
|
# Company group domain should be string (if present)
|
139
132
|
if @config.validate_subdomain?
|
140
|
-
|
141
|
-
if
|
133
|
+
subdomain = payload[@config.payload_key(:subdomain).to_s]
|
134
|
+
if subdomain.to_s.empty? || !subdomain.is_a?(String)
|
142
135
|
raise AuthenticationError, 'Invalid subdomain format in JWT payload'
|
143
136
|
end
|
144
137
|
end
|
@@ -146,8 +139,8 @@ module RackJwtAegis
|
|
146
139
|
# Company slugs should be array (if present)
|
147
140
|
return unless @config.validate_pathname_slug?
|
148
141
|
|
149
|
-
|
150
|
-
return unless
|
142
|
+
pathname_slugs = payload[@config.payload_key(:pathname_slugs).to_s]
|
143
|
+
return unless pathname_slugs && !pathname_slugs.is_a?(Array)
|
151
144
|
|
152
145
|
raise AuthenticationError, 'Invalid pathname_slugs format in JWT payload - must be array'
|
153
146
|
end
|
@@ -19,6 +19,8 @@ module RackJwtAegis
|
|
19
19
|
# @example Advanced usage
|
20
20
|
# use RackJwtAegis::Middleware, {
|
21
21
|
# jwt_secret: ENV['JWT_SECRET'],
|
22
|
+
# validate_tenant_id: true,
|
23
|
+
# validate_pathname_slug: true,
|
22
24
|
# validate_subdomain: true,
|
23
25
|
# rbac_enabled: true,
|
24
26
|
# cache_store: :redis,
|
@@ -142,7 +144,7 @@ module RackJwtAegis
|
|
142
144
|
#
|
143
145
|
# @return [Boolean] true if subdomain or pathname slug validation is enabled
|
144
146
|
def multi_tenant_enabled?
|
145
|
-
@config.validate_subdomain? || @config.validate_pathname_slug?
|
147
|
+
@config.validate_tenant_id? || @config.validate_subdomain? || @config.validate_pathname_slug?
|
146
148
|
end
|
147
149
|
|
148
150
|
# Generate a string describing enabled features for logging
|
@@ -150,8 +152,9 @@ module RackJwtAegis
|
|
150
152
|
# @return [String] comma-separated list of enabled features
|
151
153
|
def enabled_features
|
152
154
|
features = ['JWT']
|
155
|
+
features << 'TenantId' if @config.validate_tenant_id?
|
153
156
|
features << 'Subdomain' if @config.validate_subdomain?
|
154
|
-
features << '
|
157
|
+
features << 'PathnameSlug' if @config.validate_pathname_slug?
|
155
158
|
features << 'RBAC' if @config.rbac_enabled?
|
156
159
|
features.join(', ')
|
157
160
|
end
|
@@ -51,7 +51,7 @@ module RackJwtAegis
|
|
51
51
|
has_permission = check_rbac_permission(user_id, request)
|
52
52
|
|
53
53
|
# Cache the result if middleware has write access
|
54
|
-
cache_permission_result(permission_key, has_permission)
|
54
|
+
cache_permission_result(permission_key, has_permission)
|
55
55
|
|
56
56
|
return if has_permission
|
57
57
|
|
@@ -83,23 +83,21 @@ module RackJwtAegis
|
|
83
83
|
end
|
84
84
|
|
85
85
|
def build_permission_key(user_id, request)
|
86
|
-
|
87
|
-
"#{user_id}:#{full_url}:#{request.request_method.downcase}"
|
86
|
+
"#{user_id}:#{request.host}#{request.path}:#{request.request_method.downcase}"
|
88
87
|
end
|
89
88
|
|
90
89
|
def check_cached_permission(permission_key)
|
91
90
|
return nil unless @permission_cache
|
92
91
|
|
93
92
|
begin
|
94
|
-
# Get the user permissions
|
93
|
+
# Get the cached user permissions
|
95
94
|
user_permissions = @permission_cache.read('user_permissions')
|
96
95
|
return nil if user_permissions.nil? || !user_permissions.is_a?(Hash)
|
97
96
|
|
98
97
|
# First check: If RBAC permissions were updated recently, nuke ALL cached permissions
|
99
98
|
rbac_last_update = rbac_last_update_timestamp
|
100
99
|
if rbac_last_update
|
101
|
-
|
102
|
-
rbac_update_age = current_time - rbac_last_update
|
100
|
+
rbac_update_age = Time.now.to_i - rbac_last_update
|
103
101
|
|
104
102
|
# If RBAC was updated within the TTL period, all cached permissions are invalid
|
105
103
|
if rbac_update_age <= @config.user_permissions_ttl
|
@@ -108,12 +106,11 @@ module RackJwtAegis
|
|
108
106
|
end
|
109
107
|
end
|
110
108
|
|
111
|
-
# Check if permission exists in
|
109
|
+
# Check if permission exists in this format: {"user_id:full_url:method" => timestamp}
|
112
110
|
cached_timestamp = user_permissions[permission_key]
|
113
111
|
return nil unless cached_timestamp.is_a?(Integer)
|
114
112
|
|
115
|
-
|
116
|
-
permission_age = current_time - cached_timestamp
|
113
|
+
permission_age = Time.now.to_i - cached_timestamp
|
117
114
|
|
118
115
|
# Second check: TTL expiration
|
119
116
|
if permission_age > @config.user_permissions_ttl
|
@@ -181,22 +178,23 @@ module RackJwtAegis
|
|
181
178
|
return false
|
182
179
|
end
|
183
180
|
|
184
|
-
#
|
185
|
-
rbac_data['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)
|
181
|
+
# Get permissions object for direct lookup
|
182
|
+
permissions_data = rbac_data['permissions'] || rbac_data[:permissions]
|
188
183
|
|
189
|
-
|
190
|
-
|
184
|
+
# Check permissions for each user role using direct lookup
|
185
|
+
user_roles.each do |role_id|
|
186
|
+
# Try both string and integer keys for role lookup
|
187
|
+
role_permissions = permissions_data[role_id.to_s] || permissions_data[role_id.to_i]
|
188
|
+
next unless role_permissions
|
191
189
|
|
192
|
-
|
190
|
+
matched_permission = find_matching_permission(role_permissions, request)
|
191
|
+
next unless matched_permission
|
193
192
|
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
end
|
198
|
-
return true
|
193
|
+
# Cache this specific permission match for faster future lookups
|
194
|
+
if @permission_cache && @config.cache_write_enabled?
|
195
|
+
cache_permission_match(user_id, request, role_id, matched_permission)
|
199
196
|
end
|
197
|
+
return true
|
200
198
|
end
|
201
199
|
|
202
200
|
false
|
@@ -208,7 +206,7 @@ module RackJwtAegis
|
|
208
206
|
|
209
207
|
begin
|
210
208
|
rbac_data = @rbac_cache.read('permissions')
|
211
|
-
if rbac_data.is_a?(Hash) && (rbac_data
|
209
|
+
if rbac_data.is_a?(Hash) && (rbac_data.key?('last_update') || rbac_data.key?(:last_update))
|
212
210
|
return rbac_data['last_update'] || rbac_data[:last_update]
|
213
211
|
end
|
214
212
|
|
@@ -393,9 +391,9 @@ module RackJwtAegis
|
|
393
391
|
# Expected format:
|
394
392
|
# {
|
395
393
|
# last_update: timestamp,
|
396
|
-
# permissions:
|
397
|
-
#
|
398
|
-
#
|
394
|
+
# permissions: {
|
395
|
+
# "role-id": ["{resource-endpoint}:{http-method}"]
|
396
|
+
# }
|
399
397
|
# }
|
400
398
|
def validate_rbac_cache_format(rbac_data)
|
401
399
|
return false unless rbac_data.is_a?(Hash)
|
@@ -404,31 +402,35 @@ module RackJwtAegis
|
|
404
402
|
return false unless rbac_data.key?('last_update') || rbac_data.key?(:last_update)
|
405
403
|
return false unless rbac_data.key?('permissions') || rbac_data.key?(:permissions)
|
406
404
|
|
407
|
-
# Get permissions
|
405
|
+
# Get permissions object (now expecting a Hash, not Array)
|
408
406
|
permissions = rbac_data['permissions'] || rbac_data[:permissions]
|
409
|
-
return false unless permissions.is_a?(Array)
|
410
407
|
|
411
|
-
#
|
412
|
-
permissions.
|
413
|
-
|
408
|
+
# If permissions is present but not a Hash, raise an exception to help developers
|
409
|
+
if !permissions.nil? && !permissions.is_a?(Hash)
|
410
|
+
raise ConfigurationError, "RBAC permissions must be a Hash with role-id keys, not #{permissions.class}. " \
|
411
|
+
"Expected format: {\"role-id\": [\"resource:method\", ...]}, " \
|
412
|
+
"but got: #{permissions.class}"
|
413
|
+
end
|
414
414
|
|
415
|
-
|
416
|
-
|
415
|
+
# Return false if permissions is nil (should not happen given the key check above, but defensive)
|
416
|
+
return false if permissions.nil?
|
417
417
|
|
418
|
-
|
419
|
-
|
420
|
-
|
418
|
+
# Validate each role's permissions
|
419
|
+
permissions.each_value do |role_permissions|
|
420
|
+
return false unless role_permissions.is_a?(Array)
|
421
421
|
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
end
|
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) or '*' (wildcard)
|
426
|
+
return false unless permission.include?(':') || permission.include?('*')
|
428
427
|
end
|
429
428
|
end
|
430
429
|
|
431
430
|
true
|
431
|
+
rescue ConfigurationError
|
432
|
+
# Re-raise configuration errors so developers see them
|
433
|
+
raise
|
432
434
|
rescue StandardError => e
|
433
435
|
debug_log("RbacManager: Cache format validation error: #{e.message}", :warn)
|
434
436
|
false
|
@@ -100,42 +100,23 @@ module RackJwtAegis
|
|
100
100
|
tenant_id(request.env)
|
101
101
|
end
|
102
102
|
|
103
|
-
def self.
|
104
|
-
pathname_slugs(env).include?(
|
103
|
+
def self.has_pathname_slug_access?(env, pathname_slug)
|
104
|
+
pathname_slugs(env).include?(pathname_slug)
|
105
105
|
end
|
106
106
|
|
107
107
|
private
|
108
108
|
|
109
109
|
def set_user_context(env, payload)
|
110
|
-
|
111
|
-
user_id = payload[user_id_key]
|
112
|
-
|
113
|
-
env[USER_ID_KEY] = user_id
|
110
|
+
env[USER_ID_KEY] = payload[@config.payload_key(:user_id).to_s]
|
114
111
|
end
|
115
112
|
|
116
113
|
def set_tenant_context(env, payload)
|
117
|
-
# Set
|
118
|
-
|
119
|
-
|
120
|
-
tenant_id = payload[tenant_id_key]
|
121
|
-
env[TENANT_ID_KEY] = tenant_id
|
122
|
-
end
|
123
|
-
|
124
|
-
if @config.validate_subdomain?
|
125
|
-
company_domain_key = @config.payload_key(:subdomain).to_s
|
126
|
-
company_domain = payload[company_domain_key]
|
127
|
-
env[SUBDOMAIN_KEY] = company_domain
|
128
|
-
end
|
129
|
-
|
130
|
-
# Set company slugs for sub-level tenant access
|
114
|
+
# Set multi-tenant information
|
115
|
+
env[TENANT_ID_KEY] = payload[@config.payload_key(:tenant_id).to_s] if @config.validate_tenant_id?
|
116
|
+
env[SUBDOMAIN_KEY] = payload[@config.payload_key(:subdomain).to_s] if @config.validate_subdomain?
|
131
117
|
return unless @config.validate_pathname_slug? || @config.payload_mapping.key?(:pathname_slugs)
|
132
118
|
|
133
|
-
|
134
|
-
pathname_slugs = payload[pathname_slugs_key]
|
135
|
-
|
136
|
-
# Ensure it's an array
|
137
|
-
pathname_slugs = Array(pathname_slugs) if pathname_slugs
|
138
|
-
env[PATHNAME_SLUGS_KEY] = pathname_slugs || []
|
119
|
+
env[PATHNAME_SLUGS_KEY] = Array(payload[@config.payload_key(:pathname_slugs).to_s]).flatten
|
139
120
|
end
|
140
121
|
end
|
141
122
|
end
|