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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 85f1445f011d0e42c075e3777164d61b215aa04b6f78e535db987c3f0e239c6f
4
- data.tar.gz: 94fa6e5dd9041709d394e1c42dc5cc938aeb4fb9325218a94cab6352a4a7cf47
3
+ metadata.gz: 0e776985ff922d83b9ac75821698125a565dce342be61bd4bf6012fc97dfd82c
4
+ data.tar.gz: 7bbb4006689d39ea9ec47ad869b01468bbfbddfb0ecbaa430d28ac96f1fd33b7
5
5
  SHA512:
6
- metadata.gz: 0e3e83285e30c8b86efb2977aba6cfeccf728539b689fb80fb658e29ac91e61c455ab48c5793b689293154d3fe985ad6fee71d3b2381261f34b481370c89e8a1
7
- data.tar.gz: f42e6cdb32d72f06ef14d7c016cbb7970ad5b04f9626b16cc1bf7a4062cdb6b68a899548e80259bac934622b2adc3080121c2b7e82daa84ab93f2df30ad34e63
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.has_company_access?(request.env, "company-a")
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
- - `has_company_access?(env, slug)` - Check company access
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
- "123": [
445
- "sales/invoices:get",
446
- "sales/invoices:post",
447
- "%r{sales/invoices/\\d+}:get",
448
- "%r{sales/invoices/\\d+}:put",
449
- "users/*:get"
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**: Array of role permission objects
463
- - **Role ID**: String or numeric identifier for user roles
464
- - **Permission Format**: `"resource-endpoint:http-method"`
465
- - **resource-endpoint**: API path (literal string or regex pattern)
466
- - **http-method**: `get`, `post`, `put`, `delete`, or `*` (wildcard)
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 || infer_component_name
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 << @config.payload_key(:user_id)
102
-
103
- # Multi-tenant validation requirements
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].nil? }
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
- user_id_key = @config.payload_key(:user_id).to_s
124
-
117
+ user_id = payload[@config.payload_key(:user_id).to_s]
125
118
  # User ID should be numeric or string
126
- if payload[user_id_key] && !payload[user_id_key].is_a?(Numeric) && !payload[user_id_key].is_a?(String)
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
- # Company group ID should be numeric or string (if present)
131
- if @config.validate_subdomain?
132
- tenant_id_key = @config.payload_key(:tenant_id).to_s
133
- if payload[tenant_id_key] && !payload[tenant_id_key].is_a?(Numeric) && !payload[tenant_id_key].is_a?(String)
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
- company_domain_key = @config.payload_key(:subdomain).to_s
141
- if payload[company_domain_key] && !payload[company_domain_key].is_a?(String)
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
- pathname_slugs_key = @config.payload_key(:pathname_slugs).to_s
150
- return unless payload[pathname_slugs_key] && !payload[pathname_slugs_key].is_a?(Array)
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 << 'CompanySlug' if @config.validate_pathname_slug?
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) if @permission_cache && @config.cache_write_enabled?
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
- full_url = "#{request.host}#{request.path}"
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 cache using new format
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
- current_time = Time.now.to_i
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 new format: {"user_id:full_url:method" => timestamp}
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
- current_time = Time.now.to_i
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
- # 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)
181
+ # Get permissions object for direct lookup
182
+ permissions_data = rbac_data['permissions'] || rbac_data[:permissions]
188
183
 
189
- permissions = role_permissions[role_id.to_s] || role_permissions[role_id.to_i]
190
- matched_permission = find_matching_permission(permissions, request)
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
- next unless matched_permission
190
+ matched_permission = find_matching_permission(role_permissions, request)
191
+ next unless matched_permission
193
192
 
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
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['last_update'] || rbac_data[:last_update])
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
- # {role-id: ["{resource-endpoint}:{http-method}"]}
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 array
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
- # Validate each permission entry
412
- permissions.each do |permission_entry|
413
- return false unless permission_entry.is_a?(Hash)
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
- # Each entry should have at least one role-id key
416
- return false if permission_entry.empty?
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
- # Validate permission values are arrays of strings
419
- permission_entry.each_value do |role_permissions|
420
- return false unless role_permissions.is_a?(Array)
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
- # 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
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.has_company_access?(env, company_slug)
104
- pathname_slugs(env).include?(company_slug)
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
- user_id_key = @config.payload_key(:user_id).to_s
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 company group information
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
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
- pathname_slugs_key = @config.payload_key(:pathname_slugs).to_s
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RackJwtAegis
4
- VERSION = '1.0.2'
4
+ VERSION = '1.1.0'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rack_jwt_aegis
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.2
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ken C. Demanawa