verikloak-pundit 0.1.1 → 0.2.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: 9062ddd03a6b118971608756c82d1299dc4b509d7219d289aefbeaf381c8e49e
4
- data.tar.gz: f2c2a0e9a236c454aabd9316b04ee6e1e98860f6d2db3d8e3863fb826d92a29f
3
+ metadata.gz: 59ac975927d056e25dc3c468292b45622d0e5646c508fa0038c3df03158125e7
4
+ data.tar.gz: 15fbc3548ed4670ecbdc11ea11e39bb279a11bad273b8202209739842de7f3d8
5
5
  SHA512:
6
- metadata.gz: b2a830375513deb2e8d4b350ffb7960fb95a6c58d5b45af7c519b45cd6086ffd21230af26a89921fca57528e48275e4bf4be59d96a2c45794a11d262c7fe80c3
7
- data.tar.gz: deebae419df84ced4bb9ff0a9181847155db0adeb08f6b8cfb8319c6b2f50cd99cbd8e2dc2fa89417d15f2ab198c8e262503be630ab9ec3daf1ca58c5de7aeef
6
+ metadata.gz: a50e8038e0e72719bce60eeeebd3d6e028bd93b86ab59b25667d06857e9767e781831ad15d92687999223861221fd5d4c9db207922bf91cac7d377a5bf002ec9
7
+ data.tar.gz: cba11eb87bf8d77e6f265007b721c6ff1d672f21d5c245a6a9aa458e481b1c2ac4c52354ad9c04737bb41cf8506e644a842043f4f2f1fe15c200b3da7f3204dd
data/CHANGELOG.md CHANGED
@@ -7,6 +7,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ---
9
9
 
10
+ ## [0.2.1] - 2025-09-22
11
+
12
+ ### Added
13
+ - `Verikloak::Pundit.reset!` helper to restore default configuration, easing test teardown.
14
+ - `Verikloak::Pundit::Delegations` shared module consolidating role and permission helper methods.
15
+ - `Verikloak::Pundit::ClaimUtils` for consistent claim normalization across entry points.
16
+
17
+ ### Changed
18
+ - `UserContext` memoizes role lookups and caches mapped permissions to reduce repeated work inside policies.
19
+ - `UserContext` now normalizes inputs via `ClaimUtils` and supports symbolized permission comparison for mixed role map values.
20
+ - Configuration duplication derives from a unified `deep_dup`, ensuring hash keys are copied and nested structures remain isolated.
21
+ - README documents the new delegations module, configuration reset helper, and deprecation guidance.
22
+
23
+ ## [0.2.0] - 2025-09-21
24
+
25
+ ### Added
26
+ - Allow `permission_role_scope = :all_resources` to respect the new
27
+ `permission_resource_clients` whitelist so only approved clients contribute
28
+ to permission checks.
29
+ - Document verikloak-bff and verikloak-audience integration patterns,
30
+ including `env_claims_key` examples and role naming guidance.
31
+
32
+ ### Changed
33
+ - `UserContext` now snapshots the configuration at initialization to keep
34
+ behavior consistent even if `Verikloak::Pundit.configure` runs mid-request.
35
+ - Bump the minimum `verikloak` runtime dependency to `>= 0.1.5` to pick up
36
+ client whitelist support.
37
+
10
38
  ## [0.1.1] - 2025-09-20
11
39
 
12
40
  ### Added
data/README.md CHANGED
@@ -14,7 +14,7 @@ Pundit integration for the **Verikloak** family. This gem maps **Keycloak roles*
14
14
  ## Features
15
15
 
16
16
  - **UserContext**: lightweight wrapper around JWT claims
17
- - **Helpers**: `has_role?`, `in_group?`, `resource_role?(client, role)`
17
+ - **Delegations**: `has_role?`, `in_group?`, `resource_role?(client, role)` helpers for controllers and policies
18
18
  - **RoleMapper**: optional map from Keycloak roles → domain permissions
19
19
  - **Controller integration**: `pundit_user` provider for Rails controllers
20
20
  - **Generator**: `rails g verikloak:pundit:install` creates initializer + policy template (with `has_permission?` support for realm roles plus the configured resource scope)
@@ -89,12 +89,35 @@ Verikloak::Pundit.configure do |c|
89
89
  # (enabling this broadens permissions to every resource client;
90
90
  # review the upstream role assignments before turning it on)
91
91
  c.permission_role_scope = :default_resource
92
+ # Optional whitelist of resource clients when `permission_role_scope = :all_resources`.
93
+ # Leaving this as nil keeps the legacy "all clients" behavior, while providing
94
+ # an explicit list (e.g., %w[rails-api verikloak-bff]) limits which clients can
95
+ # contribute roles to permission checks.
96
+ c.permission_resource_clients = nil
92
97
 
93
98
  # Expose `verikloak_claims` to views via helper_method (Rails only)
94
99
  c.expose_helper_method = true
95
100
  end
96
101
  ```
97
102
 
103
+ ### Working with other Verikloak gems
104
+
105
+ - **verikloak-bff**: When your Rails application sits behind the BFF, the access
106
+ token presented to verikloak-pundit typically originates from the BFF
107
+ (e.g. via the `x-verikloak-user` header). Make sure your Rack stack stores the
108
+ decoded claims under the same `env_claims_key` configured above (the default
109
+ `"verikloak.user"` works out of the box with `verikloak-bff >= 0.3`). If the
110
+ BFF issues tokens for multiple downstream services, set
111
+ `permission_resource_clients` to the limited list of clients whose roles should
112
+ affect Rails-side authorization to avoid accidentally inheriting permissions
113
+ meant for other services.
114
+ - **verikloak-audience**: Audience services often mint resource roles with a
115
+ service-specific prefix (for example, `audience-service:editor`). Align your
116
+ `role_map` keys with that naming convention so `user.has_permission?` resolves
117
+ correctly. If Audience adds its own client entry inside `resource_access`, add
118
+ that client id to `permission_resource_clients` when you need to consume those
119
+ roles from Rails.
120
+
98
121
  ## Non-Rails / custom usage
99
122
 
100
123
  ```ruby
@@ -117,6 +140,14 @@ docker compose run --rm dev rspec
117
140
  docker compose run --rm dev rubocop -a
118
141
  ```
119
142
 
143
+ When writing specs, call `Verikloak::Pundit.reset!` in your test teardown to ensure configuration changes do not leak between examples:
144
+
145
+ ```ruby
146
+ RSpec.configure do |config|
147
+ config.after { Verikloak::Pundit.reset! }
148
+ end
149
+ ```
150
+
120
151
  An additional integration check exercises the gem together with the latest `verikloak` and `verikloak-rails` releases. This runs in CI automatically, and you can execute it locally with:
121
152
 
122
153
  ```bash
@@ -142,6 +173,10 @@ If you find a security vulnerability, please follow the instructions in [SECURIT
142
173
 
143
174
  ### Operational guidance
144
175
  - Enabling `permission_role_scope = :all_resources` pulls roles from every Keycloak client in `resource_access`. Review the granted roles carefully to ensure you are not expanding permissions beyond what the application expects.
176
+ - Combine `permission_role_scope = :all_resources` with `permission_resource_clients`
177
+ to explicitly opt-in the clients that may contribute permissions. Leaving the
178
+ whitelist blank (the default) reverts to the legacy behavior of trusting
179
+ every client in the token.
145
180
  - Leaving `expose_helper_method = true` exposes `verikloak_claims` to the Rails view layer. If the claims include personal or sensitive data, consider switching it to `false` and pass only the minimum required information through controller-provided helpers.
146
181
 
147
182
  ## License
@@ -11,7 +11,6 @@ module Verikloak
11
11
  desc 'Creates Verikloak Pundit initializer and a base ApplicationPolicy (optional).'
12
12
 
13
13
  # Skip creating application_policy.rb
14
- # @return [Boolean]
15
14
  class_option :skip_policy, type: :boolean, default: false, desc: 'Do not create application_policy.rb'
16
15
 
17
16
  # Create the initializer file under config/initializers.
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Verikloak
4
+ module Pundit
5
+ # Helpers for coercing incoming claims payloads into safe Hashes.
6
+ module ClaimUtils
7
+ module_function
8
+
9
+ # Normalize incoming claims to a Hash to guard against odd payload types.
10
+ #
11
+ # @param claims [Object]
12
+ # @return [Hash]
13
+ def normalize(claims)
14
+ return {} if claims.nil?
15
+ return claims if claims.is_a?(Hash)
16
+
17
+ if claims.respond_to?(:to_hash)
18
+ coerced = claims.to_hash
19
+ return coerced if coerced.is_a?(Hash)
20
+ end
21
+
22
+ {}
23
+ rescue StandardError
24
+ {}
25
+ end
26
+ end
27
+ end
28
+ end
@@ -16,12 +16,16 @@ module Verikloak
16
16
  # @return [Array<String,Proc>] path inside JWT claims to reach resource roles
17
17
  # @!attribute permission_role_scope
18
18
  # @return [Symbol] :default_resource or :all_resources for permission mapping scope
19
+ # @!attribute permission_resource_clients
20
+ # @return [Array<String>, nil] list of resource clients allowed when
21
+ # {#permission_role_scope} is `:all_resources`. `nil` permits every client.
19
22
  # @!attribute expose_helper_method
20
23
  # @return [Boolean] whether to register `verikloak_claims` as a Rails helper method
21
24
  class Configuration
22
25
  attr_accessor :resource_client, :role_map, :env_claims_key,
23
26
  :realm_roles_path, :resource_roles_path,
24
- :permission_role_scope, :expose_helper_method
27
+ :permission_role_scope, :permission_resource_clients,
28
+ :expose_helper_method
25
29
 
26
30
  # Build a new configuration, optionally copying values from another
27
31
  # configuration so callers can mutate a safe duplicate.
@@ -64,6 +68,7 @@ module Verikloak
64
68
  @role_map = dup_hash(@role_map).freeze
65
69
  @realm_roles_path = dup_array(@realm_roles_path).freeze
66
70
  @resource_roles_path = dup_array(@resource_roles_path).freeze
71
+ @permission_resource_clients = freeze_permission_clients(@permission_resource_clients)
67
72
  @expose_helper_method = !@expose_helper_method.nil? && @expose_helper_method
68
73
  freeze
69
74
  end
@@ -81,6 +86,7 @@ module Verikloak
81
86
  # rubocop:enable Style/SymbolProc
82
87
  # :default_resource (realm + default client), :all_resources (realm + all clients)
83
88
  @permission_role_scope = :default_resource
89
+ @permission_resource_clients = nil
84
90
  @expose_helper_method = true
85
91
  end
86
92
 
@@ -95,6 +101,7 @@ module Verikloak
95
101
  @realm_roles_path = dup_array(other.realm_roles_path)
96
102
  @resource_roles_path = dup_array(other.resource_roles_path)
97
103
  @permission_role_scope = other.permission_role_scope
104
+ @permission_resource_clients = dup_array(other.permission_resource_clients)
98
105
  @expose_helper_method = other.expose_helper_method
99
106
  end
100
107
 
@@ -108,29 +115,33 @@ module Verikloak
108
115
  dup_string(value).freeze
109
116
  end
110
117
 
111
- # Recursively duplicate a hash, cloning nested structures so the copy can
112
- # be mutated safely.
118
+ # Deep duplicate any object, handling nested structures recursively.
119
+ #
120
+ # @param value [Object] The value to duplicate
121
+ # @return [Object] A deep copy of the value
122
+ def deep_dup(value)
123
+ case value
124
+ when nil
125
+ nil
126
+ when Hash
127
+ value.each_with_object({}) do |(key, element), copy|
128
+ copy[deep_dup(key)] = deep_dup(element)
129
+ end
130
+ when Array
131
+ value.map { |element| deep_dup(element) }
132
+ when String
133
+ value.dup
134
+ else
135
+ duplicable?(value) ? value.dup : value
136
+ end
137
+ end
138
+
139
+ # Duplicate a hash using deep duplication.
113
140
  #
114
141
  # @param value [Hash, nil]
115
142
  # @return [Hash, nil]
116
143
  def dup_hash(value)
117
- return nil if value.nil?
118
-
119
- copy = value.dup
120
- copy.each do |key, element|
121
- copy[key] =
122
- case element
123
- when Hash
124
- dup_hash(element)
125
- when Array
126
- dup_array(element)
127
- when String
128
- dup_string(element)
129
- else
130
- duplicable?(element) ? element.dup : element
131
- end
132
- end
133
- copy
144
+ deep_dup(value)
134
145
  end
135
146
 
136
147
  # Duplicate a string guardingly, returning `nil` when no value is present.
@@ -138,33 +149,15 @@ module Verikloak
138
149
  # @param value [String, nil]
139
150
  # @return [String, nil]
140
151
  def dup_string(value)
141
- return nil if value.nil?
142
-
143
- value.dup
152
+ deep_dup(value)
144
153
  end
145
154
 
146
- # Recursively duplicate an array while copying nested structures.
155
+ # Duplicate an array using deep duplication.
147
156
  #
148
157
  # @param value [Array, nil]
149
158
  # @return [Array, nil]
150
159
  def dup_array(value)
151
- return nil if value.nil?
152
-
153
- copy = value.dup
154
- return copy unless copy.respond_to?(:map)
155
-
156
- copy.map do |element|
157
- case element
158
- when Hash
159
- dup_hash(element)
160
- when Array
161
- dup_array(element)
162
- when String
163
- dup_string(element)
164
- else
165
- duplicable?(element) ? element.dup : element
166
- end
167
- end
160
+ deep_dup(value)
168
161
  end
169
162
 
170
163
  # Check whether a value can be safely duplicated using `dup`.
@@ -178,6 +171,17 @@ module Verikloak
178
171
 
179
172
  value.respond_to?(:dup)
180
173
  end
174
+
175
+ # Normalize and freeze the configured permission clients list.
176
+ #
177
+ # @param value [Array<String, Symbol>, nil]
178
+ # @return [Array<String>, nil]
179
+ def freeze_permission_clients(value)
180
+ array = dup_array(value)
181
+ return nil if array.nil?
182
+
183
+ array.compact.map(&:to_s).uniq.freeze
184
+ end
181
185
  end
182
186
  end
183
187
  end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Verikloak
4
+ module Pundit
5
+ # Shared role/permission delegations to expose consistent helpers.
6
+ module Delegations
7
+ # Check whether the user has a realm role.
8
+ # @param role [String, Symbol]
9
+ # @return [Boolean]
10
+ def has_role?(role) = user.has_role?(role) # rubocop:disable Naming/PredicatePrefix
11
+
12
+ # Check whether the user belongs to a group (alias to role).
13
+ # @param group [String, Symbol]
14
+ # @return [Boolean]
15
+ def in_group?(group) = user.in_group?(group)
16
+
17
+ # Check whether the user has a role for a specific resource client.
18
+ # @param client [String, Symbol]
19
+ # @param role [String, Symbol]
20
+ # @return [Boolean]
21
+ def resource_role?(client, role) = user.resource_role?(client, role)
22
+
23
+ # Check whether the user has a mapped permission.
24
+ # @param perm [String, Symbol]
25
+ # @return [Boolean]
26
+ def has_permission?(perm) = user.has_permission?(perm) # rubocop:disable Naming/PredicatePrefix
27
+ end
28
+ end
29
+ end
@@ -3,27 +3,21 @@
3
3
  module Verikloak
4
4
  module Pundit
5
5
  # Helpers expose convenient delegations to the policy `user`.
6
+ #
7
+ # @deprecated Use {Delegations} directly instead. This module will be removed in v1.0.0.
6
8
  module Helpers
7
- # Check whether the user has a realm role.
8
- # @param role [String, Symbol]
9
- # @return [Boolean]
10
- def has_role?(role) = user.has_role?(role) # rubocop:disable Naming/PredicatePrefix
9
+ include Delegations
11
10
 
12
- # Check whether the user belongs to a group (alias to role).
13
- # @param group [String, Symbol]
14
- # @return [Boolean]
15
- def in_group?(group) = user.in_group?(group)
16
-
17
- # Check whether the user has a role for a specific resource client.
18
- # @param client [String, Symbol]
19
- # @param role [String, Symbol]
20
- # @return [Boolean]
21
- def resource_role?(client, role) = user.resource_role?(client, role)
22
-
23
- # Check whether the user has a mapped permission.
24
- # @param perm [String, Symbol]
25
- # @return [Boolean]
26
- def has_permission?(perm) = user.has_permission?(perm) # rubocop:disable Naming/PredicatePrefix
11
+ # Warn consumers when the deprecated helper module is included.
12
+ #
13
+ # @param base [Module] module or class including this helper
14
+ # @return [void]
15
+ def self.included(base)
16
+ warn '[DEPRECATED] Verikloak::Pundit::Helpers is deprecated. ' \
17
+ 'Include Verikloak::Pundit::Delegations directly instead. ' \
18
+ 'This will be removed in v1.0.0.'
19
+ super
20
+ end
27
21
  end
28
22
  end
29
23
  end
@@ -3,34 +3,25 @@
3
3
  module Verikloak
4
4
  module Pundit
5
5
  # Policy mixin to delegate common helpers to the `user` context.
6
+ #
7
+ # @deprecated Use {Delegations} directly instead. This module will be removed in v1.0.0.
6
8
  module Policy
9
+ # Warn consumers when the deprecated policy mixin is included.
10
+ #
11
+ # @param base [Module] module or class including this policy mixin
12
+ # @return [void]
7
13
  def self.included(base)
14
+ warn '[DEPRECATED] Verikloak::Pundit::Policy is deprecated. ' \
15
+ 'Include Verikloak::Pundit::Delegations directly instead. ' \
16
+ 'This will be removed in v1.0.0.'
8
17
  base.extend(ClassMethods)
18
+ super
9
19
  end
10
20
 
11
21
  # Placeholder for future class-level helpers
12
22
  module ClassMethods; end
13
23
 
14
- # Check whether the user has a realm role.
15
- # @param role [String, Symbol]
16
- # @return [Boolean]
17
- def has_role?(role) = user.has_role?(role) # rubocop:disable Naming/PredicatePrefix
18
-
19
- # Check whether the user belongs to a group (alias to role).
20
- # @param group [String, Symbol]
21
- # @return [Boolean]
22
- def in_group?(group) = user.in_group?(group)
23
-
24
- # Check whether the user has a role for a specific resource client.
25
- # @param client [String, Symbol]
26
- # @param role [String, Symbol]
27
- # @return [Boolean]
28
- def resource_role?(client, role) = user.resource_role?(client, role)
29
-
30
- # Check whether the user has a mapped permission.
31
- # @param perm [String, Symbol]
32
- # @return [Boolean]
33
- def has_permission?(perm) = user.has_permission?(perm) # rubocop:disable Naming/PredicatePrefix
24
+ include Delegations
34
25
  end
35
26
  end
36
27
  end
@@ -1,37 +1,50 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'set'
4
+
3
5
  module Verikloak
4
6
  module Pundit
5
7
  # Lightweight wrapper around Keycloak claims for Pundit policies.
6
8
  class UserContext
7
- attr_reader :claims, :resource_client
9
+ # JWT claim keys used internally
10
+ CLAIM_SUB = 'sub'
11
+ CLAIM_EMAIL = 'email'
12
+ CLAIM_PREFERRED_USERNAME = 'preferred_username'
13
+ CLAIM_RESOURCE_ACCESS = 'resource_access'
14
+ CLAIM_ROLES = 'roles'
15
+
16
+ attr_reader :claims, :resource_client, :config
8
17
 
9
18
  # Create a new user context from JWT claims.
10
19
  #
11
20
  # @param claims [Hash] JWT claims issued by Keycloak
12
21
  # @param resource_client [String] default resource client name for resource roles
13
- def initialize(claims, resource_client: Verikloak::Pundit.config.resource_client)
14
- @claims = claims || {}
15
- @resource_client = resource_client.to_s
22
+ # @param config [Verikloak::Pundit::Configuration] configuration snapshot to use
23
+ def initialize(claims, resource_client: nil, config: nil)
24
+ @config = config || Verikloak::Pundit.config
25
+ @claims = ClaimUtils.normalize(claims)
26
+ @resource_client = (resource_client || @config.resource_client).to_s
16
27
  end
17
28
 
18
29
  # Subject identifier from claims.
19
30
  # @return [String, nil]
20
31
  def sub
21
- claims['sub']
32
+ claims[CLAIM_SUB]
22
33
  end
23
34
 
24
35
  # Email or preferred username from claims.
25
36
  # @return [String, nil]
26
37
  def email
27
- claims['email'] || claims['preferred_username']
38
+ claims[CLAIM_EMAIL] || claims[CLAIM_PREFERRED_USERNAME]
28
39
  end
29
40
 
30
41
  # Realm-level roles from claims based on configuration path.
31
42
  # @return [Array<String>]
32
43
  def realm_roles
33
- path = resolve_path(Verikloak::Pundit.config.realm_roles_path)
34
- Array(claims.dig(*path))
44
+ @realm_roles ||= begin
45
+ path = resolve_path(config.realm_roles_path)
46
+ Array(claims.dig(*path)).map(&:to_s).uniq.freeze
47
+ end
35
48
  end
36
49
 
37
50
  # Resource-level roles for a given client from claims based on configuration path.
@@ -40,8 +53,10 @@ module Verikloak
40
53
  # @return [Array<String>]
41
54
  def resource_roles(client = resource_client)
42
55
  client = client.to_s
43
- path = resolve_path(Verikloak::Pundit.config.resource_roles_path, client: client)
44
- Array(claims.dig(*path))
56
+ (@resource_roles_cache ||= {})[client] ||= begin
57
+ path = resolve_path(config.resource_roles_path, client: client)
58
+ Array(claims.dig(*path)).map(&:to_s).uniq.freeze
59
+ end
45
60
  end
46
61
 
47
62
  # Check whether the user has a realm role.
@@ -49,8 +64,7 @@ module Verikloak
49
64
  # @param role [String, Symbol]
50
65
  # @return [Boolean]
51
66
  def has_role?(role) # rubocop:disable Naming/PredicatePrefix
52
- r = role.to_s
53
- realm_roles.include?(r)
67
+ realm_roles.include?(role.to_s)
54
68
  end
55
69
 
56
70
  # Alias to has_role? to align with group-based naming.
@@ -67,9 +81,7 @@ module Verikloak
67
81
  # @param role [String, Symbol]
68
82
  # @return [Boolean]
69
83
  def resource_role?(client, role)
70
- client = client.to_s
71
- r = role.to_s
72
- resource_roles(client).include?(r)
84
+ resource_roles(client).include?(role.to_s)
73
85
  end
74
86
 
75
87
  # Check whether the user has a mapped permission.
@@ -80,10 +92,7 @@ module Verikloak
80
92
  # @param perm [String, Symbol] permission to check
81
93
  # @return [Boolean]
82
94
  def has_permission?(perm) # rubocop:disable Naming/PredicatePrefix
83
- pr = perm.to_sym
84
- roles = realm_roles + resource_roles_scope
85
- mapped = roles.map { |r| RoleMapper.map(r, Verikloak::Pundit.config) }
86
- mapped.map(&:to_sym).include?(pr)
95
+ permission_set.include?(normalize_to_symbol(perm))
87
96
  end
88
97
 
89
98
  # Build a user context from Rack env using configured claims key.
@@ -91,8 +100,9 @@ module Verikloak
91
100
  # @param env [Hash] Rack environment
92
101
  # @return [UserContext]
93
102
  def self.from_env(env)
94
- claims = env[Verikloak::Pundit.config.env_claims_key]
95
- new(claims)
103
+ config = Verikloak::Pundit.config
104
+ claims = env&.fetch(config.env_claims_key, nil)
105
+ new(claims, config: config)
96
106
  end
97
107
 
98
108
  private
@@ -108,9 +118,9 @@ module Verikloak
108
118
  when Proc
109
119
  # Support lambdas that accept (config) or (config, client)
110
120
  if seg.arity >= 2
111
- seg.call(Verikloak::Pundit.config, client).to_s
121
+ seg.call(config, client).to_s
112
122
  else
113
- seg.call(Verikloak::Pundit.config).to_s
123
+ seg.call(config).to_s
114
124
  end
115
125
  else
116
126
  seg.to_s
@@ -121,7 +131,7 @@ module Verikloak
121
131
  # Resolve resource roles based on configured permission scope.
122
132
  # @return [Array<String>]
123
133
  def resource_roles_scope
124
- case Verikloak::Pundit.config.permission_role_scope&.to_sym
134
+ case config.permission_role_scope&.to_sym
125
135
  when :all_resources
126
136
  resource_roles_all_clients
127
137
  else
@@ -132,12 +142,76 @@ module Verikloak
132
142
  # Collect resource roles from all clients under resource_access.
133
143
  # @return [Array<String>]
134
144
  def resource_roles_all_clients
135
- access = claims['resource_access']
136
- return [] unless access.is_a?(Hash)
145
+ @resource_roles_all_clients ||= begin
146
+ access = claims[CLAIM_RESOURCE_ACCESS]
147
+ if access.is_a?(Hash)
148
+ roles = access.each_with_object([]) do |(client_id, entry), acc|
149
+ next unless permission_client_allowed?(client_id)
150
+
151
+ acc.concat(Array(entry[CLAIM_ROLES]))
152
+ end
153
+ roles.map(&:to_s).uniq.freeze
154
+ else
155
+ [].freeze
156
+ end
157
+ end
158
+ end
159
+
160
+ # Check whether the given client is allowed for permission scope.
161
+ #
162
+ # @param client_id [String]
163
+ # @return [Boolean]
164
+ def permission_client_allowed?(client_id)
165
+ whitelist = config.permission_resource_clients
166
+ return true if whitelist.nil?
137
167
 
138
- # Bypass configured path lambda (which targets the default client)
139
- # and gather roles from all clients explicitly.
140
- access.values.flat_map { |entry| Array(entry['roles']) }
168
+ whitelist.include?(client_id.to_s)
169
+ end
170
+
171
+ # Cached permission lookup set combining realm and configured resource scopes.
172
+ # @return [Set<Symbol>]
173
+ def permission_set
174
+ @permission_set ||= build_permission_set.freeze
175
+ end
176
+
177
+ # Build the permission set from roles and role mappings.
178
+ # @return [Set<Symbol>]
179
+ def build_permission_set
180
+ roles = realm_roles + resource_roles_scope
181
+ permissions = Set.new
182
+
183
+ roles.each do |role|
184
+ mapped_permission = RoleMapper.map(role, config)
185
+ symbol_permission = normalize_to_symbol(mapped_permission)
186
+ permissions << symbol_permission if symbol_permission
187
+ end
188
+
189
+ permissions
190
+ end
191
+
192
+ # Normalize a value to a symbol, handling various types safely.
193
+ # @param value [Object] The value to convert to a symbol
194
+ # @return [Symbol, nil] The symbol representation, or nil if not convertible
195
+ def normalize_to_symbol(value)
196
+ case value
197
+ when Symbol
198
+ value
199
+ when String
200
+ return nil if value.empty?
201
+
202
+ value.to_sym
203
+ else
204
+ if value.respond_to?(:to_sym)
205
+ value.to_sym
206
+ elsif value.respond_to?(:to_s)
207
+ text = value.to_s
208
+ return nil if text.empty?
209
+
210
+ text.to_sym
211
+ end
212
+ end
213
+ rescue StandardError
214
+ nil
141
215
  end
142
216
  end
143
217
  end
@@ -5,6 +5,6 @@ module Verikloak
5
5
  # Gem version for verikloak-pundit.
6
6
  #
7
7
  # @return [String]
8
- VERSION = '0.1.1'
8
+ VERSION = '0.2.1'
9
9
  end
10
10
  end
@@ -4,6 +4,8 @@
4
4
  require_relative 'pundit/version'
5
5
  require_relative 'pundit/configuration'
6
6
  require_relative 'pundit/role_mapper'
7
+ require_relative 'pundit/delegations'
8
+ require_relative 'pundit/claim_utils'
7
9
  require_relative 'pundit/user_context'
8
10
  require_relative 'pundit/helpers'
9
11
  require_relative 'pundit/controller'
@@ -38,6 +40,15 @@ module Verikloak
38
40
  end
39
41
  end
40
42
 
43
+ # Reset configuration to defaults. Useful for test suites.
44
+ #
45
+ # @return [void]
46
+ def reset!
47
+ config_mutex.synchronize do
48
+ @config = nil
49
+ end
50
+ end
51
+
41
52
  private
42
53
 
43
54
  # Mutex protecting configuration reads/writes to maintain thread safety.
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: verikloak-pundit
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - taiyaky
@@ -49,20 +49,20 @@ dependencies:
49
49
  requirements:
50
50
  - - ">="
51
51
  - !ruby/object:Gem::Version
52
- version: 0.1.2
52
+ version: 0.2.0
53
53
  - - "<"
54
54
  - !ruby/object:Gem::Version
55
- version: '0.2'
55
+ version: 1.0.0
56
56
  type: :runtime
57
57
  prerelease: false
58
58
  version_requirements: !ruby/object:Gem::Requirement
59
59
  requirements:
60
60
  - - ">="
61
61
  - !ruby/object:Gem::Version
62
- version: 0.1.2
62
+ version: 0.2.0
63
63
  - - "<"
64
64
  - !ruby/object:Gem::Version
65
- version: '0.2'
65
+ version: 1.0.0
66
66
  description: Maps Keycloak JWT roles to a Pundit-friendly UserContext with helpers
67
67
  and a Rails generator.
68
68
  executables: []
@@ -77,8 +77,10 @@ files:
77
77
  - lib/generators/verikloak/pundit/install/templates/initializer.rb
78
78
  - lib/verikloak-pundit.rb
79
79
  - lib/verikloak/pundit.rb
80
+ - lib/verikloak/pundit/claim_utils.rb
80
81
  - lib/verikloak/pundit/configuration.rb
81
82
  - lib/verikloak/pundit/controller.rb
83
+ - lib/verikloak/pundit/delegations.rb
82
84
  - lib/verikloak/pundit/helpers.rb
83
85
  - lib/verikloak/pundit/policy.rb
84
86
  - lib/verikloak/pundit/railtie.rb
@@ -92,7 +94,7 @@ metadata:
92
94
  source_code_uri: https://github.com/taiyaky/verikloak-pundit
93
95
  changelog_uri: https://github.com/taiyaky/verikloak-pundit/blob/main/CHANGELOG.md
94
96
  bug_tracker_uri: https://github.com/taiyaky/verikloak-pundit/issues
95
- documentation_uri: https://rubydoc.info/gems/verikloak-pundit/0.1.1
97
+ documentation_uri: https://rubydoc.info/gems/verikloak-pundit/0.2.1
96
98
  rubygems_mfa_required: 'true'
97
99
  rdoc_options: []
98
100
  require_paths: