verikloak-audience 0.2.9 → 0.3.1

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: 53894e2af5f290a98ec6f438b9d0271e1f8e86d18c6404c9b0394c5cd5741312
4
- data.tar.gz: 0cc799a43b2559f34113e6d738d1e2ee004c1292c86115dc83918ca32aeef04f
3
+ metadata.gz: 400925f130b177647a3d7007cf4e43b566451e5b131968fbca2946d8a2f47d06
4
+ data.tar.gz: 89fa1b482fe9cdaa9278f56380cc59bba52325dd7db6b0e9aeb473e658568eb8
5
5
  SHA512:
6
- metadata.gz: 9536f08a7ce2f5de4113483215386221dec1867c6affbea7a0e3074239017c1d7e0d4135476f72cf45cbda1de2fc17781fdf9662d038eb110afdb336a5ee6bb3
7
- data.tar.gz: 5730013314cd5dd393c9936256a5fe31cb3aa13fac4c3dda6dabb7ca308424167235d0482167f2cc92c13e26ee5bdef23eaeddab5f01b7898ea31474aeb3211d
6
+ metadata.gz: 0d39de8ac4bfb587b968e4017e36edc03fbe49c4a7495963671aacb32a5d65fa06b39957922344b1dd3336614bb20b5ad8eeef9c7248eb6ea9c82dd772323072
7
+ data.tar.gz: bc01de7533c0388fc4e9f8ed35747b9136c04f166cbcfe0dbc75ff2895703aed960ed0d3845beb0dd3ecc2a3bffae786609864a59ae335672f6754a216adda4d
data/CHANGELOG.md CHANGED
@@ -7,6 +7,43 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ---
9
9
 
10
+ ## [0.3.1] - 2026-01-01
11
+
12
+ ### Added
13
+ - **`:any_match` profile**: New audience validation profile that passes when at least one of the required audiences is present in the token.
14
+ - **`skip_paths` configuration**: Skip audience validation for specific paths (e.g., health checks). Supports exact matches, prefix matches, wildcard patterns, and Regexp.
15
+ - **Generator improvements**: Install generator now includes `Verikloak::Audience.configure` block with all four profiles documented.
16
+
17
+ ### Fixed
18
+ - **Regexp support in `skip_paths`**: Fixed `NoMethodError` when passing Regexp patterns.
19
+ - **Warning message accuracy**: Unconfigured warning now correctly states "ALL requests will be rejected with 403".
20
+ - Configuration sync now happens before middleware insertion.
21
+
22
+ ### Changed
23
+ - README now correctly describes that the Railtie automatically inserts the middleware.
24
+ - Removed manual `insert_middleware` call from generated initializer (Railtie handles this automatically).
25
+
26
+ ## [0.3.0] - 2026-01-01
27
+
28
+ ### Fixed
29
+ - **Rails 8.x+ middleware insertion**: Fixed automatic middleware insertion not working in Rails 8.x+ due to queued middleware operations not being visible via `include?` checks.
30
+
31
+ ### Changed
32
+ - Railtie initializer now specifies `before: :build_middleware_stack` to ensure middleware insertion is queued before stack construction.
33
+ - Middleware insertion now uses `app.config.middleware` instead of `app.middleware` for more reliable queuing in Rails 8.x+.
34
+ - Extracted `middleware_not_found_error?` method to handle error detection across Rails versions (Rails 7's `MiddlewareNotFound` and Rails 8+'s `RuntimeError`).
35
+
36
+ ### Added
37
+ - `@middleware_insertion_attempted` class variable to prevent duplicate middleware insertion when both railtie and generator initializer run.
38
+ - `reset_middleware_insertion_flag!` method for testing purposes.
39
+ - Support for alternative error message patterns (`"does not exist"`, `/middleware.*not found/i`) for future Rails version compatibility.
40
+ - Comprehensive test coverage for:
41
+ - `include?` early return when middleware is already present
42
+ - Duplicate insertion prevention via flag
43
+ - Rails 7 `MiddlewareNotFound` exception handling
44
+ - Alternative error message patterns
45
+ - Unexpected exception re-raising
46
+
10
47
  ## [0.2.9] - 2025-12-31
11
48
 
12
49
  ### Added
data/README.md CHANGED
@@ -22,6 +22,7 @@ For the full error behaviour (response shapes, exception classes, logging hints)
22
22
  |---------|---------|-----------------------------------------------------------|
23
23
  | `:strict_single` *(recommended)* | Requires `aud` to match `required_aud` exactly (order-insensitive, no extras). | APIs where audiences are cleanly separated. Logged suggestion when the observed `aud` already equals the configured list. |
24
24
  | `:allow_account` | Allows `account` in addition to required audiences (e.g. `["rails-api","account"]`). | SPA + API mixes where Keycloak always emits `account`. Suggested when strict mode fails and the log shows `profile=:allow_account`. |
25
+ | `:any_match` | Passes when at least one of the required audiences is present. | Shared APIs where multiple clients may have overlapping audiences. More permissive than `:strict_single`. |
25
26
  | `:resource_or_aud` | Passes when `resource_access[client].roles` is present; otherwise falls back to `:allow_account`. | Services relying on resource roles. Suggested when logs output `profile=:resource_or_aud`. |
26
27
 
27
28
  ## Installation
@@ -30,13 +31,14 @@ For the full error behaviour (response shapes, exception classes, logging hints)
30
31
  bundle add verikloak-audience
31
32
  ```
32
33
 
33
- In Rails applications, generate the initializer that automatically inserts the middleware:
34
+ In Rails applications, run the generator to create a configuration initializer:
34
35
 
35
36
  ```bash
36
37
  rails g verikloak:audience:install
37
38
  ```
38
39
 
39
- This creates `config/initializers/verikloak_audience.rb` that will insert the audience middleware after the core Verikloak middleware once it's available.
40
+ This creates `config/initializers/verikloak_audience.rb` with configuration options.
41
+ The middleware is automatically inserted by the Railtie after `Verikloak::Middleware`.
40
42
 
41
43
  ## Manual Rack / Rails setup
42
44
 
@@ -58,7 +60,7 @@ See [`examples/rack.ru`](examples/rack.ru) for a full Rack sample. In Rails, alw
58
60
 
59
61
  | Option | Type | Default | Description |
60
62
  |--------|------|---------|-------------|
61
- | `profile` | Symbol | `:strict_single` | Profile selector. Accepts `:strict_single`, `:allow_account`, or `:resource_or_aud`. |
63
+ | `profile` | Symbol | `:strict_single` | Profile selector. Accepts `:strict_single`, `:allow_account`, `:any_match`, or `:resource_or_aud`. |
62
64
  | `required_aud` | Array/String/Symbol | `[]` | Required audience values; coerced to an array internally. |
63
65
  | `resource_client` | String | `"rails-api"` | Keycloak client id used to look up `resource_access[client].roles`. |
64
66
  | `env_claims_key` | String | `"verikloak.user"` | Rack env key where verified claims are stored. |
@@ -1,7 +1,24 @@
1
1
  # frozen_string_literal: true
2
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)
3
+ # Verikloak Audience Configuration
4
+ #
5
+ # Configure the audience validation profile and settings.
6
+ # This middleware provides stricter audience validation than core Verikloak.
7
+ #
8
+ # The middleware is automatically inserted after Verikloak::Middleware by the
9
+ # Railtie. No manual middleware insertion is required.
10
+ #
11
+ Verikloak::Audience.configure do |config|
12
+ # Audience validation profile
13
+ # :strict_single - Only exact single audience match (default, recommended)
14
+ # :allow_account - Allow "account" alongside your API audience
15
+ # :any_match - At least one required audience is present (more permissive)
16
+ # :resource_or_aud - Check resource_access roles first, fallback to :allow_account
17
+ # config.profile = :strict_single
18
+
19
+ # Required audience(s) - synced from verikloak-rails automatically
20
+ # config.required_aud = ['rails-api']
21
+
22
+ # Skip paths for audience validation (synced from verikloak-rails automatically)
23
+ # config.skip_paths = %w[/up /health]
7
24
  end
@@ -7,7 +7,7 @@ module Verikloak
7
7
  # This module provides predicate helpers used by the middleware to decide
8
8
  # whether a given set of claims satisfies the configured profile.
9
9
  module Checker
10
- VALID_PROFILES = %i[strict_single allow_account resource_or_aud].freeze
10
+ VALID_PROFILES = %i[strict_single allow_account any_match resource_or_aud].freeze
11
11
 
12
12
  module_function
13
13
 
@@ -33,6 +33,8 @@ module Verikloak
33
33
  strict_single?(claims, cfg.required_aud_list)
34
34
  when :allow_account
35
35
  allow_account?(claims, cfg.required_aud_list)
36
+ when :any_match
37
+ any_match?(claims, cfg.required_aud_list)
36
38
  when :resource_or_aud
37
39
  resource_or_aud?(claims, cfg.resource_client.to_s, cfg.required_aud_list)
38
40
  end
@@ -66,6 +68,20 @@ module Verikloak
66
68
  extras.empty? && (required - aud).empty?
67
69
  end
68
70
 
71
+ # Validate that at least one required audience is present in the token.
72
+ # More permissive than :strict_single; useful when multiple clients share audiences.
73
+ #
74
+ # @param claims [Hash]
75
+ # @param required [Array<String>]
76
+ # @return [Boolean]
77
+ def any_match?(claims, required)
78
+ aud = normalized_audiences(claims)
79
+ return false if required.empty?
80
+
81
+ # At least one of the required audiences must be present
82
+ aud.intersect?(required.map(&:to_s))
83
+ end
84
+
69
85
  # Permit when resource roles exist for the client; otherwise fallback to
70
86
  # {#allow_account?}.
71
87
  #
@@ -92,6 +108,7 @@ module Verikloak
92
108
  required = cfg.required_aud_list
93
109
  return :strict_single if strict_single?(claims, required)
94
110
  return :allow_account if allow_account?(claims, required)
111
+ return :any_match if any_match?(claims, required)
95
112
  return :resource_or_aud if resource_or_aud?(claims, cfg.resource_client.to_s, required)
96
113
 
97
114
  :strict_single
@@ -8,7 +8,7 @@ module Verikloak
8
8
  #
9
9
  # @!attribute [rw] profile
10
10
  # The enforcement profile to use.
11
- # @return [:strict_single, :allow_account, :resource_or_aud]
11
+ # @return [:strict_single, :allow_account, :any_match, :resource_or_aud]
12
12
  # @!attribute [rw] required_aud
13
13
  # Required audience(s). Can be a String/Symbol or an Array of them.
14
14
  # @return [Array<String,Symbol>, String, Symbol]
@@ -21,12 +21,16 @@ module Verikloak
21
21
  # @!attribute [rw] suggest_in_logs
22
22
  # Whether to log a suggestion when audience validation fails.
23
23
  # @return [Boolean]
24
+ # @!attribute [rw] skip_paths
25
+ # Paths to skip audience validation for (e.g., health checks).
26
+ # Synced from verikloak-rails when available.
27
+ # @return [Array<String, Regexp>]
24
28
  class Configuration
25
29
  DEFAULT_RESOURCE_CLIENT = 'rails-api'
26
30
  DEFAULT_ENV_CLAIMS_KEY = 'verikloak.user'
27
31
 
28
32
  attr_accessor :profile, :required_aud, :resource_client,
29
- :suggest_in_logs
33
+ :suggest_in_logs, :skip_paths
30
34
  attr_reader :env_claims_key
31
35
 
32
36
  # Create a configuration with safe defaults.
@@ -38,6 +42,7 @@ module Verikloak
38
42
  @resource_client = DEFAULT_RESOURCE_CLIENT
39
43
  self.env_claims_key = DEFAULT_ENV_CLAIMS_KEY
40
44
  @suggest_in_logs = true
45
+ @skip_paths = []
41
46
  end
42
47
 
43
48
  # Ensure `dup` produces an independent copy.
@@ -51,6 +56,7 @@ module Verikloak
51
56
  @resource_client = safe_dup(source.resource_client)
52
57
  self.env_claims_key = safe_dup(source.env_claims_key)
53
58
  @suggest_in_logs = source.suggest_in_logs
59
+ @skip_paths = source.skip_paths&.dup || []
54
60
  end
55
61
 
56
62
  # Coerce `required_aud` into an array of strings.
@@ -15,11 +15,12 @@ module Verikloak
15
15
  class Middleware
16
16
  # @param app [#call] next Rack application
17
17
  # @param opts [Hash] configuration overrides (see {Configuration})
18
- # @option opts [Symbol] :profile
18
+ # @option opts [Symbol] :profile (:strict_single, :allow_account, :any_match, :resource_or_aud)
19
19
  # @option opts [Array<String>,String,Symbol] :required_aud
20
20
  # @option opts [String] :resource_client
21
21
  # @option opts [String] :env_claims_key
22
22
  # @option opts [Boolean] :suggest_in_logs
23
+ # @option opts [Array<String, Regexp>] :skip_paths
23
24
  def initialize(app, **opts)
24
25
  @app = app
25
26
  @config = Verikloak::Audience.config.dup
@@ -32,6 +33,9 @@ module Verikloak
32
33
  # @param env [Hash] Rack environment
33
34
  # @return [Array(Integer, Hash, #each)] Rack response triple
34
35
  def call(env)
36
+ # Skip audience validation for configured paths (e.g., health checks)
37
+ return @app.call(env) if skip_path?(env)
38
+
35
39
  env_key = @config.env_claims_key
36
40
  claims = env[env_key] || env[env_key&.to_sym] || {}
37
41
  return @app.call(env) if Checker.ok?(claims, @config)
@@ -51,6 +55,36 @@ module Verikloak
51
55
 
52
56
  private
53
57
 
58
+ # Check if the current request path should skip audience validation.
59
+ #
60
+ # @param env [Hash] Rack environment
61
+ # @return [Boolean]
62
+ def skip_path?(env)
63
+ paths = @config.skip_paths
64
+ return false if paths.nil? || paths.empty?
65
+
66
+ request_path = env['PATH_INFO'] || env['REQUEST_PATH'] || ''
67
+ paths.any? { |pattern| path_matches?(request_path, pattern) }
68
+ end
69
+
70
+ # Determine if a request path matches the given pattern.
71
+ # Supports exact matches and prefix matches (pattern ending with *).
72
+ #
73
+ # @param request_path [String]
74
+ # @param pattern [String, Regexp]
75
+ # @return [Boolean]
76
+ def path_matches?(request_path, pattern)
77
+ return false if pattern.nil?
78
+ return request_path.match?(pattern) if pattern.is_a?(Regexp)
79
+ return false if pattern.empty?
80
+
81
+ if pattern.end_with?('*')
82
+ request_path.start_with?(pattern.chomp('*'))
83
+ else
84
+ request_path == pattern || request_path.start_with?("#{pattern}/")
85
+ end
86
+ end
87
+
54
88
  # Apply provided options to the configuration instance.
55
89
  #
56
90
  # @param opts [Hash] raw overrides provided to the middleware
@@ -70,14 +104,24 @@ module Verikloak
70
104
 
71
105
  # Determine whether configuration validation should run. This allows
72
106
  # Rails generators to boot without a fully-populated configuration since
73
- # the install task is responsible for creating it.
107
+ # the install task is responsible for creating it. Also skips validation
108
+ # when configuration is not explicitly set up.
74
109
  #
75
110
  # @return [Boolean]
76
111
  def skip_validation?
77
- return false unless defined?(::Verikloak::Audience::Railtie)
78
- return false unless ::Verikloak::Audience::Railtie.respond_to?(:skip_configuration_validation?)
112
+ # Skip if Railtie indicates generator mode
113
+ if defined?(::Verikloak::Audience::Railtie)
114
+ if ::Verikloak::Audience::Railtie.respond_to?(:skip_configuration_validation?) && ::Verikloak::Audience::Railtie.skip_configuration_validation?
115
+ return true
116
+ end
117
+
118
+ # Skip if configuration is incomplete (no audiences configured)
119
+ if ::Verikloak::Audience::Railtie.respond_to?(:skip_unconfigured_validation?) && ::Verikloak::Audience::Railtie.skip_unconfigured_validation?
120
+ return true
121
+ end
122
+ end
79
123
 
80
- ::Verikloak::Audience::Railtie.skip_configuration_validation?
124
+ false
81
125
  end
82
126
 
83
127
  # Emit a warning for failed audience checks using request-scoped loggers
@@ -4,69 +4,8 @@ require 'rails/railtie'
4
4
 
5
5
  module Verikloak
6
6
  module Audience
7
- # Rails integration for verikloak-audience.
8
- #
9
- # This Railtie automatically inserts {Verikloak::Audience::Middleware}
10
- # into the Rails middleware stack directly after {::Verikloak::Middleware}
11
- # when it is present.
12
- #
13
- # If the core Verikloak middleware is not available, nothing is inserted.
14
- # You may still customize the placement in your application via
15
- # `config.middleware`.
16
- #
17
- # @example Configure placement and options
18
- # # config/application.rb
19
- # config.middleware.insert_after Verikloak::Middleware,
20
- # Verikloak::Audience::Middleware,
21
- # profile: :allow_account,
22
- # required_aud: ['rails-api'],
23
- # resource_client: 'rails-api',
24
- # env_claims_key: 'verikloak.user',
25
- # suggest_in_logs: true
26
- class Railtie < ::Rails::Railtie
27
- # Adds the audience middleware after the core Verikloak middleware
28
- # when available.
29
- #
30
- # @param app [Rails::Application] the Rails application instance
31
- initializer 'verikloak_audience.middleware' do |app|
32
- # Insert automatically after core verikloak if present
33
- self.class.insert_middleware(app)
34
- end
35
-
36
- initializer 'verikloak_audience.configuration' do
37
- config.after_initialize do
38
- self.class.apply_verikloak_rails_configuration
39
- next if Verikloak::Audience::Railtie.skip_configuration_validation?
40
-
41
- Verikloak::Audience.config.validate!
42
- end
43
- end
44
-
45
- # Performs the insertion into the middleware stack when the core
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.
51
- #
52
- # @param app [#middleware] An object exposing a Rack middleware stack via `#middleware`.
53
- # @return [void]
54
- def self.insert_middleware(app)
55
- return unless defined?(::Verikloak::Middleware)
56
-
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
-
7
+ # Warning messages for Railtie.
8
+ module RailtieWarnings
70
9
  WARNING_MESSAGE = <<~MSG
71
10
  [verikloak-audience] Skipping automatic middleware insertion because ::Verikloak::Middleware
72
11
  is not present in the Rails middleware stack.
@@ -82,184 +21,270 @@ module Verikloak
82
21
  middleware is inserted.
83
22
  MSG
84
23
 
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
24
+ CORE_NOT_CONFIGURED_WARNING = <<~MSG
25
+ [verikloak-audience] Skipping automatic middleware insertion because verikloak-rails
26
+ is not fully configured (discovery_url is not set).
27
+
28
+ The audience middleware requires the core Verikloak middleware to be present in the stack.
29
+ Please configure verikloak-rails first by setting the discovery_url in your initializer.
30
+ MSG
31
+
32
+ UNCONFIGURED_WARNING = <<~MSG
33
+ [verikloak-audience] Skipping configuration validation because required_aud is not configured.
34
+
35
+ To use verikloak-audience, you must configure at least one required audience.
36
+ Run `rails g verikloak:audience:install` to generate the initializer, or add:
37
+
38
+ Verikloak::Audience.configure do |config|
39
+ config.required_aud = ['your-audience']
40
+ end
41
+
42
+ WARNING: Without this configuration, ALL requests will be rejected with 403.
43
+ MSG
44
+
45
+ def log_warning(message)
94
46
  logger = (::Rails.logger if defined?(::Rails) && ::Rails.respond_to?(:logger))
47
+ logger ? logger.warn(message) : Kernel.warn(message)
48
+ end
49
+
50
+ def warn_core_not_configured = log_warning(CORE_NOT_CONFIGURED_WARNING)
51
+ def warn_missing_core_middleware = log_warning(WARNING_MESSAGE)
52
+ def warn_unconfigured = log_warning(UNCONFIGURED_WARNING)
53
+ end
95
54
 
96
- if logger
97
- logger.warn(WARNING_MESSAGE)
98
- else
99
- Kernel.warn(WARNING_MESSAGE)
55
+ # Helper methods for Railtie configuration sync.
56
+ module RailtieHelpers
57
+ include RailtieWarnings
58
+
59
+ def apply_verikloak_rails_configuration
60
+ rails_config = verikloak_rails_config
61
+ return unless rails_config
62
+
63
+ Verikloak::Audience.configure do |cfg|
64
+ sync_env_claims_key(cfg, rails_config)
65
+ sync_required_aud(cfg, rails_config)
66
+ sync_resource_client(cfg, rails_config)
67
+ sync_skip_paths(cfg, rails_config)
100
68
  end
101
69
  end
102
70
 
103
- # Rails short commands (`g`, `d`) are stripped from ARGV fairly early in
104
- # the boot process. Treat `verikloak:*:install` generators as safe so they
105
- # can run before configuration files exist.
106
- COMMANDS_SKIPPING_VALIDATION = %w[generate g destroy d].freeze
71
+ def discovery_url_configured?(value)
72
+ return false unless value
73
+ return !value.blank? if value.respond_to?(:blank?)
74
+ return !value.empty? if value.respond_to?(:empty?)
107
75
 
108
- # Detect whether Rails is currently executing a generator-style command.
109
- # Generators boot the application before configuration exists, so we
110
- # temporarily skip validation to let the install task complete.
111
- #
112
- # @return [Boolean]
113
- def self.skip_configuration_validation?
114
- tokens = first_cli_tokens
115
- return false if tokens.empty?
76
+ true
77
+ end
116
78
 
117
- command = tokens.first
118
- return true if COMMANDS_SKIPPING_VALIDATION.include?(command)
79
+ def middleware_not_found_error?(error)
80
+ return true if error.class.name.to_s.include?('MiddlewareNotFound')
119
81
 
120
- tokens.any? { |token| verikloak_install_generator?(token) }
82
+ message = error.message.to_s
83
+ message.include?('No such middleware') ||
84
+ message.include?('does not exist') ||
85
+ message.match?(/middleware.*not found/i)
121
86
  end
122
87
 
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.
126
- #
127
- # @return [Array<String>] ordered CLI tokens that may signal a generator
128
- def self.first_cli_tokens
129
- tokens = []
88
+ def verikloak_install_generator?(command)
89
+ return false unless command.is_a?(String)
130
90
 
91
+ command.start_with?('verikloak:') && command.end_with?(':install')
92
+ end
93
+
94
+ def first_cli_tokens
95
+ tokens = []
131
96
  ARGV.each do |arg|
132
- next if arg.start_with?('-')
133
- next if arg == 'rails'
97
+ next if arg.start_with?('-') || arg == 'rails'
134
98
 
135
99
  tokens << arg
136
100
  break if tokens.size >= 2
137
101
  end
138
-
139
102
  tokens
140
103
  end
141
104
 
142
- # Detect whether the provided CLI token refers to a Verikloak install
143
- # generator (e.g. `verikloak:install`, `verikloak:pundit:install`).
144
- #
145
- # @param command [String, nil] first non-option argument from ARGV
146
- # @return [Boolean]
147
- def self.verikloak_install_generator?(command)
148
- return false unless command.is_a?(String)
105
+ private
149
106
 
150
- command.start_with?('verikloak:') && command.end_with?(':install')
107
+ def verikloak_rails_config
108
+ return unless defined?(::Verikloak::Rails)
109
+ return unless ::Verikloak::Rails.respond_to?(:config)
110
+
111
+ ::Verikloak::Rails.config
112
+ rescue StandardError
113
+ nil
151
114
  end
152
115
 
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
116
+ def sync_env_claims_key(cfg, rails_config)
117
+ return unless rails_config.respond_to?(:user_env_key)
169
118
 
170
- private
119
+ user_key = rails_config.user_env_key
120
+ return if blank?(user_key)
171
121
 
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)
122
+ current = cfg.env_claims_key
123
+ return unless current.nil? || current == Verikloak::Audience::Configuration::DEFAULT_ENV_CLAIMS_KEY
178
124
 
179
- ::Verikloak::Rails.config
180
- rescue StandardError
181
- nil
182
- end
125
+ cfg.env_claims_key = user_key
126
+ end
183
127
 
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)
128
+ def sync_required_aud(cfg, rails_config)
129
+ return unless cfg_required_aud_blank?(cfg)
130
+ return unless rails_config.respond_to?(:audience)
191
131
 
192
- user_key = rails_config.user_env_key
193
- return if blank?(user_key)
132
+ audiences = normalized_audiences(rails_config.audience)
133
+ return if audiences.empty?
194
134
 
195
- current = cfg.env_claims_key
196
- return unless current.nil? || current == Verikloak::Audience::Configuration::DEFAULT_ENV_CLAIMS_KEY
135
+ cfg.required_aud = audiences.size == 1 ? audiences.first : audiences
136
+ end
137
+
138
+ def sync_resource_client(cfg, rails_config)
139
+ return unless rails_config.respond_to?(:audience)
140
+
141
+ audiences = normalized_audiences(rails_config.audience)
142
+ return unless audiences.size == 1
197
143
 
198
- cfg.env_claims_key = user_key
144
+ current_client = cfg.resource_client
145
+ unless blank?(current_client) || current_client == Verikloak::Audience::Configuration::DEFAULT_RESOURCE_CLIENT
146
+ return
199
147
  end
200
148
 
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)
149
+ cfg.resource_client = audiences.first
150
+ end
151
+
152
+ def sync_skip_paths(cfg, rails_config)
153
+ return unless rails_config.respond_to?(:skip_paths)
154
+
155
+ paths = rails_config.skip_paths
156
+ return if paths.nil?
157
+ return unless cfg.skip_paths.nil? || cfg.skip_paths.empty?
158
+
159
+ cfg.skip_paths = Array(paths).compact
160
+ end
161
+
162
+ def cfg_required_aud_blank?(cfg) = value_blank?(cfg.required_aud)
163
+
164
+ def value_blank?(value)
165
+ return true if value.nil?
166
+ return true if value.respond_to?(:empty?) && value.empty?
167
+
168
+ value.to_s.empty?
169
+ end
170
+
171
+ alias blank? value_blank?
172
+
173
+ def normalized_audiences(source)
174
+ Array(source).compact.map(&:to_s).reject(&:empty?)
175
+ end
176
+ end
177
+
178
+ # Rails integration for verikloak-audience.
179
+ #
180
+ # This Railtie automatically inserts {Verikloak::Audience::Middleware}
181
+ # into the Rails middleware stack directly after {::Verikloak::Middleware}
182
+ # when it is present.
183
+ #
184
+ # If the core Verikloak middleware is not available, nothing is inserted.
185
+ # You may still customize the placement in your application via
186
+ # `config.middleware`.
187
+ #
188
+ # @example Configure placement and options
189
+ # # config/application.rb
190
+ # config.middleware.insert_after Verikloak::Middleware,
191
+ # Verikloak::Audience::Middleware,
192
+ # profile: :allow_account,
193
+ # required_aud: ['rails-api'],
194
+ # resource_client: 'rails-api',
195
+ # env_claims_key: 'verikloak.user',
196
+ # suggest_in_logs: true
197
+ class Railtie < ::Rails::Railtie
198
+ class << self
199
+ attr_accessor :middleware_insertion_attempted, :unconfigured_warning_emitted
200
+
201
+ include RailtieHelpers
202
+ end
203
+
204
+ @middleware_insertion_attempted = false
205
+ @unconfigured_warning_emitted = false
206
+
207
+ initializer 'verikloak_audience.middleware',
208
+ after: 'verikloak.configure',
209
+ before: :build_middleware_stack do |app|
210
+ self.class.apply_verikloak_rails_configuration
211
+ self.class.insert_middleware(app)
212
+ end
209
213
 
210
- audiences = normalized_audiences(rails_config.audience)
211
- return if audiences.empty?
214
+ initializer 'verikloak_audience.configuration' do
215
+ config.after_initialize do
216
+ next if Verikloak::Audience::Railtie.skip_configuration_validation?
217
+ next if Verikloak::Audience::Railtie.skip_unconfigured_validation?
212
218
 
213
- cfg.required_aud = audiences.size == 1 ? audiences.first : audiences
219
+ Verikloak::Audience.config.validate!
214
220
  end
221
+ end
215
222
 
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
+ COMMANDS_SKIPPING_VALIDATION = %w[generate g destroy d].freeze
223
224
 
224
- audiences = normalized_audiences(rails_config.audience)
225
- return unless audiences.size == 1
225
+ def self.insert_middleware(app)
226
+ return unless defined?(::Verikloak::Middleware)
226
227
 
227
- current_client = cfg.resource_client
228
- unless blank?(current_client) || current_client == Verikloak::Audience::Configuration::DEFAULT_RESOURCE_CLIENT
228
+ if defined?(::Verikloak::Rails) && ::Verikloak::Rails.respond_to?(:config)
229
+ discovery_url = ::Verikloak::Rails.config.discovery_url
230
+ unless discovery_url_configured?(discovery_url)
231
+ warn_core_not_configured
229
232
  return
230
233
  end
231
-
232
- cfg.resource_client = audiences.first
233
234
  end
234
235
 
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)
236
+ return if middleware_insertion_attempted
237
+
238
+ middleware_stack = app.respond_to?(:config) ? app.config.middleware : app.middleware
239
+
240
+ if middleware_stack.respond_to?(:include?) && middleware_stack.include?(::Verikloak::Audience::Middleware)
241
+ return
241
242
  end
242
243
 
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?
244
+ self.middleware_insertion_attempted = true
250
245
 
251
- value.to_s.empty?
246
+ begin
247
+ middleware_stack.insert_after ::Verikloak::Middleware, ::Verikloak::Audience::Middleware
248
+ rescue StandardError => e
249
+ raise unless middleware_not_found_error?(e)
250
+
251
+ warn_missing_core_middleware
252
252
  end
253
+ end
254
+
255
+ def self.reset_middleware_insertion_flag!
256
+ self.middleware_insertion_attempted = false
257
+ end
253
258
 
254
- alias blank? value_blank?
259
+ def self.skip_configuration_validation?
260
+ tokens = first_cli_tokens
261
+ return false if tokens.empty?
255
262
 
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?)
263
+ return true if COMMANDS_SKIPPING_VALIDATION.include?(tokens.first)
264
+
265
+ tokens.any? { |token| verikloak_install_generator?(token) }
266
+ end
267
+
268
+ def self.skip_unconfigured_validation?
269
+ audiences_unconfigured?
270
+ end
271
+
272
+ def self.audiences_unconfigured?
273
+ audiences = Verikloak::Audience.config.required_aud_list
274
+
275
+ if audiences.empty?
276
+ warn_unconfigured_once
277
+ return true
262
278
  end
279
+
280
+ false
281
+ end
282
+
283
+ def self.warn_unconfigured_once
284
+ return if unconfigured_warning_emitted
285
+
286
+ self.unconfigured_warning_emitted = true
287
+ warn_unconfigured
263
288
  end
264
289
  end
265
290
  end
@@ -4,6 +4,6 @@ module Verikloak
4
4
  module Audience
5
5
  # Current gem version.
6
6
  # @return [String]
7
- VERSION = '0.2.9'
7
+ VERSION = '0.3.1'
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.9
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - taiyaky
@@ -76,7 +76,7 @@ metadata:
76
76
  source_code_uri: https://github.com/taiyaky/verikloak-audience
77
77
  changelog_uri: https://github.com/taiyaky/verikloak-audience/blob/main/CHANGELOG.md
78
78
  bug_tracker_uri: https://github.com/taiyaky/verikloak-audience/issues
79
- documentation_uri: https://rubydoc.info/gems/verikloak-audience/0.2.9
79
+ documentation_uri: https://rubydoc.info/gems/verikloak-audience/0.3.1
80
80
  rubygems_mfa_required: 'true'
81
81
  rdoc_options: []
82
82
  require_paths: