verikloak-audience 0.2.5 → 0.2.7

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: 3b5e110926bc9c76f3fd1060601f6633ac033e1d8e4609e87be95a7a7d96f0a4
4
- data.tar.gz: 50fb3c748a6702d63a049a3dc70d27739bf80e00719880d195d00e854d6c0586
3
+ metadata.gz: 59d9195c0dfe4835b91a16bf8f08a58369f398890b5aff664979421a388a91d1
4
+ data.tar.gz: 8bd59e610df30f6eb1238b06f12a2778ddb0287264a967424751db38acae2f81
5
5
  SHA512:
6
- metadata.gz: c3555c88018205a6d4f7926c8316e9b188e7401a920376cae45cf1100830c29b47dfb3f8d6f0975bb37fbeab09758327f91975ede97e3dedc1427a4dd603f557
7
- data.tar.gz: 9e701c727b9049fe912fdca6763c3e88e57bf53f951d69303653ec0430d6ff814ff69d2bdef36277c98c9dbf47c29ef00f4b209c84a2cf6cfd3c70f300dca967
6
+ metadata.gz: 0d5d65f4ff5ca863d1f638b7e19a71edb256aa905bcc21b1ac49802b4c8db53697ed50d6f45bd67730a147a215972ddadbc78a403155e5d3b6ea1cf5b341dc69
7
+ data.tar.gz: c149176b446b3021204c039d2a07f9297676b57db52ae63f40812029b7f899c7398a425ba1a1542f058303e5a0615d51ebd793b5610d1c9feeb98bd7009c1c88
data/CHANGELOG.md CHANGED
@@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ---
9
9
 
10
+ ## [0.2.7] - 2025-09-27
11
+
12
+ ### Changed
13
+ - Rails Railtie now syncs `env_claims_key`, `required_aud`, and `resource_client` defaults with verikloak-rails configuration after boot.
14
+ - Generator template converted to ERB and bundled with the gem so `rails g verikloak:audience:install` produces the initializer without relying on Rails internals.
15
+ - Middleware option validation tightened to fail fast on unknown overrides with clearer error messages.
16
+
17
+ ### Documentation
18
+ - Added RubyDoc comments across the Railtie to clarify initializer responsibilities and helper methods.
19
+
20
+ ## [0.2.6] - 2025-09-23
21
+
22
+ ### Added
23
+ - Rails generator `verikloak:audience:install` to create an initializer that inserts the audience middleware once the core Verikloak middleware is available.
24
+
25
+ ### Changed
26
+ - Improved warning message when core Verikloak middleware is not present in the Rails middleware stack.
27
+ - Enhanced middleware insertion logic to provide clearer guidance on setup requirements.
28
+
10
29
  ## [0.2.5] - 2025-09-23
11
30
 
12
31
  ### Changed
data/README.md CHANGED
@@ -30,9 +30,17 @@ For the full error behaviour (response shapes, exception classes, logging hints)
30
30
  bundle add verikloak-audience
31
31
  ```
32
32
 
33
- ## Rack / Rails usage
33
+ In Rails applications, generate the initializer that automatically inserts the middleware:
34
34
 
35
- Insert **after** `Verikloak::Middleware`:
35
+ ```bash
36
+ rails g verikloak:audience:install
37
+ ```
38
+
39
+ This creates `config/initializers/verikloak_audience.rb` that will insert the audience middleware after the core Verikloak middleware once it's available.
40
+
41
+ ## Manual Rack / Rails setup
42
+
43
+ Alternatively, you can manually insert **after** `Verikloak::Middleware`:
36
44
 
37
45
  ```ruby
38
46
  # config/application.rb
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require 'rails/generators'
5
+ require 'rails/generators/base'
6
+ rescue LoadError
7
+ raise unless defined?(Rails::Generators::Base)
8
+ end
9
+
10
+ module Verikloak
11
+ module Audience
12
+ module Generators
13
+ # Installs the verikloak audience middleware configuration into a Rails
14
+ # application. This generator creates an initializer that inserts the
15
+ # audience middleware after the core Verikloak middleware once it is
16
+ # available.
17
+ class InstallGenerator < Rails::Generators::Base
18
+ source_root File.expand_path('templates', __dir__)
19
+
20
+ def create_initializer
21
+ template 'initializer.rb.erb', 'config/initializers/verikloak_audience.rb'
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Insert the audience middleware after the core Verikloak middleware once it
4
+ # has been loaded into the application.
5
+ if defined?(Rails) && Rails.respond_to?(:application)
6
+ Verikloak::Audience::Railtie.insert_middleware(Rails.application)
7
+ end
@@ -44,7 +44,7 @@ module Verikloak
44
44
  # @param required [Array<String>]
45
45
  # @return [Boolean]
46
46
  def strict_single?(claims, required)
47
- aud = Array(claims['aud']).map(&:to_s)
47
+ aud = normalized_audiences(claims)
48
48
  return false if required.empty?
49
49
 
50
50
  # Must contain all required and have no unexpected extra (order-insensitive)
@@ -57,7 +57,7 @@ module Verikloak
57
57
  # @param required [Array<String>]
58
58
  # @return [Boolean]
59
59
  def allow_account?(claims, required)
60
- aud = Array(claims['aud']).map(&:to_s)
60
+ aud = normalized_audiences(claims)
61
61
  return false if required.empty?
62
62
 
63
63
  # Permit 'account' extra
@@ -89,13 +89,10 @@ module Verikloak
89
89
  def suggest(claims, cfg)
90
90
  claims = normalize_claims(claims)
91
91
 
92
- aud = Array(claims['aud']).map(&:to_s)
93
- req = cfg.required_aud_list
94
- has_roles = !Array(claims.dig('resource_access', cfg.resource_client.to_s, 'roles')).empty?
95
-
96
- return :strict_single if aud.sort == req.sort
97
- return :allow_account if (aud - req) == ['account'] && (req - aud).empty?
98
- return :resource_or_aud if has_roles
92
+ required = cfg.required_aud_list
93
+ return :strict_single if strict_single?(claims, required)
94
+ return :allow_account if allow_account?(claims, required)
95
+ return :resource_or_aud if resource_or_aud?(claims, cfg.resource_client.to_s, required)
99
96
 
100
97
  :strict_single
101
98
  end
@@ -120,6 +117,16 @@ module Verikloak
120
117
  end
121
118
  module_function :normalize_claims
122
119
  private_class_method :normalize_claims
120
+
121
+ # Normalize audience claims into a predictable array of strings.
122
+ #
123
+ # @param claims [Hash]
124
+ # @return [Array<String>]
125
+ def normalized_audiences(claims)
126
+ Array(claims['aud']).map(&:to_s)
127
+ end
128
+ module_function :normalized_audiences
129
+ private_class_method :normalized_audiences
123
130
  end
124
131
  end
125
132
  end
@@ -23,6 +23,7 @@ module Verikloak
23
23
  # @return [Boolean]
24
24
  class Configuration
25
25
  DEFAULT_RESOURCE_CLIENT = 'rails-api'
26
+ DEFAULT_ENV_CLAIMS_KEY = 'verikloak.user'
26
27
 
27
28
  attr_accessor :profile, :required_aud, :resource_client,
28
29
  :suggest_in_logs
@@ -35,7 +36,7 @@ module Verikloak
35
36
  @profile = :strict_single
36
37
  @required_aud = []
37
38
  @resource_client = DEFAULT_RESOURCE_CLIENT
38
- self.env_claims_key = 'verikloak.user'
39
+ self.env_claims_key = DEFAULT_ENV_CLAIMS_KEY
39
40
  @suggest_in_logs = true
40
41
  end
41
42
 
@@ -121,23 +122,19 @@ module Verikloak
121
122
  # @return [void]
122
123
  def ensure_resource_client!(audiences)
123
124
  client = resource_client.to_s
125
+ error_msg = 'resource_client must match one of required_aud when using :resource_or_aud profile'
124
126
 
125
- needs_inference = needs_resource_client_inference?(client, audiences)
127
+ if needs_resource_client_inference?(client, audiences)
128
+ raise Verikloak::Audience::ConfigurationError, error_msg unless audiences.one?
129
+
130
+ self.resource_client = audiences.first
131
+ client = resource_client.to_s
126
132
 
127
- if needs_inference
128
- if audiences.one?
129
- self.resource_client = audiences.first
130
- client = resource_client.to_s
131
- else
132
- raise Verikloak::Audience::ConfigurationError,
133
- 'resource_client must match one of required_aud when using :resource_or_aud profile'
134
- end
135
133
  end
136
134
 
137
135
  return if audiences.include?(client)
138
136
 
139
- raise Verikloak::Audience::ConfigurationError,
140
- 'resource_client must match one of required_aud when using :resource_or_aud profile'
137
+ raise Verikloak::Audience::ConfigurationError, error_msg
141
138
  end
142
139
 
143
140
  # Decide whether the resource client should be inferred from the
@@ -57,16 +57,14 @@ module Verikloak
57
57
  # @return [void]
58
58
  def apply_overrides!(opts)
59
59
  cfg = @config
60
- opts.each_key do |key|
60
+ opts.each do |key, value|
61
61
  writer = "#{key}="
62
- next if cfg.respond_to?(writer)
62
+ unless cfg.respond_to?(writer)
63
+ raise Verikloak::Audience::ConfigurationError,
64
+ "unknown middleware option :#{key}"
65
+ end
63
66
 
64
- raise Verikloak::Audience::ConfigurationError,
65
- "unknown middleware option :#{key}"
66
- end
67
-
68
- opts.each do |k, v|
69
- cfg.public_send("#{k}=", v)
67
+ cfg.public_send(writer, value)
70
68
  end
71
69
  end
72
70
 
@@ -90,11 +88,9 @@ module Verikloak
90
88
  # @return [void]
91
89
  def log_warning(env, message)
92
90
  logger = env['verikloak.logger'] || env['rack.logger'] || env['action_dispatch.logger']
93
- if logger.respond_to?(:warn)
94
- logger.warn(message)
95
- else
96
- warn(message)
97
- end
91
+ return logger.warn(message) if logger.respond_to?(:warn)
92
+
93
+ Kernel.warn(message)
98
94
  end
99
95
  end
100
96
  end
@@ -35,6 +35,7 @@ module Verikloak
35
35
 
36
36
  initializer 'verikloak_audience.configuration' do
37
37
  config.after_initialize do
38
+ self.class.apply_verikloak_rails_configuration
38
39
  next if Verikloak::Audience::Railtie.skip_configuration_validation?
39
40
 
40
41
  Verikloak::Audience.config.validate!
@@ -42,15 +43,61 @@ module Verikloak
42
43
  end
43
44
 
44
45
  # Performs the insertion into the middleware stack when the core
45
- # Verikloak middleware is available. Extracted for testability without
46
- # requiring a full Rails boot process.
46
+ # Verikloak middleware is available and already present. Extracted for
47
+ # testability without requiring a full Rails boot process.
48
+ #
49
+ # Insert the audience middleware after the base Verikloak middleware when
50
+ # both are available on the stack.
47
51
  #
48
52
  # @param app [#middleware] An object exposing a Rack middleware stack via `#middleware`.
49
53
  # @return [void]
50
54
  def self.insert_middleware(app)
51
55
  return unless defined?(::Verikloak::Middleware)
52
56
 
53
- app.middleware.insert_after ::Verikloak::Middleware, ::Verikloak::Audience::Middleware
57
+ middleware_stack = app.middleware
58
+ return unless middleware_stack.respond_to?(:include?)
59
+
60
+ return if middleware_stack.include?(::Verikloak::Audience::Middleware)
61
+
62
+ unless middleware_stack.include?(::Verikloak::Middleware)
63
+ warn_missing_core_middleware
64
+ return
65
+ end
66
+
67
+ middleware_stack.insert_after ::Verikloak::Middleware, ::Verikloak::Audience::Middleware
68
+ end
69
+
70
+ WARNING_MESSAGE = <<~MSG
71
+ [verikloak-audience] Skipping automatic middleware insertion because ::Verikloak::Middleware
72
+ is not present in the Rails middleware stack.
73
+
74
+ To enable verikloak-audience, first ensure that the core Verikloak middleware (`Verikloak::Middleware`)
75
+ is added to your Rails middleware stack. Once the core middleware is present, you can run
76
+ `rails g verikloak:audience:install` to generate the initializer for the audience middleware,
77
+ or manually add:
78
+
79
+ config.middleware.insert_after Verikloak::Middleware, Verikloak::Audience::Middleware
80
+
81
+ This warning will disappear once the core middleware is properly configured and the audience
82
+ middleware is inserted.
83
+ MSG
84
+
85
+ # Logs a warning message when the core Verikloak middleware is missing
86
+ # from the Rails middleware stack. Uses the Rails logger if available,
87
+ # otherwise falls back to Kernel.warn for output.
88
+ #
89
+ # This method is called when automatic middleware insertion is skipped
90
+ # due to the absence of the required core middleware.
91
+ #
92
+ # @return [void]
93
+ def self.warn_missing_core_middleware
94
+ logger = (::Rails.logger if defined?(::Rails) && ::Rails.respond_to?(:logger))
95
+
96
+ if logger
97
+ logger.warn(WARNING_MESSAGE)
98
+ else
99
+ Kernel.warn(WARNING_MESSAGE)
100
+ end
54
101
  end
55
102
 
56
103
  # Rails short commands (`g`, `d`) are stripped from ARGV fairly early in
@@ -64,25 +111,32 @@ module Verikloak
64
111
  #
65
112
  # @return [Boolean]
66
113
  def self.skip_configuration_validation?
67
- command = first_rails_command
68
- return false unless command
114
+ tokens = first_cli_tokens
115
+ return false if tokens.empty?
116
+
117
+ command = tokens.first
118
+ return true if COMMANDS_SKIPPING_VALIDATION.include?(command)
69
119
 
70
- COMMANDS_SKIPPING_VALIDATION.include?(command) || verikloak_install_generator?(command)
120
+ tokens.any? { |token| verikloak_install_generator?(token) }
71
121
  end
72
122
 
73
- # Capture the first non-option argument passed to the Rails CLI,
74
- # ignoring wrapper tokens such as "rails".
123
+ # Capture the first non-option arguments passed to the Rails CLI,
124
+ # ignoring wrapper tokens such as "rails". Only the first two tokens are
125
+ # relevant for generator detection, so we keep the return list short.
75
126
  #
76
- # @return [String, nil]
77
- def self.first_rails_command
127
+ # @return [Array<String>] ordered CLI tokens that may signal a generator
128
+ def self.first_cli_tokens
129
+ tokens = []
130
+
78
131
  ARGV.each do |arg|
79
132
  next if arg.start_with?('-')
80
133
  next if arg == 'rails'
81
134
 
82
- return arg
135
+ tokens << arg
136
+ break if tokens.size >= 2
83
137
  end
84
138
 
85
- nil
139
+ tokens
86
140
  end
87
141
 
88
142
  # Detect whether the provided CLI token refers to a Verikloak install
@@ -95,6 +149,118 @@ module Verikloak
95
149
 
96
150
  command.start_with?('verikloak:') && command.end_with?(':install')
97
151
  end
152
+
153
+ class << self
154
+ # Synchronize configuration with verikloak-rails when it is present.
155
+ # Aligns env_claims_key, required_aud, and resource_client defaults so
156
+ # that both gems operate on the same Rack env payload and audience list.
157
+ #
158
+ # @return [void]
159
+ def apply_verikloak_rails_configuration
160
+ rails_config = verikloak_rails_config
161
+ return unless rails_config
162
+
163
+ Verikloak::Audience.configure do |cfg|
164
+ sync_env_claims_key(cfg, rails_config)
165
+ sync_required_aud(cfg, rails_config)
166
+ sync_resource_client(cfg, rails_config)
167
+ end
168
+ end
169
+
170
+ private
171
+
172
+ # Resolve the verikloak-rails configuration object if the gem is loaded.
173
+ #
174
+ # @return [Verikloak::Rails::Configuration, nil]
175
+ def verikloak_rails_config
176
+ return unless defined?(::Verikloak::Rails)
177
+ return unless ::Verikloak::Rails.respond_to?(:config)
178
+
179
+ ::Verikloak::Rails.config
180
+ rescue StandardError
181
+ nil
182
+ end
183
+
184
+ # Align the environment claims key with the one configured in verikloak-rails.
185
+ #
186
+ # @param cfg [Verikloak::Audience::Configuration]
187
+ # @param rails_config [Verikloak::Rails::Configuration]
188
+ # @return [void]
189
+ def sync_env_claims_key(cfg, rails_config)
190
+ return unless rails_config.respond_to?(:user_env_key)
191
+
192
+ user_key = rails_config.user_env_key
193
+ return if blank?(user_key)
194
+
195
+ current = cfg.env_claims_key
196
+ return unless current.nil? || current == Verikloak::Audience::Configuration::DEFAULT_ENV_CLAIMS_KEY
197
+
198
+ cfg.env_claims_key = user_key
199
+ end
200
+
201
+ # Populate required audiences from the verikloak-rails configuration when absent.
202
+ #
203
+ # @param cfg [Verikloak::Audience::Configuration]
204
+ # @param rails_config [Verikloak::Rails::Configuration]
205
+ # @return [void]
206
+ def sync_required_aud(cfg, rails_config)
207
+ return unless cfg_required_aud_blank?(cfg)
208
+ return unless rails_config.respond_to?(:audience)
209
+
210
+ audiences = normalized_audiences(rails_config.audience)
211
+ return if audiences.empty?
212
+
213
+ cfg.required_aud = audiences.size == 1 ? audiences.first : audiences
214
+ end
215
+
216
+ # Infer the resource client based on the configured audience when possible.
217
+ #
218
+ # @param cfg [Verikloak::Audience::Configuration]
219
+ # @param rails_config [Verikloak::Rails::Configuration]
220
+ # @return [void]
221
+ def sync_resource_client(cfg, rails_config)
222
+ return unless rails_config.respond_to?(:audience)
223
+
224
+ audiences = normalized_audiences(rails_config.audience)
225
+ return unless audiences.size == 1
226
+
227
+ current_client = cfg.resource_client
228
+ unless blank?(current_client) || current_client == Verikloak::Audience::Configuration::DEFAULT_RESOURCE_CLIENT
229
+ return
230
+ end
231
+
232
+ cfg.resource_client = audiences.first
233
+ end
234
+
235
+ # Determine whether the audience configuration is effectively empty.
236
+ #
237
+ # @param cfg [Verikloak::Audience::Configuration]
238
+ # @return [Boolean]
239
+ def cfg_required_aud_blank?(cfg)
240
+ value_blank?(cfg.required_aud)
241
+ end
242
+
243
+ # Generic blank? helper that tolerates nil, empty, or blank-ish values.
244
+ #
245
+ # @param value [Object]
246
+ # @return [Boolean]
247
+ def value_blank?(value)
248
+ return true if value.nil?
249
+ return true if value.respond_to?(:empty?) && value.empty?
250
+
251
+ value.to_s.empty?
252
+ end
253
+
254
+ alias blank? value_blank?
255
+
256
+ # Coerce the given source into an array of non-empty string audiences.
257
+ #
258
+ # @param source [Object]
259
+ # @return [Array<String>]
260
+ def normalized_audiences(source)
261
+ Array(source).compact.map(&:to_s).reject(&:empty?)
262
+ end
263
+ end
98
264
  end
99
265
  end
100
266
  end
@@ -4,6 +4,6 @@ module Verikloak
4
4
  module Audience
5
5
  # Current gem version.
6
6
  # @return [String]
7
- VERSION = '0.2.5'
7
+ VERSION = '0.2.7'
8
8
  end
9
9
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: verikloak-audience
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.5
4
+ version: 0.2.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - taiyaky
@@ -59,6 +59,8 @@ files:
59
59
  - CHANGELOG.md
60
60
  - LICENSE
61
61
  - README.md
62
+ - lib/generators/verikloak/audience/install/install_generator.rb
63
+ - lib/generators/verikloak/audience/install/templates/initializer.rb.erb
62
64
  - lib/verikloak-audience.rb
63
65
  - lib/verikloak/audience.rb
64
66
  - lib/verikloak/audience/checker.rb
@@ -74,7 +76,7 @@ metadata:
74
76
  source_code_uri: https://github.com/taiyaky/verikloak-audience
75
77
  changelog_uri: https://github.com/taiyaky/verikloak-audience/blob/main/CHANGELOG.md
76
78
  bug_tracker_uri: https://github.com/taiyaky/verikloak-audience/issues
77
- documentation_uri: https://rubydoc.info/gems/verikloak-audience/0.2.5
79
+ documentation_uri: https://rubydoc.info/gems/verikloak-audience/0.2.7
78
80
  rubygems_mfa_required: 'true'
79
81
  rdoc_options: []
80
82
  require_paths: