verikloak-audience 0.2.6 → 0.2.8

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: 1ba5a1ca7162831839679e843f5def72fb8efeecb4b88e9cded4d6aa283c66a3
4
- data.tar.gz: 4a4a71f5408b0b77cb63ffa9b5bc998a721de2083133234b1dfa2e96c4afb609
3
+ metadata.gz: 88a1386b46aebb2940ede856bdde5993bd68a05000d71f9f5784d73dd85c2fc6
4
+ data.tar.gz: cf84cfe50d3a6acf004ce0535b02b1644693d2c89b78e422d809b0f184e53cc3
5
5
  SHA512:
6
- metadata.gz: d54e6ccb8c86555cb3c3821355912fc283e155fb103cdb7df5a0166e59163f9ea086ea1a7fcdbb7c7a82aa76b65b120d166379cfacd18fcff610bdb9fe4fc65a
7
- data.tar.gz: c610ec408d85b8bd89b1a7f60d112731b3dbcaac696e66a993caa790c2ee5bb8c8b2f43227c87c681b3a1384fb59cca43e267ecd4a0f69f43f924187cc90f0ea
6
+ metadata.gz: c97758819fac422b4f81e5d22655b677f7d1bd762430e8fd68d038b1fdf9adce004da3261b6de5d2af66874a9dc2a417a1c1272933d51acd979e55416131e4dc
7
+ data.tar.gz: b12d1fa1f74f5b4d1bb17e461956ecabf4e094eb673b9a03e47bb174603ebcacf3d6bc3a48bab9854df8c3d94b3f6a7d252080f83248ca841354465554191740
data/CHANGELOG.md CHANGED
@@ -7,6 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ---
9
9
 
10
+ ## [0.2.8] - 2025-09-28
11
+
12
+ ### Changed
13
+ - Require `rails/generators` directly in the install generator and inherit via `::Rails::Generators::Base` to avoid constant lookup issues in non-Rails contexts.
14
+ - Document the generator purpose with a `desc` string so `rails g --help` includes a clearer description.
15
+
16
+ ### Fixed
17
+ - Stub Thor semantics in the generator spec and restore `tmpdir` usage so error handling tests assert `Thor::Error`, matching the real generator behavior.
18
+ - Ensure the generator spec recreates missing template and destination conflict scenarios using the same API surface Rails provides.
19
+
20
+ ## [0.2.7] - 2025-09-27
21
+
22
+ ### Changed
23
+ - Rails Railtie now syncs `env_claims_key`, `required_aud`, and `resource_client` defaults with verikloak-rails configuration after boot.
24
+ - Generator template converted to ERB and bundled with the gem so `rails g verikloak:audience:install` produces the initializer without relying on Rails internals.
25
+ - Middleware option validation tightened to fail fast on unknown overrides with clearer error messages.
26
+
27
+ ### Documentation
28
+ - Added RubyDoc comments across the Railtie to clarify initializer responsibilities and helper methods.
29
+
10
30
  ## [0.2.6] - 2025-09-23
11
31
 
12
32
  ### Added
@@ -1,11 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- begin
4
- require 'rails/generators'
5
- require 'rails/generators/base'
6
- rescue LoadError
7
- # Allow the generator to be required without Rails.
8
- end
3
+ require 'rails/generators'
9
4
 
10
5
  module Verikloak
11
6
  module Audience
@@ -14,13 +9,13 @@ module Verikloak
14
9
  # application. This generator creates an initializer that inserts the
15
10
  # audience middleware after the core Verikloak middleware once it is
16
11
  # available.
17
- class InstallGenerator < (defined?(Rails::Generators::Base) ? Rails::Generators::Base : Object)
18
- source_root File.expand_path('templates', __dir__) if respond_to?(:source_root)
12
+ class InstallGenerator < ::Rails::Generators::Base
13
+ source_root File.expand_path('templates', __dir__)
19
14
 
20
- def create_initializer
21
- return unless respond_to?(:template, true)
15
+ desc 'Creates an initializer for verikloak-audience middleware integration.'
22
16
 
23
- template 'verikloak_audience.rb.tt', 'config/initializers/verikloak_audience.rb'
17
+ def create_initializer
18
+ template 'initializer.rb.erb', 'config/initializers/verikloak_audience.rb'
24
19
  end
25
20
  end
26
21
  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!
@@ -45,6 +46,9 @@ module Verikloak
45
46
  # Verikloak middleware is available and already present. Extracted for
46
47
  # testability without requiring a full Rails boot process.
47
48
  #
49
+ # Insert the audience middleware after the base Verikloak middleware when
50
+ # both are available on the stack.
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)
@@ -53,6 +57,8 @@ module Verikloak
53
57
  middleware_stack = app.middleware
54
58
  return unless middleware_stack.respond_to?(:include?)
55
59
 
60
+ return if middleware_stack.include?(::Verikloak::Audience::Middleware)
61
+
56
62
  unless middleware_stack.include?(::Verikloak::Middleware)
57
63
  warn_missing_core_middleware
58
64
  return
@@ -85,7 +91,7 @@ module Verikloak
85
91
  #
86
92
  # @return [void]
87
93
  def self.warn_missing_core_middleware
88
- logger = rails_logger
94
+ logger = (::Rails.logger if defined?(::Rails) && ::Rails.respond_to?(:logger))
89
95
 
90
96
  if logger
91
97
  logger.warn(WARNING_MESSAGE)
@@ -94,20 +100,6 @@ module Verikloak
94
100
  end
95
101
  end
96
102
 
97
- # Retrieves the Rails application logger if available.
98
- #
99
- # This method safely attempts to access the Rails logger, returning nil
100
- # if Rails is not defined, doesn't respond to the logger method, or if
101
- # the logger itself is nil.
102
- #
103
- # @return [Logger, nil] the Rails logger instance, or nil if unavailable
104
- def self.rails_logger
105
- return unless defined?(::Rails)
106
- return unless ::Rails.respond_to?(:logger)
107
-
108
- ::Rails.logger
109
- end
110
-
111
103
  # Rails short commands (`g`, `d`) are stripped from ARGV fairly early in
112
104
  # the boot process. Treat `verikloak:*:install` generators as safe so they
113
105
  # can run before configuration files exist.
@@ -119,25 +111,32 @@ module Verikloak
119
111
  #
120
112
  # @return [Boolean]
121
113
  def self.skip_configuration_validation?
122
- command = first_rails_command
123
- 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)
124
119
 
125
- COMMANDS_SKIPPING_VALIDATION.include?(command) || verikloak_install_generator?(command)
120
+ tokens.any? { |token| verikloak_install_generator?(token) }
126
121
  end
127
122
 
128
- # Capture the first non-option argument passed to the Rails CLI,
129
- # 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.
130
126
  #
131
- # @return [String, nil]
132
- 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
+
133
131
  ARGV.each do |arg|
134
132
  next if arg.start_with?('-')
135
133
  next if arg == 'rails'
136
134
 
137
- return arg
135
+ tokens << arg
136
+ break if tokens.size >= 2
138
137
  end
139
138
 
140
- nil
139
+ tokens
141
140
  end
142
141
 
143
142
  # Detect whether the provided CLI token refers to a Verikloak install
@@ -150,6 +149,118 @@ module Verikloak
150
149
 
151
150
  command.start_with?('verikloak:') && command.end_with?(':install')
152
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
153
264
  end
154
265
  end
155
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.6'
7
+ VERSION = '0.2.8'
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.6
4
+ version: 0.2.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - taiyaky
@@ -60,6 +60,7 @@ files:
60
60
  - LICENSE
61
61
  - README.md
62
62
  - lib/generators/verikloak/audience/install/install_generator.rb
63
+ - lib/generators/verikloak/audience/install/templates/initializer.rb.erb
63
64
  - lib/verikloak-audience.rb
64
65
  - lib/verikloak/audience.rb
65
66
  - lib/verikloak/audience/checker.rb
@@ -75,7 +76,7 @@ metadata:
75
76
  source_code_uri: https://github.com/taiyaky/verikloak-audience
76
77
  changelog_uri: https://github.com/taiyaky/verikloak-audience/blob/main/CHANGELOG.md
77
78
  bug_tracker_uri: https://github.com/taiyaky/verikloak-audience/issues
78
- documentation_uri: https://rubydoc.info/gems/verikloak-audience/0.2.6
79
+ documentation_uri: https://rubydoc.info/gems/verikloak-audience/0.2.8
79
80
  rubygems_mfa_required: 'true'
80
81
  rdoc_options: []
81
82
  require_paths: