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 +4 -4
- data/CHANGELOG.md +48 -0
- data/README.md +38 -1
- data/lib/generators/verikloak/pundit/install/templates/initializer.rb +9 -2
- data/lib/verikloak/pundit/configuration.rb +70 -82
- data/lib/verikloak/pundit/controller.rb +19 -5
- data/lib/verikloak/pundit/railtie.rb +4 -1
- data/lib/verikloak/pundit/role_mapper.rb +20 -3
- data/lib/verikloak/pundit/user_context.rb +16 -22
- data/lib/verikloak/pundit/version.rb +1 -1
- data/lib/verikloak/pundit.rb +16 -10
- metadata +6 -34
- data/lib/verikloak/pundit/helpers.rb +0 -23
- data/lib/verikloak/pundit/policy.rb +0 -27
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bf2182c6f85bdb89c0d109fcabc9fdcf0141b9b9dfef32dd4d953694e9f3d239
|
|
4
|
+
data.tar.gz: d93f6833adda5312d3485c9f9aa310777c35341207f0bda7fd54c1667f7fa18a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
-
|
|
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
|
|
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
|
|
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
|
|
49
|
-
# configuration so callers can mutate a
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
87
|
-
@realm_roles_path =
|
|
88
|
-
@resource_roles_path =
|
|
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
|
-
|
|
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
|
-
#
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
@
|
|
107
|
-
@
|
|
108
|
-
@
|
|
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
|
|
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.
|
|
119
|
-
|
|
139
|
+
map.each do |key, value|
|
|
140
|
+
next if value.nil? || value.is_a?(Symbol) || value.is_a?(String)
|
|
120
141
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
#
|
|
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]
|
|
16
|
+
# @return [String, Symbol, nil] see {.permission_for}
|
|
14
17
|
def map(role, config)
|
|
15
|
-
|
|
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.
|
|
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
|
-
#
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
194
|
-
symbol_permission = normalize_to_symbol(
|
|
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
|
|
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
|
-
|
|
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
|
data/lib/verikloak/pundit.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
54
|
+
config_lock.synchronize do
|
|
48
55
|
@config = nil
|
|
49
56
|
end
|
|
50
57
|
end
|
|
51
58
|
|
|
52
59
|
private
|
|
53
60
|
|
|
54
|
-
#
|
|
61
|
+
# Reentrant lock protecting configuration reads/writes.
|
|
62
|
+
# Eagerly initialized at load time (see module body above).
|
|
55
63
|
#
|
|
56
|
-
# @return [
|
|
57
|
-
|
|
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:
|
|
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.
|
|
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.
|
|
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/
|
|
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
|