rack_jwt_aegis 0.0.0 → 1.0.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/.rubocop.yml +9 -0
- data/.yard/yard_gfm_config.rb +21 -0
- data/.yardopts +16 -0
- data/CHANGELOG.md +204 -0
- data/README.md +339 -45
- 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 +205 -44
- data/lib/rack_jwt_aegis/jwt_validator.rb +56 -14
- data/lib/rack_jwt_aegis/middleware.rb +72 -2
- data/lib/rack_jwt_aegis/multi_tenant_validator.rb +43 -18
- data/lib/rack_jwt_aegis/rbac_manager.rb +323 -76
- 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 +36 -1
- metadata +24 -13
- data/examples/basic_usage.rb +0 -85
- /data/sig/{rack_jwt_bastion.rbs → rack_jwt_aegis.rbs} +0 -0
data/exe/rack_jwt_aegis
ADDED
@@ -0,0 +1,235 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'optparse'
|
5
|
+
require 'securerandom'
|
6
|
+
require 'base64'
|
7
|
+
|
8
|
+
# CLI for rack_jwt_aegis gem
|
9
|
+
class RackJwtAegisCLI
|
10
|
+
def initialize
|
11
|
+
@options = {
|
12
|
+
length: 64,
|
13
|
+
format: :hex,
|
14
|
+
count: 1,
|
15
|
+
}
|
16
|
+
end
|
17
|
+
|
18
|
+
def run(args = ARGV)
|
19
|
+
parse_options(args)
|
20
|
+
|
21
|
+
case @command
|
22
|
+
when :generate_secret
|
23
|
+
generate_secrets
|
24
|
+
when :version
|
25
|
+
show_version
|
26
|
+
else
|
27
|
+
show_help
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def parse_options(args)
|
34
|
+
@command = :generate_secret # default command
|
35
|
+
|
36
|
+
OptionParser.new do |opts| # rubocop:disable Metrics/BlockLength
|
37
|
+
opts.banner = 'Usage: rack-jwt-aegis [command] [options]'
|
38
|
+
opts.separator ''
|
39
|
+
opts.separator 'Commands:'
|
40
|
+
opts.separator ' secret Generate JWT secret(s) (default)'
|
41
|
+
opts.separator ' version Show version'
|
42
|
+
opts.separator ' help Show this help'
|
43
|
+
opts.separator ''
|
44
|
+
opts.separator 'Secret generation options:'
|
45
|
+
|
46
|
+
opts.on('-l', '--length LENGTH', Integer, 'Secret length in bytes (default: 64)') do |length|
|
47
|
+
@options[:length] = length
|
48
|
+
end
|
49
|
+
|
50
|
+
opts.on('-f', '--format FORMAT', [:hex, :base64, :raw], # rubocop:disable Naming/VariableNumber
|
51
|
+
'Output format: hex, base64, raw (default: hex)') do |format|
|
52
|
+
@options[:format] = format
|
53
|
+
end
|
54
|
+
|
55
|
+
opts.on('-c', '--count COUNT', Integer, 'Number of secrets to generate (default: 1)') do |count|
|
56
|
+
@options[:count] = count
|
57
|
+
end
|
58
|
+
|
59
|
+
opts.on('-e', '--env', 'Output in environment variable format') do
|
60
|
+
@options[:env_format] = true
|
61
|
+
end
|
62
|
+
|
63
|
+
opts.on('-q', '--quiet', 'Quiet mode - only output the secret(s)') do
|
64
|
+
@options[:quiet] = true
|
65
|
+
end
|
66
|
+
|
67
|
+
opts.on('-h', '--help', 'Show this help') do
|
68
|
+
@command = :help
|
69
|
+
end
|
70
|
+
|
71
|
+
opts.on('-v', '--version', 'Show version') do
|
72
|
+
@command = :version
|
73
|
+
end
|
74
|
+
|
75
|
+
opts.separator ''
|
76
|
+
opts.separator 'Examples:'
|
77
|
+
opts.separator ' rack-jwt-aegis secret # Generate hex secret'
|
78
|
+
opts.separator ' rack-jwt-aegis secret -f base64 # Generate base64 secret'
|
79
|
+
opts.separator ' rack-jwt-aegis secret -l 32 -c 3 # Generate 3 secrets, 32 bytes each'
|
80
|
+
opts.separator ' rack-jwt-aegis secret -e # Output as JWT_SECRET=...'
|
81
|
+
opts.separator ' rack-jwt-aegis secret -q # Quiet mode'
|
82
|
+
opts.separator ''
|
83
|
+
end.parse!(args)
|
84
|
+
|
85
|
+
# Handle command from remaining args
|
86
|
+
return unless args.length.positive?
|
87
|
+
|
88
|
+
case args[0].downcase
|
89
|
+
when 'secret', 'generate'
|
90
|
+
@command = :generate_secret
|
91
|
+
when 'version'
|
92
|
+
@command = :version
|
93
|
+
when 'help'
|
94
|
+
@command = :help
|
95
|
+
else
|
96
|
+
# Invalid command - show help
|
97
|
+
@command = :help
|
98
|
+
@invalid_command = args[0]
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def generate_secrets
|
103
|
+
unless @options[:quiet]
|
104
|
+
puts '🛡️ Rack JWT Aegis - Secret Generator'
|
105
|
+
puts '=' * 50
|
106
|
+
puts
|
107
|
+
end
|
108
|
+
|
109
|
+
@options[:count].times do |i|
|
110
|
+
secret = SecureRandom.random_bytes(@options[:length])
|
111
|
+
formatted_secret = format_secret(secret)
|
112
|
+
|
113
|
+
if @options[:env_format]
|
114
|
+
puts "JWT_SECRET=#{formatted_secret}"
|
115
|
+
elsif @options[:quiet]
|
116
|
+
puts formatted_secret
|
117
|
+
else
|
118
|
+
puts "Secret #{i + 1}:" if @options[:count] > 1
|
119
|
+
puts formatted_secret
|
120
|
+
unless @options[:quiet]
|
121
|
+
puts "Length: #{@options[:length]} bytes (#{formatted_secret.length} characters)"
|
122
|
+
puts "Format: #{@options[:format]}"
|
123
|
+
puts "Entropy: ~#{(@options[:length] * 8).to_f} bits"
|
124
|
+
puts
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
return if @options[:quiet]
|
130
|
+
|
131
|
+
puts '💡 Usage in your application:'
|
132
|
+
puts " export JWT_SECRET=\"#{format_secret(SecureRandom.random_bytes(@options[:length]))}\""
|
133
|
+
puts ' # or add to your .env file'
|
134
|
+
puts
|
135
|
+
puts '⚠️ Security reminders:'
|
136
|
+
puts ' - Store secrets securely (environment variables, not in code)'
|
137
|
+
puts ' - Use different secrets for different environments'
|
138
|
+
puts ' - Rotate secrets periodically'
|
139
|
+
puts ' - Never commit secrets to version control'
|
140
|
+
end
|
141
|
+
|
142
|
+
def format_secret(secret)
|
143
|
+
case @options[:format]
|
144
|
+
when :hex
|
145
|
+
secret.unpack1('H*')
|
146
|
+
when :base64 # rubocop:disable Naming/VariableNumber
|
147
|
+
Base64.strict_encode64(secret)
|
148
|
+
when :raw
|
149
|
+
secret
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def show_version
|
154
|
+
# Try to load the version from the gem
|
155
|
+
|
156
|
+
require_relative '../lib/rack_jwt_aegis/version'
|
157
|
+
puts "rack_jwt_aegis #{RackJwtAegis::VERSION}"
|
158
|
+
rescue LoadError
|
159
|
+
puts 'rack_jwt_aegis (version unknown)'
|
160
|
+
end
|
161
|
+
|
162
|
+
def show_help
|
163
|
+
if @invalid_command
|
164
|
+
puts "❌ Unknown command: #{@invalid_command}"
|
165
|
+
puts
|
166
|
+
end
|
167
|
+
|
168
|
+
puts <<~HELP
|
169
|
+
🛡️ Rack JWT Aegis CLI
|
170
|
+
|
171
|
+
A command-line tool for generating secure JWT secrets and managing
|
172
|
+
rack_jwt_aegis configurations.
|
173
|
+
|
174
|
+
USAGE:
|
175
|
+
rack-jwt-aegis [command] [options]
|
176
|
+
|
177
|
+
COMMANDS:
|
178
|
+
secret Generate secure JWT secret(s)
|
179
|
+
version Show gem version
|
180
|
+
help Show this help message
|
181
|
+
|
182
|
+
SECRET GENERATION:
|
183
|
+
The secret command generates cryptographically secure random
|
184
|
+
secrets suitable for JWT signing.
|
185
|
+
|
186
|
+
EXAMPLES:
|
187
|
+
# Generate a 64-byte hex secret (default)
|
188
|
+
rack-jwt-aegis secret
|
189
|
+
|
190
|
+
# Generate a base64-encoded secret
|
191
|
+
rack-jwt-aegis secret --format base64
|
192
|
+
|
193
|
+
# Generate 3 secrets at once
|
194
|
+
rack-jwt-aegis secret --count 3
|
195
|
+
|
196
|
+
# Generate secret for environment variable
|
197
|
+
rack-jwt-aegis secret --env
|
198
|
+
|
199
|
+
# Quiet mode (only output secret)
|
200
|
+
rack-jwt-aegis secret --quiet
|
201
|
+
|
202
|
+
# Custom length (32 bytes)
|
203
|
+
rack-jwt-aegis secret --length 32
|
204
|
+
|
205
|
+
SECURITY NOTES:
|
206
|
+
- Generated secrets use SecureRandom for cryptographic security
|
207
|
+
- Default 64-byte secrets provide ~512 bits of entropy
|
208
|
+
- Always store secrets securely (environment variables, secret managers)
|
209
|
+
- Use different secrets for different environments
|
210
|
+
- Rotate secrets periodically
|
211
|
+
|
212
|
+
For more information, visit:
|
213
|
+
https://github.com/kanutocd/rack_jwt_aegis
|
214
|
+
HELP
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
# Run CLI if this file is executed directly
|
219
|
+
if $PROGRAM_NAME == __FILE__
|
220
|
+
exit_status = 1
|
221
|
+
cli = RackJwtAegisCLI.new
|
222
|
+
begin
|
223
|
+
cli.run
|
224
|
+
exit_status = 0
|
225
|
+
rescue Interrupt
|
226
|
+
puts "\n\n⚠️ Operation cancelled."
|
227
|
+
rescue OptionParser::InvalidArgument, OptionParser::InvalidOption => e
|
228
|
+
warn "❌ Error: #{e.message}"
|
229
|
+
puts
|
230
|
+
puts "Use 'rack-jwt-aegis --help' for usage information."
|
231
|
+
rescue StandardError => e
|
232
|
+
warn "❌ Error: #{e.message}"
|
233
|
+
end
|
234
|
+
exit exit_status
|
235
|
+
end
|
@@ -1,32 +1,176 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module RackJwtAegis
|
4
|
+
# Configuration class for RackJwtAegis middleware
|
5
|
+
#
|
6
|
+
# Manages all configuration options for JWT authentication, multi-tenant validation,
|
7
|
+
# RBAC authorization, and caching behavior.
|
8
|
+
#
|
9
|
+
# @author Ken Camajalan Demanawa
|
10
|
+
# @since 0.1.0
|
11
|
+
#
|
12
|
+
# @example Basic configuration
|
13
|
+
# config = Configuration.new(jwt_secret: 'your-secret')
|
14
|
+
#
|
15
|
+
# @example Full configuration
|
16
|
+
# config = Configuration.new(
|
17
|
+
# jwt_secret: ENV['JWT_SECRET'],
|
18
|
+
# jwt_algorithm: 'HS256',
|
19
|
+
# validate_subdomain: true,
|
20
|
+
# validate_pathname_slug: true,
|
21
|
+
# rbac_enabled: true,
|
22
|
+
# cache_store: :redis,
|
23
|
+
# cache_write_enabled: true,
|
24
|
+
# skip_paths: ['/health', '/api/public/*'],
|
25
|
+
# debug_mode: Rails.env.development?
|
26
|
+
# )
|
4
27
|
class Configuration
|
5
|
-
# Core JWT
|
6
|
-
attr_accessor :jwt_secret, :jwt_algorithm
|
28
|
+
# @!group Core JWT Settings
|
7
29
|
|
8
|
-
#
|
9
|
-
|
30
|
+
# The secret key used for JWT signature verification
|
31
|
+
# @return [String] the JWT secret key
|
32
|
+
# @note This is required and must not be empty
|
33
|
+
attr_accessor :jwt_secret
|
10
34
|
|
11
|
-
#
|
12
|
-
|
35
|
+
# The JWT algorithm to use for token verification
|
36
|
+
# @return [String] the JWT algorithm (default: 'HS256')
|
37
|
+
# @note Supported algorithms: HS256, HS384, HS512, RS256, RS384, RS512, ES256, ES384, ES512
|
38
|
+
attr_accessor :jwt_algorithm
|
13
39
|
|
14
|
-
#
|
40
|
+
# @!endgroup
|
41
|
+
|
42
|
+
# @!group Feature Toggles
|
43
|
+
|
44
|
+
# Whether to validate subdomain-based multi-tenancy
|
45
|
+
# @return [Boolean] true if subdomain validation is enabled
|
46
|
+
attr_accessor :validate_subdomain
|
47
|
+
|
48
|
+
# Whether to validate pathname slug-based multi-tenancy
|
49
|
+
# @return [Boolean] true if pathname slug validation is enabled
|
50
|
+
attr_accessor :validate_pathname_slug
|
51
|
+
|
52
|
+
# Whether RBAC (Role-Based Access Control) is enabled
|
53
|
+
# @return [Boolean] true if RBAC is enabled
|
54
|
+
attr_accessor :rbac_enabled
|
55
|
+
|
56
|
+
# @!endgroup
|
57
|
+
|
58
|
+
# @!group Multi-tenant Settings
|
59
|
+
|
60
|
+
# The HTTP header name containing the tenant ID
|
61
|
+
# @return [String] the tenant ID header name (default: 'X-Tenant-Id')
|
62
|
+
attr_accessor :tenant_id_header_name
|
63
|
+
|
64
|
+
# The regular expression pattern to extract pathname slugs
|
65
|
+
# @return [Regexp] the pathname slug pattern (default: /^\/api\/v1\/([^\/]+)\//)
|
66
|
+
attr_accessor :pathname_slug_pattern
|
67
|
+
|
68
|
+
# Mapping of standard payload keys to custom JWT claim names
|
69
|
+
# @return [Hash] the payload mapping configuration
|
70
|
+
# @example
|
71
|
+
# { user_id: :sub, tenant_id: :company_id, subdomain: :domain }
|
72
|
+
attr_accessor :payload_mapping
|
73
|
+
|
74
|
+
# @!endgroup
|
75
|
+
|
76
|
+
# @!group Path Management
|
77
|
+
|
78
|
+
# Array of paths that should skip JWT authentication
|
79
|
+
# @return [Array<String, Regexp>] paths to skip authentication for
|
80
|
+
# @example
|
81
|
+
# ['/health', '/api/public', /^\/assets/]
|
15
82
|
attr_accessor :skip_paths
|
16
83
|
|
17
|
-
#
|
18
|
-
|
19
|
-
|
84
|
+
# @!endgroup
|
85
|
+
|
86
|
+
# @!group Cache Configuration
|
87
|
+
|
88
|
+
# The primary cache store adapter type
|
89
|
+
# @return [Symbol] the cache store type (:memory, :redis, :memcached, :solid_cache)
|
90
|
+
attr_accessor :cache_store
|
91
|
+
|
92
|
+
# Options passed to the cache store adapter
|
93
|
+
# @return [Hash] cache store configuration options
|
94
|
+
attr_accessor :cache_options
|
95
|
+
|
96
|
+
# Whether the middleware can write to cache stores
|
97
|
+
# @return [Boolean] true if cache writing is enabled
|
98
|
+
attr_accessor :cache_write_enabled
|
20
99
|
|
21
|
-
#
|
100
|
+
# The RBAC cache store adapter type (separate from main cache)
|
101
|
+
# @return [Symbol] the RBAC cache store type
|
102
|
+
attr_accessor :rbac_cache_store
|
103
|
+
|
104
|
+
# Options for the RBAC cache store
|
105
|
+
# @return [Hash] RBAC cache configuration options
|
106
|
+
attr_accessor :rbac_cache_options
|
107
|
+
|
108
|
+
# The permission cache store adapter type
|
109
|
+
# @return [Symbol] the permission cache store type
|
110
|
+
attr_accessor :permission_cache_store
|
111
|
+
|
112
|
+
# Options for the permission cache store
|
113
|
+
# @return [Hash] permission cache configuration options
|
114
|
+
attr_accessor :permission_cache_options
|
115
|
+
|
116
|
+
# Time-to-live for user permissions cache in seconds
|
117
|
+
# @return [Integer] TTL in seconds (default: 1800 - 30 minutes)
|
118
|
+
attr_accessor :user_permissions_ttl
|
119
|
+
|
120
|
+
# @!endgroup
|
121
|
+
|
122
|
+
# @!group Custom Validators
|
123
|
+
|
124
|
+
# Custom payload validation proc
|
125
|
+
# @return [Proc] a callable that receives (payload, request) and returns boolean
|
126
|
+
# @example
|
127
|
+
# ->(payload, request) { payload['role'] == 'admin' }
|
22
128
|
attr_accessor :custom_payload_validator
|
23
129
|
|
24
|
-
#
|
25
|
-
|
130
|
+
# @!endgroup
|
131
|
+
|
132
|
+
# @!group Response Customization
|
26
133
|
|
27
|
-
#
|
134
|
+
# Custom response for unauthorized requests (401)
|
135
|
+
# @return [Hash] the unauthorized response body
|
136
|
+
# @example
|
137
|
+
# { error: 'Authentication required', code: 'AUTH_001' }
|
138
|
+
attr_accessor :unauthorized_response
|
139
|
+
|
140
|
+
# Custom response for forbidden requests (403)
|
141
|
+
# @return [Hash] the forbidden response body
|
142
|
+
# @example
|
143
|
+
# { error: 'Access denied', code: 'AUTH_002' }
|
144
|
+
attr_accessor :forbidden_response
|
145
|
+
|
146
|
+
# @!endgroup
|
147
|
+
|
148
|
+
# @!group Development Settings
|
149
|
+
|
150
|
+
# Whether debug mode is enabled for additional logging
|
151
|
+
# @return [Boolean] true if debug mode is enabled
|
28
152
|
attr_accessor :debug_mode
|
29
153
|
|
154
|
+
# @!endgroup
|
155
|
+
|
156
|
+
# Initialize a new Configuration instance
|
157
|
+
#
|
158
|
+
# @param options [Hash] configuration options
|
159
|
+
# @option options [String] :jwt_secret (required) JWT secret key for signature verification
|
160
|
+
# @option options [String] :jwt_algorithm ('HS256') JWT algorithm to use
|
161
|
+
# @option options [Boolean] :validate_subdomain (false) enable subdomain validation
|
162
|
+
# @option options [Boolean] :validate_pathname_slug (false) enable pathname slug validation
|
163
|
+
# @option options [Boolean] :rbac_enabled (false) enable RBAC authorization
|
164
|
+
# @option options [String] :tenant_id_header_name ('X-Tenant-Id') tenant ID header name
|
165
|
+
# @option options [Regexp] :pathname_slug_pattern default pattern for pathname slugs
|
166
|
+
# @option options [Hash] :payload_mapping mapping of JWT claim names
|
167
|
+
# @option options [Array<String, Regexp>] :skip_paths ([]) paths to skip authentication
|
168
|
+
# @option options [Symbol] :cache_store cache adapter type
|
169
|
+
# @option options [Hash] :cache_options cache configuration options
|
170
|
+
# @option options [Boolean] :cache_write_enabled (false) enable cache writing
|
171
|
+
# @option options [Integer] :user_permissions_ttl (1800) user permissions cache TTL in seconds
|
172
|
+
# @option options [Boolean] :debug_mode (false) enable debug logging
|
173
|
+
# @raise [ConfigurationError] if jwt_secret is missing or configuration is invalid
|
30
174
|
def initialize(options = {})
|
31
175
|
# Set defaults
|
32
176
|
set_defaults
|
@@ -42,27 +186,39 @@ module RackJwtAegis
|
|
42
186
|
validate!
|
43
187
|
end
|
44
188
|
|
189
|
+
# Check if RBAC is enabled
|
190
|
+
# @return [Boolean] true if RBAC is enabled
|
45
191
|
def rbac_enabled?
|
46
|
-
config_boolean(rbac_enabled)
|
192
|
+
config_boolean?(rbac_enabled)
|
47
193
|
end
|
48
194
|
|
195
|
+
# Check if subdomain validation is enabled
|
196
|
+
# @return [Boolean] true if subdomain validation is enabled
|
49
197
|
def validate_subdomain?
|
50
|
-
config_boolean(validate_subdomain)
|
198
|
+
config_boolean?(validate_subdomain)
|
51
199
|
end
|
52
200
|
|
53
|
-
|
54
|
-
|
201
|
+
# Check if pathname slug validation is enabled
|
202
|
+
# @return [Boolean] true if pathname slug validation is enabled
|
203
|
+
def validate_pathname_slug?
|
204
|
+
config_boolean?(validate_pathname_slug)
|
55
205
|
end
|
56
206
|
|
207
|
+
# Check if debug mode is enabled
|
208
|
+
# @return [Boolean] true if debug mode is enabled
|
57
209
|
def debug_mode?
|
58
|
-
config_boolean(debug_mode)
|
210
|
+
config_boolean?(debug_mode)
|
59
211
|
end
|
60
212
|
|
213
|
+
# Check if cache write access is enabled
|
214
|
+
# @return [Boolean] true if cache writing is enabled
|
61
215
|
def cache_write_enabled?
|
62
|
-
config_boolean(cache_write_enabled)
|
216
|
+
config_boolean?(cache_write_enabled)
|
63
217
|
end
|
64
218
|
|
65
|
-
# Check if path should
|
219
|
+
# Check if the given path should skip JWT authentication
|
220
|
+
# @param path [String] the request path to check
|
221
|
+
# @return [Boolean] true if the path should be skipped
|
66
222
|
def skip_path?(path)
|
67
223
|
return false if skip_paths.nil? || skip_paths.empty?
|
68
224
|
|
@@ -78,7 +234,12 @@ module RackJwtAegis
|
|
78
234
|
end
|
79
235
|
end
|
80
236
|
|
81
|
-
# Get mapped payload key
|
237
|
+
# Get the mapped payload key for a standard key
|
238
|
+
# @param standard_key [Symbol] the standard key to map
|
239
|
+
# @return [Symbol] the mapped key from payload_mapping, or the original key if no mapping exists
|
240
|
+
# @example
|
241
|
+
# config.payload_key(:user_id) #=> :sub (if mapped)
|
242
|
+
# config.payload_key(:user_id) #=> :user_id (if not mapped)
|
82
243
|
def payload_key(standard_key)
|
83
244
|
payload_mapping&.fetch(standard_key, standard_key) || standard_key
|
84
245
|
end
|
@@ -86,13 +247,12 @@ module RackJwtAegis
|
|
86
247
|
private
|
87
248
|
|
88
249
|
# Convert various falsy/truthy values to proper boolean for configuration
|
89
|
-
def config_boolean(value)
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
250
|
+
def config_boolean?(value)
|
251
|
+
if (value.is_a?(Numeric) && value.zero?) ||
|
252
|
+
(value.is_a?(String) && ['false', '0', '', 'no'].include?(value.downcase.strip))
|
253
|
+
return false
|
254
|
+
end
|
255
|
+
|
96
256
|
# Everything else is truthy
|
97
257
|
!!value
|
98
258
|
end
|
@@ -100,18 +260,19 @@ module RackJwtAegis
|
|
100
260
|
def set_defaults
|
101
261
|
@jwt_algorithm = 'HS256'
|
102
262
|
@validate_subdomain = false
|
103
|
-
@
|
263
|
+
@validate_pathname_slug = false
|
104
264
|
@rbac_enabled = false
|
105
|
-
@
|
106
|
-
@
|
265
|
+
@tenant_id_header_name = 'X-Tenant-Id'
|
266
|
+
@pathname_slug_pattern = %r{^/api/v1/([^/]+)/}
|
107
267
|
@skip_paths = []
|
108
268
|
@cache_write_enabled = false
|
269
|
+
@user_permissions_ttl = 1800 # 30 minutes default
|
109
270
|
@debug_mode = false
|
110
271
|
@payload_mapping = {
|
111
272
|
user_id: :user_id,
|
112
|
-
|
113
|
-
|
114
|
-
|
273
|
+
tenant_id: :tenant_id,
|
274
|
+
subdomain: :subdomain,
|
275
|
+
pathname_slugs: :pathname_slugs,
|
115
276
|
}
|
116
277
|
@unauthorized_response = { error: 'Authentication required' }
|
117
278
|
@forbidden_response = { error: 'Access denied' }
|
@@ -151,23 +312,23 @@ module RackJwtAegis
|
|
151
312
|
end
|
152
313
|
|
153
314
|
# Set default fallback for permission_cache_store when rbac_cache_store is provided
|
154
|
-
|
155
|
-
|
156
|
-
|
315
|
+
return unless !rbac_cache_store.nil? && permission_cache_store.nil?
|
316
|
+
|
317
|
+
@permission_cache_store = :memory # Default fallback
|
157
318
|
end
|
158
319
|
|
159
320
|
def validate_multi_tenant_settings!
|
160
|
-
if
|
161
|
-
raise ConfigurationError, '
|
321
|
+
if validate_pathname_slug? && pathname_slug_pattern.nil?
|
322
|
+
raise ConfigurationError, 'pathname_slug_pattern is required when validate_pathname_slug is true'
|
162
323
|
end
|
163
324
|
|
164
|
-
if validate_subdomain? && !payload_mapping.key?(:
|
165
|
-
raise ConfigurationError, 'payload_mapping must include :
|
325
|
+
if validate_subdomain? && !payload_mapping.key?(:subdomain)
|
326
|
+
raise ConfigurationError, 'payload_mapping must include :subdomain when validate_subdomain is true'
|
166
327
|
end
|
167
328
|
|
168
|
-
return unless
|
329
|
+
return unless validate_pathname_slug? && !payload_mapping.key?(:pathname_slugs)
|
169
330
|
|
170
|
-
raise ConfigurationError, 'payload_mapping must include :
|
331
|
+
raise ConfigurationError, 'payload_mapping must include :pathname_slugs when validate_pathname_slug is true'
|
171
332
|
end
|
172
333
|
end
|
173
334
|
end
|