verikloak-pundit 0.4.0 → 1.1.0

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: 89c932324252efb900c44575ab54dffb96e1d6300c958d267217e612952664bc
4
- data.tar.gz: aa4409c08ff3af4e48960980c684988975054a6d5a574e209a64ad4889a58f61
3
+ metadata.gz: bf2182c6f85bdb89c0d109fcabc9fdcf0141b9b9dfef32dd4d953694e9f3d239
4
+ data.tar.gz: d93f6833adda5312d3485c9f9aa310777c35341207f0bda7fd54c1667f7fa18a
5
5
  SHA512:
6
- metadata.gz: 34e87036e92f4c0af62a2238298bbafc0ed61337c44b92a4c0533dc142ebd4983ca8d2040caa4a3566f6ca9233cf14b26ebe19f3302e5e15805111bf222df0ee
7
- data.tar.gz: 532af9d0e912a5fc23df9859ccc42cc5853d4da78a3a6349f45f8a200979588273306186f5a3532cbed50f3f8b1706ab3ebe7db38c3d508eeab6e704894dd9e1
6
+ metadata.gz: ba7e80788a77d28ba624e9c5b41da2427c9d42eecb44de665d6c88a6f21e1b42f7fe0b387964219182d35ba336cac664066b4be983068aa78cfb0f6855d0badb
7
+ data.tar.gz: e876d7ddf9c83469c6f9eb8423786392d4f45abd687c37d7e835d41d013aa2ee133a6d1840de8f171562908065d579a85f6dce7bb1eb9f9cb49648eba35e5c18
data/CHANGELOG.md CHANGED
@@ -7,6 +7,54 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ---
9
9
 
10
+ ## [1.1.0] - 2026-07-03
11
+
12
+ ### Added
13
+ - **`strict_permissions` configuration**: when enabled, `has_permission?` only grants permissions defined as `role_map` values; unmapped role names no longer act as implicit permissions. Recommended together with `permission_role_scope = :all_resources`
14
+
15
+ ### Changed
16
+ - Minimum `verikloak` dependency raised to `~> 1.1`, aligned with the verikloak 1.1.0 release (compatibility with verikloak-rails 1.2.0 verified: default user env key and the `verikloak.configure` initializer ordering are unchanged)
17
+ - **Helper exposure is evaluated at call time**: `verikloak_claims` is exposed to views through a helper module that consults `expose_helper_method` on each call, so the setting takes effect regardless of initializer ordering. When disabled, views receive `nil` instead of raising `NoMethodError`
18
+ - **Reentrant configuration lock**: the internal config `Mutex` was replaced with a `Monitor`, so reading `Verikloak::Pundit.config` inside a `configure` block no longer raises `ThreadError`
19
+ - `role_map` values are validated at assignment: Symbols, Strings, and `nil` are accepted; anything else raises `ArgumentError` so misconfiguration surfaces at boot (v1.0.0 silently coerced such values via `to_s`)
20
+ - An explicit `nil` value in `role_map` now revokes the role's implicit permission even when `strict_permissions` is off (previously a `nil` mapping was treated as unmapped and the bare role name fell through as a permission)
21
+ - Boolean configuration flags (`strict_permissions`, `expose_helper_method`) are coerced to strict `true`/`false` on finalize
22
+
23
+ ### Deprecated
24
+ - `RoleMapper.map`: use `RoleMapper.permission_for` instead. `map` now delegates to it, so strict mode and explicit `nil` revocations are honored consistently by every caller
25
+
26
+ ### Security
27
+ - Updated locked development/CI dependencies to resolve all known advisories (25 Dependabot alerts): rack 3.2.6, activesupport 8.1.3, concurrent-ruby 1.3.7, faraday 2.14.3, jwt 3.2.0, json 2.20.0. The `pundit ~> 2.3` runtime constraint is unchanged (the `verikloak` constraint bump is listed under Changed)
28
+
29
+ ### Removed
30
+ - **Unused `rack` runtime dependency**: the gem only reads the Rack env Hash and uses no Rack APIs (`rack-test` was likewise removed from development dependencies)
31
+
32
+ ### Fixed
33
+ - **Per-client `resource_role?` with the default configuration**: the default `resource_roles_path` lambda ignored the requested client, so `resource_role?(client, role)` always inspected the default resource client's roles — granting or denying based on the wrong client. The default path lambda now receives `(config, client)` and resolves the explicitly requested client
34
+ - **Path lambdas with optional or variadic parameters**: custom `resource_roles_path` lambdas such as `->(cfg, client = nil) { ... }` (negative arity) never received the requested client and silently fell back to the default resource client. Zero-argument procs are now called as thunks, single-argument procs receive `(config)`, and every other signature receives `(config, client)`
35
+ - ERRORS.md no longer claims configuration is not thread-safe (stale since the v1.0.0 thread-safety fix)
36
+
37
+ ### Internal
38
+ - CI: added a Ruby compatibility matrix (3.1 / 3.2 / 3.3, mirroring the verikloak repo) that runs the suite against `gemfiles/compat.gemfile` with a fresh per-Ruby resolution; the docker-based job continues to cover the development Ruby (3.4)
39
+ - Refactoring: unified deep-copy helpers in `Configuration` (removed the `dup_hash`/`dup_string`/`dup_array` wrappers and the redundant `dup` override), simplified `RoleMapper.map` and `UserContext#normalize_to_symbol`
40
+ - Test coverage: added specs for `Delegations`, `ClaimUtils`, `Railtie.sync_with_verikloak_rails`, `UserContext.from_env`, the `KEYCLOAK_RESOURCE_CLIENT` ENV fallback, and strict permission mode. Spec files are now linted by RuboCop, and Rails-stubbing specs share a scoped `stub_require` helper instead of mutating `$LOADED_FEATURES`
41
+
42
+ ---
43
+
44
+ ## [1.0.0] - 2026-02-15
45
+
46
+ ### Fixed
47
+ - **Thread-safety**: `config_mutex` is now eagerly initialized at load time. The previous `||= Mutex.new` pattern was racy — two threads could create separate Mutex instances, defeating mutual exclusion
48
+
49
+ ### Removed
50
+ - **BREAKING**: `Verikloak::Pundit::Policy` and `Verikloak::Pundit::Helpers` deprecated modules have been removed as announced in their deprecation notices. Use `Verikloak::Pundit::Delegations` directly instead
51
+
52
+ ### Changed
53
+ - **BREAKING**: Minimum `verikloak` dependency raised to `~> 1.0`
54
+ - **v1.0.0 stable release**: Public API is now considered stable under Semantic Versioning
55
+
56
+ ---
57
+
10
58
  ## [0.4.0] - 2026-02-15
11
59
 
12
60
  ### Changed
data/README.md CHANGED
@@ -90,6 +90,42 @@ Verikloak::Pundit.configure do |c|
90
90
  end
91
91
  ```
92
92
 
93
+ The synchronization runs after your `config/initializers` have been applied
94
+ (verikloak-rails registers its `verikloak.configure` initializer with
95
+ `after: :load_config_initializers`), so a `user_env_key` customized in your
96
+ verikloak-rails initializer is picked up automatically.
97
+
98
+ ### Strict permission mapping
99
+
100
+ By default, `has_permission?` treats unmapped role names as permissions
101
+ themselves: a bare Keycloak role `admin` satisfies `has_permission?(:admin)`
102
+ even without a `role_map` entry. To only grant permissions that appear as
103
+ `role_map` values, enable strict mode:
104
+
105
+ ```ruby
106
+ Verikloak::Pundit.configure do |c|
107
+ c.role_map = { admin: :manage_all }
108
+ c.strict_permissions = true
109
+ end
110
+ ```
111
+
112
+ With strict mode on, roles without a `role_map` entry contribute no
113
+ permissions. This is recommended when combining
114
+ `permission_role_scope = :all_resources` with tokens shared across services,
115
+ so role names minted for other clients cannot accidentally satisfy
116
+ permission checks.
117
+
118
+ `role_map` values must be Symbols, Strings, or `nil` — any other type raises
119
+ an `ArgumentError` when assigned. Mapping a role to `nil` explicitly revokes
120
+ its implicit permission even when strict mode is off:
121
+
122
+ ```ruby
123
+ Verikloak::Pundit.configure do |c|
124
+ # `legacy_role` in a token no longer satisfies has_permission?(:legacy_role)
125
+ c.role_map = { legacy_role: nil }
126
+ end
127
+ ```
128
+
93
129
  ### Working with other Verikloak gems
94
130
 
95
131
  - **verikloak-bff**: When your Rails application sits behind the BFF, the access
@@ -318,7 +354,8 @@ If you find a security vulnerability, please follow the instructions in [SECURIT
318
354
 
319
355
  - 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.
320
356
  - Combine `permission_role_scope = :all_resources` with `permission_resource_clients` to explicitly opt-in the clients that may contribute permissions. Leaving the whitelist blank (the default) reverts to the legacy behavior of trusting every client in the token.
321
- - 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.
357
+ - Enable `strict_permissions = true` so `has_permission?` only grants permissions defined as `role_map` values. Without it, any role name in the token doubles as a permission, which is convenient but broadens what a token can satisfy.
358
+ - 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. The setting is consulted every time the view helper is called, so it can be changed in any initializer regardless of load order; when disabled, the view helper returns `nil`.
322
359
 
323
360
  ## License
324
361
  This project is licensed under the [MIT License](LICENSE).
@@ -13,16 +13,23 @@ Verikloak::Pundit.configure do |c|
13
13
  # Resource client (optional - falls back to ENV['KEYCLOAK_RESOURCE_CLIENT'] or 'rails-api')
14
14
  # c.resource_client = ENV.fetch('KEYCLOAK_RESOURCE_CLIENT', 'rails-api')
15
15
 
16
- # Role to permission mapping (optional)
16
+ # Role to permission mapping (optional).
17
+ # Values must be Symbols or Strings; map a role to nil to explicitly
18
+ # revoke its implicit permission.
17
19
  # c.role_map = {
18
20
  # admin: :manage_all,
19
21
  # editor: :write_notes,
20
22
  # reader: :read_notes
21
23
  # }
22
24
 
25
+ # Only grant permissions defined as role_map values (optional, default: false).
26
+ # When true, bare role names no longer act as implicit permissions in
27
+ # has_permission?. Recommended with permission_role_scope = :all_resources.
28
+ # c.strict_permissions = true
29
+
23
30
  # Uncomment to customize JWT claims path and scope (usually not needed):
24
31
  # c.env_claims_key = 'verikloak.user'
25
32
  # c.realm_roles_path = %w[realm_access roles]
26
- # c.resource_roles_path = ['resource_access', ->(cfg) { cfg.resource_client }, 'roles']
33
+ # c.resource_roles_path = ['resource_access', ->(cfg, client) { client || cfg.resource_client }, 'roles']
27
34
  # c.permission_role_scope = :default_resource # or :all_resources
28
35
  end
@@ -7,7 +7,9 @@ module Verikloak
7
7
  # @!attribute resource_client
8
8
  # @return [String] default Keycloak resource client used for resource roles
9
9
  # @!attribute role_map
10
- # @return [Hash{Symbol=>Symbol,String}] mapping from roles to permissions
10
+ # @return [Hash{Symbol=>Symbol,String,nil}] mapping from roles to
11
+ # permissions; a nil value explicitly revokes the role's implicit
12
+ # permission
11
13
  # @!attribute env_claims_key
12
14
  # @return [String] Rack env key where claims are stored (when using verikloak/verikloak-rails)
13
15
  # @!attribute realm_roles_path
@@ -19,21 +21,30 @@ module Verikloak
19
21
  # @!attribute permission_resource_clients
20
22
  # @return [Array<String>, nil] list of resource clients allowed when
21
23
  # {#permission_role_scope} is `:all_resources`. `nil` permits every client.
24
+ # @!attribute strict_permissions
25
+ # @return [Boolean] when true, `has_permission?` only grants permissions that
26
+ # appear as values in {#role_map}; unmapped role names no longer act as
27
+ # implicit permissions
22
28
  # @!attribute expose_helper_method
23
- # @return [Boolean] whether to register `verikloak_claims` as a Rails helper method
29
+ # @return [Boolean] whether `verikloak_claims` is exposed to Rails views
24
30
  class Configuration
25
31
  attr_accessor :env_claims_key,
26
32
  :realm_roles_path, :resource_roles_path,
27
33
  :permission_role_scope, :permission_resource_clients,
28
- :expose_helper_method
34
+ :strict_permissions, :expose_helper_method
29
35
 
30
36
  attr_reader :role_map
31
37
  attr_writer :resource_client
32
38
 
33
39
  # Set the role map, normalizing keys to symbols for consistent lookup.
40
+ # Values must be Symbols, Strings, or nil (an explicit nil revokes the
41
+ # role's implicit permission); anything else raises at assignment time
42
+ # so misconfiguration surfaces at boot instead of as silently missing
43
+ # permissions.
34
44
  #
35
45
  # @param value [Hash]
36
46
  # @return [void]
47
+ # @raise [ArgumentError] when a value is neither Symbol, String, nor nil
37
48
  def role_map=(value)
38
49
  @role_map = normalize_role_map(value)
39
50
  end
@@ -45,34 +56,37 @@ module Verikloak
45
56
  @resource_client || ENV.fetch('KEYCLOAK_RESOURCE_CLIENT', 'rails-api')
46
57
  end
47
58
 
48
- # Build a new configuration, optionally copying values from another
49
- # configuration so callers can mutate a safe duplicate.
59
+ # Build a new configuration populated with default values, optionally
60
+ # copying values from another configuration so callers can mutate a
61
+ # safe duplicate. The `copy_from` parameter is kept for backward
62
+ # compatibility with the v1.0.0 signature; `dup` is the idiomatic way
63
+ # to copy.
50
64
  #
51
65
  # @param copy_from [Configuration, nil]
52
66
  def initialize(copy_from = nil)
53
- if copy_from
54
- initialize_from(copy_from)
55
- else
56
- initialize_defaults
57
- end
58
- end
59
-
60
- # Create a deep-ish copy that can be safely mutated without affecting the
61
- # source configuration. `dup` is overridden so the object returned from
62
- # `Verikloak::Pundit.config.dup` behaves as expected.
63
- #
64
- # @return [Configuration]
65
- def dup
66
- self.class.new(self)
67
+ @resource_client = nil # Falls back to ENV['KEYCLOAK_RESOURCE_CLIENT'] or 'rails-api'
68
+ @role_map = {} # e.g., { admin: :manage_all }
69
+ @env_claims_key = 'verikloak.user'
70
+ @realm_roles_path = %w[realm_access roles]
71
+ # The lambda receives (config, client) so that an explicitly requested
72
+ # client (e.g. resource_role?(:other, :role)) resolves to that client's
73
+ # entry instead of always falling back to the default resource client.
74
+ @resource_roles_path = ['resource_access', ->(cfg, client) { client || cfg.resource_client }, 'roles']
75
+ # :default_resource (realm + default client), :all_resources (realm + all clients)
76
+ @permission_role_scope = :default_resource
77
+ @permission_resource_clients = nil
78
+ @strict_permissions = false
79
+ @expose_helper_method = true
80
+ copy_state_from(copy_from) if copy_from
67
81
  end
68
82
 
69
- # Duplicate the configuration via Ruby's `dup`, ensuring the new instance
70
- # receives freshly-copied nested state.
83
+ # Duplicate the configuration via Ruby's `dup`/`clone`, ensuring the new
84
+ # instance receives freshly-copied (and unfrozen) nested state.
71
85
  #
72
86
  # @param other [Configuration]
73
87
  def initialize_copy(other)
74
88
  super
75
- initialize_from(other)
89
+ copy_state_from(other)
76
90
  end
77
91
 
78
92
  # Freeze the configuration and its nested structures to prevent runtime
@@ -83,55 +97,53 @@ module Verikloak
83
97
  def finalize!
84
98
  @resource_client = freeze_string(@resource_client)
85
99
  @env_claims_key = freeze_string(@env_claims_key)
86
- @role_map = dup_hash(@role_map).freeze
87
- @realm_roles_path = dup_array(@realm_roles_path).freeze
88
- @resource_roles_path = dup_array(@resource_roles_path).freeze
100
+ @role_map = deep_dup(@role_map).freeze
101
+ @realm_roles_path = deep_dup(@realm_roles_path).freeze
102
+ @resource_roles_path = deep_dup(@resource_roles_path).freeze
89
103
  @permission_resource_clients = freeze_permission_clients(@permission_resource_clients)
90
- @expose_helper_method = !@expose_helper_method.nil? && @expose_helper_method
104
+ # Coerce flags to strict booleans based on truthiness
105
+ @strict_permissions = @strict_permissions ? true : false
106
+ @expose_helper_method = @expose_helper_method ? true : false
91
107
  freeze
92
108
  end
93
109
 
94
110
  private
95
111
 
96
- # Populate default values that mirror the gem's out-of-the-box behavior.
97
- def initialize_defaults
98
- @resource_client = nil # Falls back to ENV['KEYCLOAK_RESOURCE_CLIENT'] or 'rails-api'
99
- @role_map = {} # e.g., { admin: :manage_all }
100
- @env_claims_key = 'verikloak.user'
101
- @realm_roles_path = %w[realm_access roles]
102
- # rubocop:disable Style/SymbolProc -- we need a Proc object here, not block pass
103
- @resource_roles_path = ['resource_access', ->(cfg) { cfg.resource_client }, 'roles']
104
- # rubocop:enable Style/SymbolProc
105
- # :default_resource (realm + default client), :all_resources (realm + all clients)
106
- @permission_role_scope = :default_resource
107
- @permission_resource_clients = nil
108
- @expose_helper_method = true
112
+ # Copy all configuration state from another instance, deep-duplicating
113
+ # nested structures (and reading the raw @resource_client rather than
114
+ # the getter, to preserve its ENV fallback behavior) so the copy can be
115
+ # mutated without affecting the source.
116
+ #
117
+ # @param other [Configuration]
118
+ # @return [void]
119
+ def copy_state_from(other)
120
+ @resource_client = deep_dup(other.instance_variable_get(:@resource_client))
121
+ @role_map = deep_dup(other.role_map)
122
+ @env_claims_key = deep_dup(other.env_claims_key)
123
+ @realm_roles_path = deep_dup(other.realm_roles_path)
124
+ @resource_roles_path = deep_dup(other.resource_roles_path)
125
+ @permission_role_scope = other.permission_role_scope
126
+ @permission_resource_clients = deep_dup(other.permission_resource_clients)
127
+ @strict_permissions = other.strict_permissions
128
+ @expose_helper_method = other.expose_helper_method
109
129
  end
110
130
 
111
- # Normalize role_map keys to symbols for consistent lookup.
131
+ # Normalize role_map keys to symbols and validate value types.
112
132
  #
113
133
  # @param map [Hash, nil]
114
134
  # @return [Hash]
135
+ # @raise [ArgumentError] when a value is neither Symbol, String, nor nil
115
136
  def normalize_role_map(map)
116
137
  return {} unless map.is_a?(Hash)
117
138
 
118
- map.transform_keys(&:to_sym)
119
- end
139
+ map.each do |key, value|
140
+ next if value.nil? || value.is_a?(Symbol) || value.is_a?(String)
120
141
 
121
- # Copy configuration fields from another instance, duplicating mutable
122
- # structures so future writes do not leak across instances.
123
- #
124
- # @param other [Configuration]
125
- def initialize_from(other)
126
- # Copy the raw instance variable, not the getter, to preserve ENV fallback behavior
127
- @resource_client = dup_string(other.instance_variable_get(:@resource_client))
128
- @role_map = dup_hash(other.role_map)
129
- @env_claims_key = dup_string(other.env_claims_key)
130
- @realm_roles_path = dup_array(other.realm_roles_path)
131
- @resource_roles_path = dup_array(other.resource_roles_path)
132
- @permission_role_scope = other.permission_role_scope
133
- @permission_resource_clients = dup_array(other.permission_resource_clients)
134
- @expose_helper_method = other.expose_helper_method
142
+ raise ArgumentError,
143
+ "role_map value for #{key.inspect} must be a Symbol, a String, " \
144
+ "or nil (explicit revocation); got #{value.class}"
145
+ end
146
+ map.transform_keys(&:to_sym)
135
147
  end
136
148
 
137
149
  # Duplicate and freeze a string value, returning `nil` when appropriate.
@@ -141,7 +153,7 @@ module Verikloak
141
153
  def freeze_string(value)
142
154
  return nil if value.nil?
143
155
 
144
- dup_string(value).freeze
156
+ deep_dup(value).freeze
145
157
  end
146
158
 
147
159
  # Deep duplicate any object, handling nested structures recursively.
@@ -165,30 +177,6 @@ module Verikloak
165
177
  end
166
178
  end
167
179
 
168
- # Duplicate a hash using deep duplication.
169
- #
170
- # @param value [Hash, nil]
171
- # @return [Hash, nil]
172
- def dup_hash(value)
173
- deep_dup(value)
174
- end
175
-
176
- # Duplicate a string guardingly, returning `nil` when no value is present.
177
- #
178
- # @param value [String, nil]
179
- # @return [String, nil]
180
- def dup_string(value)
181
- deep_dup(value)
182
- end
183
-
184
- # Duplicate an array using deep duplication.
185
- #
186
- # @param value [Array, nil]
187
- # @return [Array, nil]
188
- def dup_array(value)
189
- deep_dup(value)
190
- end
191
-
192
180
  # Check whether a value can be safely duplicated using `dup`.
193
181
  #
194
182
  # @param value [Object]
@@ -206,7 +194,7 @@ module Verikloak
206
194
  # @param value [Array<String, Symbol>, nil]
207
195
  # @return [Array<String>, nil]
208
196
  def freeze_permission_clients(value)
209
- array = dup_array(value)
197
+ array = deep_dup(value)
210
198
  return nil if array.nil?
211
199
 
212
200
  array.compact.map(&:to_s).uniq.freeze
@@ -4,13 +4,27 @@ module Verikloak
4
4
  module Pundit
5
5
  # Rails controller mixin providing `pundit_user` and claims accessor.
6
6
  module Controller
7
- # Hook used by Rails to include helper methods in views when available.
7
+ # View-facing helpers registered on helper-capable controllers.
8
+ #
9
+ # Exposure is decided at call time (not include time) so that
10
+ # `expose_helper_method` set in `config/initializers` is honored even
11
+ # when ActionController loads before application initializers run.
12
+ module ViewHelpers
13
+ # Access raw Verikloak claims from the controller, or nil when
14
+ # exposure to views is disabled via configuration.
15
+ #
16
+ # @return [Hash, nil]
17
+ def verikloak_claims
18
+ return nil unless Verikloak::Pundit.config.expose_helper_method
19
+
20
+ controller&.verikloak_claims
21
+ end
22
+ end
23
+
24
+ # Hook used by Rails to register view helpers when available.
8
25
  # @param base [Class]
9
26
  def self.included(base)
10
- return unless base.respond_to?(:helper_method)
11
-
12
- config = Verikloak::Pundit.config
13
- base.helper_method :verikloak_claims if config.expose_helper_method
27
+ base.helper(ViewHelpers) if base.respond_to?(:helper)
14
28
  end
15
29
 
16
30
  # Pundit hook returning the UserContext built from Rack env claims.
@@ -14,7 +14,10 @@ module Verikloak
14
14
  end
15
15
 
16
16
  # Synchronize configuration with verikloak-rails when available.
17
- # Runs after verikloak-rails configuration is applied.
17
+ # verikloak-rails defines `initializer 'verikloak.configure',
18
+ # after: :load_config_initializers`, so this hook runs after both
19
+ # verikloak-rails' configuration and the app's config/initializers
20
+ # have been applied.
18
21
  initializer 'verikloak_pundit.sync_configuration', after: 'verikloak.configure' do
19
22
  Verikloak::Pundit::Railtie.sync_with_verikloak_rails if defined?(Verikloak::Rails)
20
23
  end
@@ -8,13 +8,30 @@ module Verikloak
8
8
 
9
9
  # Map a Keycloak role to a domain permission via configuration.
10
10
  #
11
+ # @deprecated Use {.permission_for} instead. This method now delegates
12
+ # to it so that strict mode and explicit nil revocations are honored
13
+ # consistently by every caller.
11
14
  # @param role [String, Symbol] Role name from JWT claims
12
15
  # @param config [Configuration] Configuration providing the role_map
13
- # @return [String, Symbol] Mapped permission (or the role itself if unmapped)
16
+ # @return [String, Symbol, nil] see {.permission_for}
14
17
  def map(role, config)
15
- return role unless config.role_map && !config.role_map.empty?
18
+ permission_for(role, config)
19
+ end
20
+
21
+ # Resolve the permission granted by a role, honoring strict mode.
22
+ # A role explicitly mapped to nil grants nothing even when strict mode
23
+ # is off — an explicit revocation of the role's implicit permission.
24
+ #
25
+ # @param role [String, Symbol] Role name from JWT claims
26
+ # @param config [Configuration] Configuration providing role_map and strict_permissions
27
+ # @return [String, Symbol, nil] Mapped permission, the role itself when
28
+ # unmapped and strict mode is off, or nil when unmapped in strict mode
29
+ # or explicitly mapped to nil
30
+ def permission_for(role, config)
31
+ key = role.to_sym
32
+ return config.role_map[key] if config.role_map.key?(key)
16
33
 
17
- config.role_map[role.to_sym] || role
34
+ config.strict_permissions ? nil : role
18
35
  end
19
36
  end
20
37
  end
@@ -124,11 +124,15 @@ module Verikloak
124
124
  Array(path_config).map do |seg|
125
125
  case seg
126
126
  when Proc
127
- # Support lambdas that accept (config) or (config, client)
128
- if seg.arity >= 2
129
- seg.call(config, client).to_s
130
- else
131
- seg.call(config).to_s
127
+ # Zero-argument procs are called as thunks and single-argument
128
+ # procs receive (config). Every other signature — including
129
+ # optional/variadic ones such as ->(cfg, client = nil)
130
+ # (arity -2) — receives (config, client), so an explicitly
131
+ # requested client is never silently dropped.
132
+ case seg.arity
133
+ when 0 then seg.call.to_s
134
+ when 1 then seg.call(config).to_s
135
+ else seg.call(config, client).to_s
132
136
  end
133
137
  else
134
138
  seg.to_s
@@ -190,15 +194,18 @@ module Verikloak
190
194
  permissions = Set.new
191
195
 
192
196
  roles.each do |role|
193
- mapped_permission = RoleMapper.map(role, config)
194
- symbol_permission = normalize_to_symbol(mapped_permission)
197
+ permission = RoleMapper.permission_for(role, config)
198
+ symbol_permission = normalize_to_symbol(permission)
195
199
  permissions << symbol_permission if symbol_permission
196
200
  end
197
201
 
198
202
  permissions
199
203
  end
200
204
 
201
- # Normalize a value to a symbol, handling various types safely.
205
+ # Normalize a value to a symbol. Only Symbols and non-empty Strings are
206
+ # convertible; nil (a strict-mode miss or an explicit role_map
207
+ # revocation) and any other type yield nil so no permission is granted.
208
+ #
202
209
  # @param value [Object] The value to convert to a symbol
203
210
  # @return [Symbol, nil] The symbol representation, or nil if not convertible
204
211
  def normalize_to_symbol(value)
@@ -206,21 +213,8 @@ module Verikloak
206
213
  when Symbol
207
214
  value
208
215
  when String
209
- return nil if value.empty?
210
-
211
- value.to_sym
212
- else
213
- if value.respond_to?(:to_sym)
214
- value.to_sym
215
- elsif value.respond_to?(:to_s)
216
- text = value.to_s
217
- return nil if text.empty?
218
-
219
- text.to_sym
220
- end
216
+ value.empty? ? nil : value.to_sym
221
217
  end
222
- rescue StandardError
223
- nil
224
218
  end
225
219
  end
226
220
  end
@@ -5,6 +5,6 @@ module Verikloak
5
5
  # Gem version for verikloak-pundit.
6
6
  #
7
7
  # @return [String]
8
- VERSION = '0.4.0'
8
+ VERSION = '1.1.0'
9
9
  end
10
10
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'monitor'
4
+
3
5
  # Verikloak::Pundit provides Pundit integration over Keycloak claims.
4
6
  require_relative 'pundit/version'
5
7
  require_relative 'pundit/configuration'
@@ -7,14 +9,19 @@ require_relative 'pundit/role_mapper'
7
9
  require_relative 'pundit/delegations'
8
10
  require_relative 'pundit/claim_utils'
9
11
  require_relative 'pundit/user_context'
10
- require_relative 'pundit/helpers'
11
12
  require_relative 'pundit/controller'
12
- require_relative 'pundit/policy'
13
13
  require_relative 'pundit/railtie' if defined?(Rails::Railtie)
14
14
 
15
15
  module Verikloak
16
16
  # Pundit integration namespace
17
17
  module Pundit
18
+ # Eagerly-initialized lock to protect configuration reads/writes.
19
+ # Using ||= inside a method is NOT thread-safe — two threads can race
20
+ # past the nil-check and create separate lock instances. A reentrant
21
+ # Monitor (rather than Mutex) allows `config` to be read from within a
22
+ # `configure` block without raising ThreadError.
23
+ @config_lock = Monitor.new
24
+
18
25
  class << self
19
26
  # Configure the library at runtime.
20
27
  #
@@ -22,7 +29,7 @@ module Verikloak
22
29
  # @return [Configuration] the current configuration after applying changes
23
30
  def configure
24
31
  new_config = nil
25
- config_mutex.synchronize do
32
+ config_lock.synchronize do
26
33
  current = @config&.dup || Configuration.new
27
34
  yield current if block_given?
28
35
  new_config = current.finalize!
@@ -35,7 +42,7 @@ module Verikloak
35
42
  #
36
43
  # @return [Configuration]
37
44
  def config
38
- config_mutex.synchronize do
45
+ config_lock.synchronize do
39
46
  @config ||= Configuration.new.finalize!
40
47
  end
41
48
  end
@@ -44,19 +51,18 @@ module Verikloak
44
51
  #
45
52
  # @return [void]
46
53
  def reset!
47
- config_mutex.synchronize do
54
+ config_lock.synchronize do
48
55
  @config = nil
49
56
  end
50
57
  end
51
58
 
52
59
  private
53
60
 
54
- # Mutex protecting configuration reads/writes to maintain thread safety.
61
+ # Reentrant lock protecting configuration reads/writes.
62
+ # Eagerly initialized at load time (see module body above).
55
63
  #
56
- # @return [Mutex]
57
- def config_mutex
58
- @config_mutex ||= Mutex.new
59
- end
64
+ # @return [Monitor]
65
+ attr_reader :config_lock
60
66
  end
61
67
  end
62
68
  end
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.4.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - taiyaky
@@ -23,46 +23,20 @@ dependencies:
23
23
  - - "~>"
24
24
  - !ruby/object:Gem::Version
25
25
  version: '2.3'
26
- - !ruby/object:Gem::Dependency
27
- name: rack
28
- requirement: !ruby/object:Gem::Requirement
29
- requirements:
30
- - - ">="
31
- - !ruby/object:Gem::Version
32
- version: '2.2'
33
- - - "<"
34
- - !ruby/object:Gem::Version
35
- version: '4.0'
36
- type: :runtime
37
- prerelease: false
38
- version_requirements: !ruby/object:Gem::Requirement
39
- requirements:
40
- - - ">="
41
- - !ruby/object:Gem::Version
42
- version: '2.2'
43
- - - "<"
44
- - !ruby/object:Gem::Version
45
- version: '4.0'
46
26
  - !ruby/object:Gem::Dependency
47
27
  name: verikloak
48
28
  requirement: !ruby/object:Gem::Requirement
49
29
  requirements:
50
- - - ">="
51
- - !ruby/object:Gem::Version
52
- version: 0.4.0
53
- - - "<"
30
+ - - "~>"
54
31
  - !ruby/object:Gem::Version
55
- version: 1.0.0
32
+ version: '1.1'
56
33
  type: :runtime
57
34
  prerelease: false
58
35
  version_requirements: !ruby/object:Gem::Requirement
59
36
  requirements:
60
- - - ">="
61
- - !ruby/object:Gem::Version
62
- version: 0.4.0
63
- - - "<"
37
+ - - "~>"
64
38
  - !ruby/object:Gem::Version
65
- version: 1.0.0
39
+ version: '1.1'
66
40
  description: Maps Keycloak JWT roles to a Pundit-friendly UserContext with helpers
67
41
  and a Rails generator.
68
42
  executables: []
@@ -81,8 +55,6 @@ files:
81
55
  - lib/verikloak/pundit/configuration.rb
82
56
  - lib/verikloak/pundit/controller.rb
83
57
  - lib/verikloak/pundit/delegations.rb
84
- - lib/verikloak/pundit/helpers.rb
85
- - lib/verikloak/pundit/policy.rb
86
58
  - lib/verikloak/pundit/railtie.rb
87
59
  - lib/verikloak/pundit/role_mapper.rb
88
60
  - lib/verikloak/pundit/user_context.rb
@@ -94,7 +66,7 @@ metadata:
94
66
  source_code_uri: https://github.com/taiyaky/verikloak-pundit
95
67
  changelog_uri: https://github.com/taiyaky/verikloak-pundit/blob/main/CHANGELOG.md
96
68
  bug_tracker_uri: https://github.com/taiyaky/verikloak-pundit/issues
97
- documentation_uri: https://rubydoc.info/gems/verikloak-pundit/0.4.0
69
+ documentation_uri: https://rubydoc.info/gems/verikloak-pundit/1.1.0
98
70
  rubygems_mfa_required: 'true'
99
71
  rdoc_options: []
100
72
  require_paths:
@@ -1,23 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Verikloak
4
- module Pundit
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.
8
- module Helpers
9
- include Delegations
10
-
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
21
- end
22
- end
23
- end
@@ -1,27 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Verikloak
4
- module Pundit
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.
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]
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.'
17
- base.extend(ClassMethods)
18
- super
19
- end
20
-
21
- # Placeholder for future class-level helpers
22
- module ClassMethods; end
23
-
24
- include Delegations
25
- end
26
- end
27
- end