verikloak-audience 0.3.0 → 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 +4 -4
- data/CHANGELOG.md +16 -0
- data/README.md +5 -3
- data/lib/generators/verikloak/audience/install/templates/initializer.rb.erb +20 -7
- data/lib/verikloak/audience/checker.rb +18 -1
- data/lib/verikloak/audience/configuration.rb +8 -2
- data/lib/verikloak/audience/middleware.rb +49 -5
- data/lib/verikloak/audience/railtie.rb +206 -244
- data/lib/verikloak/audience/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 400925f130b177647a3d7007cf4e43b566451e5b131968fbca2946d8a2f47d06
|
|
4
|
+
data.tar.gz: 89fa1b482fe9cdaa9278f56380cc59bba52325dd7db6b0e9aeb473e658568eb8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0d39de8ac4bfb587b968e4017e36edc03fbe49c4a7495963671aacb32a5d65fa06b39957922344b1dd3336614bb20b5ad8eeef9c7248eb6ea9c82dd772323072
|
|
7
|
+
data.tar.gz: bc01de7533c0388fc4e9f8ed35747b9136c04f166cbcfe0dbc75ff2895703aed960ed0d3845beb0dd3ecc2a3bffae786609864a59ae335672f6754a216adda4d
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,22 @@ 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
|
+
|
|
10
26
|
## [0.3.0] - 2026-01-01
|
|
11
27
|
|
|
12
28
|
### Fixed
|
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,
|
|
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`
|
|
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,11 +1,24 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
#
|
|
4
|
-
# has been loaded into the application.
|
|
3
|
+
# Verikloak Audience Configuration
|
|
5
4
|
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
|
|
10
|
-
|
|
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]
|
|
11
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
|
-
|
|
78
|
-
|
|
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
|
-
|
|
124
|
+
false
|
|
81
125
|
end
|
|
82
126
|
|
|
83
127
|
# Emit a warning for failed audience checks using request-scoped loggers
|
|
@@ -4,6 +4,177 @@ require 'rails/railtie'
|
|
|
4
4
|
|
|
5
5
|
module Verikloak
|
|
6
6
|
module Audience
|
|
7
|
+
# Warning messages for Railtie.
|
|
8
|
+
module RailtieWarnings
|
|
9
|
+
WARNING_MESSAGE = <<~MSG
|
|
10
|
+
[verikloak-audience] Skipping automatic middleware insertion because ::Verikloak::Middleware
|
|
11
|
+
is not present in the Rails middleware stack.
|
|
12
|
+
|
|
13
|
+
To enable verikloak-audience, first ensure that the core Verikloak middleware (`Verikloak::Middleware`)
|
|
14
|
+
is added to your Rails middleware stack. Once the core middleware is present, you can run
|
|
15
|
+
`rails g verikloak:audience:install` to generate the initializer for the audience middleware,
|
|
16
|
+
or manually add:
|
|
17
|
+
|
|
18
|
+
config.middleware.insert_after Verikloak::Middleware, Verikloak::Audience::Middleware
|
|
19
|
+
|
|
20
|
+
This warning will disappear once the core middleware is properly configured and the audience
|
|
21
|
+
middleware is inserted.
|
|
22
|
+
MSG
|
|
23
|
+
|
|
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)
|
|
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
|
|
54
|
+
|
|
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)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
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?)
|
|
75
|
+
|
|
76
|
+
true
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def middleware_not_found_error?(error)
|
|
80
|
+
return true if error.class.name.to_s.include?('MiddlewareNotFound')
|
|
81
|
+
|
|
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)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def verikloak_install_generator?(command)
|
|
89
|
+
return false unless command.is_a?(String)
|
|
90
|
+
|
|
91
|
+
command.start_with?('verikloak:') && command.end_with?(':install')
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def first_cli_tokens
|
|
95
|
+
tokens = []
|
|
96
|
+
ARGV.each do |arg|
|
|
97
|
+
next if arg.start_with?('-') || arg == 'rails'
|
|
98
|
+
|
|
99
|
+
tokens << arg
|
|
100
|
+
break if tokens.size >= 2
|
|
101
|
+
end
|
|
102
|
+
tokens
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
private
|
|
106
|
+
|
|
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
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def sync_env_claims_key(cfg, rails_config)
|
|
117
|
+
return unless rails_config.respond_to?(:user_env_key)
|
|
118
|
+
|
|
119
|
+
user_key = rails_config.user_env_key
|
|
120
|
+
return if blank?(user_key)
|
|
121
|
+
|
|
122
|
+
current = cfg.env_claims_key
|
|
123
|
+
return unless current.nil? || current == Verikloak::Audience::Configuration::DEFAULT_ENV_CLAIMS_KEY
|
|
124
|
+
|
|
125
|
+
cfg.env_claims_key = user_key
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def sync_required_aud(cfg, rails_config)
|
|
129
|
+
return unless cfg_required_aud_blank?(cfg)
|
|
130
|
+
return unless rails_config.respond_to?(:audience)
|
|
131
|
+
|
|
132
|
+
audiences = normalized_audiences(rails_config.audience)
|
|
133
|
+
return if audiences.empty?
|
|
134
|
+
|
|
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
|
|
143
|
+
|
|
144
|
+
current_client = cfg.resource_client
|
|
145
|
+
unless blank?(current_client) || current_client == Verikloak::Audience::Configuration::DEFAULT_RESOURCE_CLIENT
|
|
146
|
+
return
|
|
147
|
+
end
|
|
148
|
+
|
|
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
|
+
|
|
7
178
|
# Rails integration for verikloak-audience.
|
|
8
179
|
#
|
|
9
180
|
# This Railtie automatically inserts {Verikloak::Audience::Middleware}
|
|
@@ -24,305 +195,96 @@ module Verikloak
|
|
|
24
195
|
# env_claims_key: 'verikloak.user',
|
|
25
196
|
# suggest_in_logs: true
|
|
26
197
|
class Railtie < ::Rails::Railtie
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
+
|
|
31
207
|
initializer 'verikloak_audience.middleware',
|
|
32
208
|
after: 'verikloak.configure',
|
|
33
209
|
before: :build_middleware_stack do |app|
|
|
34
|
-
|
|
210
|
+
self.class.apply_verikloak_rails_configuration
|
|
35
211
|
self.class.insert_middleware(app)
|
|
36
212
|
end
|
|
37
213
|
|
|
38
214
|
initializer 'verikloak_audience.configuration' do
|
|
39
215
|
config.after_initialize do
|
|
40
|
-
self.class.apply_verikloak_rails_configuration
|
|
41
216
|
next if Verikloak::Audience::Railtie.skip_configuration_validation?
|
|
217
|
+
next if Verikloak::Audience::Railtie.skip_unconfigured_validation?
|
|
42
218
|
|
|
43
219
|
Verikloak::Audience.config.validate!
|
|
44
220
|
end
|
|
45
221
|
end
|
|
46
222
|
|
|
47
|
-
|
|
48
|
-
# duplicate insertions when both railtie and generator initializer run.
|
|
49
|
-
# In Rails 8.x+, middleware operations may be queued and `include?` may
|
|
50
|
-
# not reflect pending insertions, so we use this flag as an additional
|
|
51
|
-
# safeguard.
|
|
52
|
-
@middleware_insertion_attempted = false
|
|
53
|
-
|
|
54
|
-
class << self
|
|
55
|
-
attr_accessor :middleware_insertion_attempted
|
|
56
|
-
end
|
|
223
|
+
COMMANDS_SKIPPING_VALIDATION = %w[generate g destroy d].freeze
|
|
57
224
|
|
|
58
|
-
# Performs the insertion into the middleware stack when the core
|
|
59
|
-
# Verikloak middleware is available and already present. Extracted for
|
|
60
|
-
# testability without requiring a full Rails boot process.
|
|
61
|
-
#
|
|
62
|
-
# Insert the audience middleware after the base Verikloak middleware when
|
|
63
|
-
# both are available on the stack.
|
|
64
|
-
#
|
|
65
|
-
# In Rails 8.x+, middleware stack operations may be queued rather than
|
|
66
|
-
# immediately applied, so `include?` checks may return false even when
|
|
67
|
-
# the middleware will be inserted. We use `insert_after` with exception
|
|
68
|
-
# handling to gracefully handle this case.
|
|
69
|
-
#
|
|
70
|
-
# @param app [#middleware] An object exposing a Rack middleware stack via `#middleware`.
|
|
71
|
-
# @return [void]
|
|
72
225
|
def self.insert_middleware(app)
|
|
73
226
|
return unless defined?(::Verikloak::Middleware)
|
|
74
227
|
|
|
75
|
-
|
|
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
|
|
232
|
+
return
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
76
236
|
return if middleware_insertion_attempted
|
|
77
237
|
|
|
78
|
-
# Use app.config.middleware for queued operations in Rails 8.x+
|
|
79
|
-
# This ensures the insert_after operation is queued and applied during
|
|
80
|
-
# build_middleware_stack, maintaining proper ordering with verikloak-rails
|
|
81
238
|
middleware_stack = app.respond_to?(:config) ? app.config.middleware : app.middleware
|
|
82
239
|
|
|
83
|
-
|
|
84
|
-
if middleware_stack.respond_to?(:include?) &&
|
|
85
|
-
middleware_stack.include?(::Verikloak::Audience::Middleware)
|
|
240
|
+
if middleware_stack.respond_to?(:include?) && middleware_stack.include?(::Verikloak::Audience::Middleware)
|
|
86
241
|
return
|
|
87
242
|
end
|
|
88
243
|
|
|
89
|
-
# Mark as attempted before insertion to prevent concurrent/subsequent calls
|
|
90
244
|
self.middleware_insertion_attempted = true
|
|
91
245
|
|
|
92
|
-
# Attempt to insert after the core Verikloak middleware.
|
|
93
|
-
# In Rails 8.x+, the middleware may be queued but not yet visible via include?,
|
|
94
|
-
# so we try the insertion and handle any exceptions gracefully.
|
|
95
246
|
begin
|
|
96
247
|
middleware_stack.insert_after ::Verikloak::Middleware, ::Verikloak::Audience::Middleware
|
|
97
248
|
rescue StandardError => e
|
|
98
|
-
# Handle middleware not found errors (varies by Rails version):
|
|
99
|
-
# - Rails 8+: RuntimeError with "No such middleware" message
|
|
100
|
-
# - Earlier: ActionDispatch::MiddlewareStack::MiddlewareNotFound
|
|
101
249
|
raise unless middleware_not_found_error?(e)
|
|
102
250
|
|
|
103
251
|
warn_missing_core_middleware
|
|
104
252
|
end
|
|
105
253
|
end
|
|
106
254
|
|
|
107
|
-
# Determines if the given exception indicates a middleware not found error.
|
|
108
|
-
# This handles variations across Rails versions:
|
|
109
|
-
# - Rails 8+: RuntimeError with "No such middleware" message
|
|
110
|
-
# - Rails 7 and earlier: ActionDispatch::MiddlewareStack::MiddlewareNotFound
|
|
111
|
-
#
|
|
112
|
-
# @param error [StandardError] the exception to check
|
|
113
|
-
# @return [Boolean] true if the error indicates missing middleware
|
|
114
|
-
def self.middleware_not_found_error?(error)
|
|
115
|
-
# Check exception class name (works for Rails 7's MiddlewareNotFound)
|
|
116
|
-
return true if error.class.name.to_s.include?('MiddlewareNotFound')
|
|
117
|
-
|
|
118
|
-
# Check message patterns for Rails 8+ RuntimeError
|
|
119
|
-
message = error.message.to_s
|
|
120
|
-
message.include?('No such middleware') ||
|
|
121
|
-
message.include?('does not exist') ||
|
|
122
|
-
message.match?(/middleware.*not found/i)
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
# Resets the insertion flag. Primarily used for testing to allow
|
|
126
|
-
# multiple insertion attempts within the same process.
|
|
127
|
-
#
|
|
128
|
-
# @return [void]
|
|
129
255
|
def self.reset_middleware_insertion_flag!
|
|
130
256
|
self.middleware_insertion_attempted = false
|
|
131
257
|
end
|
|
132
258
|
|
|
133
|
-
WARNING_MESSAGE = <<~MSG
|
|
134
|
-
[verikloak-audience] Skipping automatic middleware insertion because ::Verikloak::Middleware
|
|
135
|
-
is not present in the Rails middleware stack.
|
|
136
|
-
|
|
137
|
-
To enable verikloak-audience, first ensure that the core Verikloak middleware (`Verikloak::Middleware`)
|
|
138
|
-
is added to your Rails middleware stack. Once the core middleware is present, you can run
|
|
139
|
-
`rails g verikloak:audience:install` to generate the initializer for the audience middleware,
|
|
140
|
-
or manually add:
|
|
141
|
-
|
|
142
|
-
config.middleware.insert_after Verikloak::Middleware, Verikloak::Audience::Middleware
|
|
143
|
-
|
|
144
|
-
This warning will disappear once the core middleware is properly configured and the audience
|
|
145
|
-
middleware is inserted.
|
|
146
|
-
MSG
|
|
147
|
-
|
|
148
|
-
# Logs a warning message when the core Verikloak middleware is missing
|
|
149
|
-
# from the Rails middleware stack. Uses the Rails logger if available,
|
|
150
|
-
# otherwise falls back to Kernel.warn for output.
|
|
151
|
-
#
|
|
152
|
-
# This method is called when automatic middleware insertion is skipped
|
|
153
|
-
# due to the absence of the required core middleware.
|
|
154
|
-
#
|
|
155
|
-
# @return [void]
|
|
156
|
-
def self.warn_missing_core_middleware
|
|
157
|
-
logger = (::Rails.logger if defined?(::Rails) && ::Rails.respond_to?(:logger))
|
|
158
|
-
|
|
159
|
-
if logger
|
|
160
|
-
logger.warn(WARNING_MESSAGE)
|
|
161
|
-
else
|
|
162
|
-
Kernel.warn(WARNING_MESSAGE)
|
|
163
|
-
end
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
# Rails short commands (`g`, `d`) are stripped from ARGV fairly early in
|
|
167
|
-
# the boot process. Treat `verikloak:*:install` generators as safe so they
|
|
168
|
-
# can run before configuration files exist.
|
|
169
|
-
COMMANDS_SKIPPING_VALIDATION = %w[generate g destroy d].freeze
|
|
170
|
-
|
|
171
|
-
# Detect whether Rails is currently executing a generator-style command.
|
|
172
|
-
# Generators boot the application before configuration exists, so we
|
|
173
|
-
# temporarily skip validation to let the install task complete.
|
|
174
|
-
#
|
|
175
|
-
# @return [Boolean]
|
|
176
259
|
def self.skip_configuration_validation?
|
|
177
260
|
tokens = first_cli_tokens
|
|
178
261
|
return false if tokens.empty?
|
|
179
262
|
|
|
180
|
-
|
|
181
|
-
return true if COMMANDS_SKIPPING_VALIDATION.include?(command)
|
|
263
|
+
return true if COMMANDS_SKIPPING_VALIDATION.include?(tokens.first)
|
|
182
264
|
|
|
183
265
|
tokens.any? { |token| verikloak_install_generator?(token) }
|
|
184
266
|
end
|
|
185
267
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
# relevant for generator detection, so we keep the return list short.
|
|
189
|
-
#
|
|
190
|
-
# @return [Array<String>] ordered CLI tokens that may signal a generator
|
|
191
|
-
def self.first_cli_tokens
|
|
192
|
-
tokens = []
|
|
193
|
-
|
|
194
|
-
ARGV.each do |arg|
|
|
195
|
-
next if arg.start_with?('-')
|
|
196
|
-
next if arg == 'rails'
|
|
197
|
-
|
|
198
|
-
tokens << arg
|
|
199
|
-
break if tokens.size >= 2
|
|
200
|
-
end
|
|
201
|
-
|
|
202
|
-
tokens
|
|
203
|
-
end
|
|
204
|
-
|
|
205
|
-
# Detect whether the provided CLI token refers to a Verikloak install
|
|
206
|
-
# generator (e.g. `verikloak:install`, `verikloak:pundit:install`).
|
|
207
|
-
#
|
|
208
|
-
# @param command [String, nil] first non-option argument from ARGV
|
|
209
|
-
# @return [Boolean]
|
|
210
|
-
def self.verikloak_install_generator?(command)
|
|
211
|
-
return false unless command.is_a?(String)
|
|
212
|
-
|
|
213
|
-
command.start_with?('verikloak:') && command.end_with?(':install')
|
|
268
|
+
def self.skip_unconfigured_validation?
|
|
269
|
+
audiences_unconfigured?
|
|
214
270
|
end
|
|
215
271
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
# Aligns env_claims_key, required_aud, and resource_client defaults so
|
|
219
|
-
# that both gems operate on the same Rack env payload and audience list.
|
|
220
|
-
#
|
|
221
|
-
# @return [void]
|
|
222
|
-
def apply_verikloak_rails_configuration
|
|
223
|
-
rails_config = verikloak_rails_config
|
|
224
|
-
return unless rails_config
|
|
225
|
-
|
|
226
|
-
Verikloak::Audience.configure do |cfg|
|
|
227
|
-
sync_env_claims_key(cfg, rails_config)
|
|
228
|
-
sync_required_aud(cfg, rails_config)
|
|
229
|
-
sync_resource_client(cfg, rails_config)
|
|
230
|
-
end
|
|
231
|
-
end
|
|
232
|
-
|
|
233
|
-
private
|
|
234
|
-
|
|
235
|
-
# Resolve the verikloak-rails configuration object if the gem is loaded.
|
|
236
|
-
#
|
|
237
|
-
# @return [Verikloak::Rails::Configuration, nil]
|
|
238
|
-
def verikloak_rails_config
|
|
239
|
-
return unless defined?(::Verikloak::Rails)
|
|
240
|
-
return unless ::Verikloak::Rails.respond_to?(:config)
|
|
272
|
+
def self.audiences_unconfigured?
|
|
273
|
+
audiences = Verikloak::Audience.config.required_aud_list
|
|
241
274
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
275
|
+
if audiences.empty?
|
|
276
|
+
warn_unconfigured_once
|
|
277
|
+
return true
|
|
245
278
|
end
|
|
246
279
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
# @param cfg [Verikloak::Audience::Configuration]
|
|
250
|
-
# @param rails_config [Verikloak::Rails::Configuration]
|
|
251
|
-
# @return [void]
|
|
252
|
-
def sync_env_claims_key(cfg, rails_config)
|
|
253
|
-
return unless rails_config.respond_to?(:user_env_key)
|
|
254
|
-
|
|
255
|
-
user_key = rails_config.user_env_key
|
|
256
|
-
return if blank?(user_key)
|
|
257
|
-
|
|
258
|
-
current = cfg.env_claims_key
|
|
259
|
-
return unless current.nil? || current == Verikloak::Audience::Configuration::DEFAULT_ENV_CLAIMS_KEY
|
|
260
|
-
|
|
261
|
-
cfg.env_claims_key = user_key
|
|
262
|
-
end
|
|
263
|
-
|
|
264
|
-
# Populate required audiences from the verikloak-rails configuration when absent.
|
|
265
|
-
#
|
|
266
|
-
# @param cfg [Verikloak::Audience::Configuration]
|
|
267
|
-
# @param rails_config [Verikloak::Rails::Configuration]
|
|
268
|
-
# @return [void]
|
|
269
|
-
def sync_required_aud(cfg, rails_config)
|
|
270
|
-
return unless cfg_required_aud_blank?(cfg)
|
|
271
|
-
return unless rails_config.respond_to?(:audience)
|
|
272
|
-
|
|
273
|
-
audiences = normalized_audiences(rails_config.audience)
|
|
274
|
-
return if audiences.empty?
|
|
275
|
-
|
|
276
|
-
cfg.required_aud = audiences.size == 1 ? audiences.first : audiences
|
|
277
|
-
end
|
|
278
|
-
|
|
279
|
-
# Infer the resource client based on the configured audience when possible.
|
|
280
|
-
#
|
|
281
|
-
# @param cfg [Verikloak::Audience::Configuration]
|
|
282
|
-
# @param rails_config [Verikloak::Rails::Configuration]
|
|
283
|
-
# @return [void]
|
|
284
|
-
def sync_resource_client(cfg, rails_config)
|
|
285
|
-
return unless rails_config.respond_to?(:audience)
|
|
286
|
-
|
|
287
|
-
audiences = normalized_audiences(rails_config.audience)
|
|
288
|
-
return unless audiences.size == 1
|
|
289
|
-
|
|
290
|
-
current_client = cfg.resource_client
|
|
291
|
-
unless blank?(current_client) || current_client == Verikloak::Audience::Configuration::DEFAULT_RESOURCE_CLIENT
|
|
292
|
-
return
|
|
293
|
-
end
|
|
294
|
-
|
|
295
|
-
cfg.resource_client = audiences.first
|
|
296
|
-
end
|
|
297
|
-
|
|
298
|
-
# Determine whether the audience configuration is effectively empty.
|
|
299
|
-
#
|
|
300
|
-
# @param cfg [Verikloak::Audience::Configuration]
|
|
301
|
-
# @return [Boolean]
|
|
302
|
-
def cfg_required_aud_blank?(cfg)
|
|
303
|
-
value_blank?(cfg.required_aud)
|
|
304
|
-
end
|
|
305
|
-
|
|
306
|
-
# Generic blank? helper that tolerates nil, empty, or blank-ish values.
|
|
307
|
-
#
|
|
308
|
-
# @param value [Object]
|
|
309
|
-
# @return [Boolean]
|
|
310
|
-
def value_blank?(value)
|
|
311
|
-
return true if value.nil?
|
|
312
|
-
return true if value.respond_to?(:empty?) && value.empty?
|
|
313
|
-
|
|
314
|
-
value.to_s.empty?
|
|
315
|
-
end
|
|
280
|
+
false
|
|
281
|
+
end
|
|
316
282
|
|
|
317
|
-
|
|
283
|
+
def self.warn_unconfigured_once
|
|
284
|
+
return if unconfigured_warning_emitted
|
|
318
285
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
# @param source [Object]
|
|
322
|
-
# @return [Array<String>]
|
|
323
|
-
def normalized_audiences(source)
|
|
324
|
-
Array(source).compact.map(&:to_s).reject(&:empty?)
|
|
325
|
-
end
|
|
286
|
+
self.unconfigured_warning_emitted = true
|
|
287
|
+
warn_unconfigured
|
|
326
288
|
end
|
|
327
289
|
end
|
|
328
290
|
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.3.
|
|
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.3.
|
|
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:
|