verikloak-pundit 0.2.3 → 0.3.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: a4618def320e58859e62b0562ac2d942a3e307a61b3c287bc3a748511c76a3d0
4
- data.tar.gz: ce80df2707bf38a548ef51dc6ed68acd149e2c06b2ef02d2b0a20dc709040cbf
3
+ metadata.gz: 8a9cbde30c7f580f43089a171707034fc1d476bdc63cf358ee00a7e2ceffe48c
4
+ data.tar.gz: 4bda6971e321a4a46045523f1dd01a610cb6a95b28e47a01c665d24a38bdaaf8
5
5
  SHA512:
6
- metadata.gz: 246e5840dc895ee184ef33d88ea92c5e73f58c6120217e2661e6757dd2539ee74f7ba2b2eb47502ca70115884a971ef11530458af6a8aa6f3900f96f70d1119e
7
- data.tar.gz: 6d4762d24e1b90c71ea68a1642456a9b4f3132d6424ef9587bfc1f41d70c3f124ebb1c8e60ea49d273b3e73a1077ebaeac4106311e1244890a29d41af8169300
6
+ metadata.gz: 26364d453900ef0bd915eedda4567343b65d8e9adfd7c61e02e926c954ad8bcc778042c7432ea4878a8fa1173a7f7eedccd5cea009f384b042044a65fadd53ca
7
+ data.tar.gz: 57494f3d1b8de679d2fb81b3a3ba7eff5d8635cadf0f4ef19110f8bd9d6bc70010629ed7c64b6584524c580e8968041d3c0581cc7d0468f067c13f45e4bdc254
data/CHANGELOG.md CHANGED
@@ -7,6 +7,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ---
9
9
 
10
+ ## [0.3.0] - 2026-01-01
11
+
12
+ ### Fixed
13
+ - **ENV fallback preservation**: `Configuration#dup` now correctly preserves the `nil` state of `resource_client`, allowing ENV fallback to remain dynamic after duplication. Previously, duplicating a config would freeze the resolved ENV value.
14
+ - **Non-hash resource_access entries**: `resource_roles_all_clients` now guards against malformed `resource_access` entries that are not hashes, preventing potential `NoMethodError`.
15
+
16
+ ### Changed
17
+ - **role_map key normalization**: `role_map` keys are now automatically normalized to symbols when set. This allows users to configure with string keys (e.g., from YAML) while maintaining consistent symbol-based lookup in `RoleMapper`.
18
+ - **pundit_user memoization**: `Controller#pundit_user` is now memoized with `@pundit_user ||=` to avoid creating multiple `UserContext` instances per request.
19
+
20
+ ---
21
+
22
+ ## [0.2.4] - 2026-01-01
23
+
24
+ ### Added
25
+ - **Environment variable fallback**: `resource_client` now falls back to `ENV['KEYCLOAK_RESOURCE_CLIENT']` when not explicitly configured, enabling environment-based configuration.
26
+ - **Auto-sync with verikloak-rails**: When used alongside `verikloak-rails`, `env_claims_key` is automatically synchronized from `Verikloak::Rails.config.user_env_key` if not explicitly set.
27
+
28
+ ### Changed
29
+ - **Simplified initializer template**: Generator now produces a minimal initializer with commented examples instead of explicit defaults. Most settings work out of the box.
30
+ - **README improvements**: Updated Configuration section with environment variables table, auto-configuration documentation, and removed redundant examples.
31
+ - **Consistent Pundit include**: Quick Start examples now use `Pundit::Authorization` consistently.
32
+
10
33
  ## [0.2.3] - 2025-12-31
11
34
 
12
35
  ### Added
data/README.md CHANGED
@@ -43,7 +43,7 @@ For error-handling guidance, see [ERRORS.md](ERRORS.md).
43
43
  ```ruby
44
44
  # app/controllers/application_controller.rb
45
45
  class ApplicationController < ActionController::API
46
- include Pundit
46
+ include Pundit::Authorization
47
47
  include Verikloak::Pundit::Controller # provides pundit_user
48
48
 
49
49
  # If you're also using verikloak-rails:
@@ -65,38 +65,28 @@ Where `user` is the **UserContext** provided by `pundit_user`.
65
65
 
66
66
  ## Configuration
67
67
 
68
+ Most settings have sensible defaults and can be auto-configured. You only need to customize what's different for your application.
69
+
70
+ ### Environment Variables
71
+
72
+ | Variable | Description | Default |
73
+ |----------|-------------|---------|
74
+ | `KEYCLOAK_RESOURCE_CLIENT` | Default resource client ID for resource roles | `"rails-api"` |
75
+
76
+ ### Auto-configuration with verikloak-rails
77
+
78
+ When used alongside `verikloak-rails`, the following settings are automatically synchronized:
79
+
80
+ - **`env_claims_key`**: Inherits from `Verikloak::Rails.config.user_env_key` if you haven't explicitly set it. This ensures both gems read claims from the same Rack env key.
81
+
82
+ This means in most cases you can use a minimal initializer:
83
+
68
84
  ```ruby
69
- # config/initializers/verikloak_pundit.rb
70
85
  Verikloak::Pundit.configure do |c|
71
- c.resource_client = "rails-api" # default client for resource roles
72
- c.role_map = { # optional role → permission mapping
86
+ c.role_map = {
73
87
  admin: :manage_all,
74
- editor: :write_notes,
75
- reader: :read_notes
88
+ editor: :write_notes
76
89
  }
77
- # Where to find claims in Rack env (when using verikloak/verikloak-rails)
78
- c.env_claims_key = "verikloak.user"
79
-
80
- # How to traverse JWT for roles
81
- c.realm_roles_path = %w[realm_access roles] # => claims["realm_access"]["roles"]
82
- # Lambdas in the path may accept (cfg) or (cfg, client)
83
- # where `client` is the argument passed to `user.resource_roles(client)`
84
- c.resource_roles_path = ["resource_access", ->(cfg){ cfg.resource_client }, "roles"]
85
-
86
- # Permission mapping scope for `user.has_permission?`:
87
- # :default_resource => realm roles + default client roles (recommended)
88
- # :all_resources => realm roles + roles from all clients in resource_access
89
- # (enabling this broadens permissions to every resource client;
90
- # review the upstream role assignments before turning it on)
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
97
-
98
- # Expose `verikloak_claims` to views via helper_method (Rails only)
99
- c.expose_helper_method = true
100
90
  end
101
91
  ```
102
92
 
@@ -105,12 +95,11 @@ end
105
95
  - **verikloak-bff**: When your Rails application sits behind the BFF, the access
106
96
  token presented to verikloak-pundit typically originates from the BFF
107
97
  (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.
98
+ decoded claims under the same `env_claims_key` (default: `"verikloak.user"`,
99
+ which works out of the box with `verikloak-bff >= 0.3`). If the BFF issues
100
+ tokens for multiple downstream services, set `permission_resource_clients` to
101
+ the limited list of clients whose roles should affect Rails-side authorization
102
+ to avoid accidentally inheriting permissions meant for other services.
114
103
  - **verikloak-audience**: Audience services often mint resource roles with a
115
104
  service-specific prefix (for example, `audience-service:editor`). Align your
116
105
  `role_map` keys with that naming convention so `user.has_permission?` resolves
@@ -319,35 +308,16 @@ RSpec.configure do |config|
319
308
  end
320
309
  ```
321
310
 
322
- 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:
323
-
324
- ```bash
325
- docker compose run --rm -e BUNDLE_FROZEN=0 dev bash -lc '
326
- cd integration && \
327
- apk add --no-cache --virtual .integration-build-deps \
328
- build-base \
329
- linux-headers \
330
- openssl-dev \
331
- yaml-dev && \
332
- bundle config set --local path vendor/bundle && \
333
- bundle install --jobs 4 --retry 3 && \
334
- bundle exec ruby check.rb && \
335
- apk del .integration-build-deps
336
- '
337
- ```
338
-
339
311
  ## Contributing
340
312
  Bug reports and pull requests are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details.
341
313
 
342
314
  ## Security
343
315
  If you find a security vulnerability, please follow the instructions in [SECURITY.md](SECURITY.md).
344
316
 
345
- ### Operational guidance
317
+ ## Operational Guidance
318
+
346
319
  - 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.
347
- - Combine `permission_role_scope = :all_resources` with `permission_resource_clients`
348
- to explicitly opt-in the clients that may contribute permissions. Leaving the
349
- whitelist blank (the default) reverts to the legacy behavior of trusting
350
- every client in the token.
320
+ - 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.
351
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.
352
322
 
353
323
  ## License
@@ -1,24 +1,28 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Verikloak::Pundit initializer
4
- # Configure how to read roles from Keycloak claims and how to map them
5
- # into application permissions.
4
+ #
5
+ # Most settings are auto-configured from environment variables and verikloak-rails.
6
+ # Uncomment and customize only what you need to override.
7
+ #
8
+ # Environment variables supported:
9
+ # KEYCLOAK_RESOURCE_CLIENT - resource client ID (default: 'rails-api')
10
+ #
11
+ # When used with verikloak-rails, env_claims_key is automatically synchronized.
6
12
  Verikloak::Pundit.configure do |c|
7
- c.resource_client = ENV.fetch('KEYCLOAK_RESOURCE_CLIENT', 'rails-api')
8
- c.role_map = {
9
- # admin: :manage_all,
10
- # editor: :write_notes,
11
- # reader: :read_notes
12
- }
13
- c.env_claims_key = 'verikloak.user'
14
- # claims['realm_access']['roles']
15
- c.realm_roles_path = %w[realm_access roles]
16
- # rubocop:disable Style/SymbolProc -- we need a Proc object here, not block pass
17
- # claims['resource_access'][resource_client]['roles']
18
- c.resource_roles_path = ['resource_access', ->(cfg) { cfg.resource_client }, 'roles']
19
- # rubocop:enable Style/SymbolProc
20
- # Permission mapping scope:
21
- # :default_resource => realm roles + default client roles (recommended)
22
- # :all_resources => realm roles + roles from all clients in resource_access
23
- c.permission_role_scope = :default_resource
13
+ # Resource client (optional - falls back to ENV['KEYCLOAK_RESOURCE_CLIENT'] or 'rails-api')
14
+ # c.resource_client = ENV.fetch('KEYCLOAK_RESOURCE_CLIENT', 'rails-api')
15
+
16
+ # Role to permission mapping (optional)
17
+ # c.role_map = {
18
+ # admin: :manage_all,
19
+ # editor: :write_notes,
20
+ # reader: :read_notes
21
+ # }
22
+
23
+ # Uncomment to customize JWT claims path and scope (usually not needed):
24
+ # c.env_claims_key = 'verikloak.user'
25
+ # c.realm_roles_path = %w[realm_access roles]
26
+ # c.resource_roles_path = ['resource_access', ->(cfg) { cfg.resource_client }, 'roles']
27
+ # c.permission_role_scope = :default_resource # or :all_resources
24
28
  end
@@ -22,11 +22,29 @@ module Verikloak
22
22
  # @!attribute expose_helper_method
23
23
  # @return [Boolean] whether to register `verikloak_claims` as a Rails helper method
24
24
  class Configuration
25
- attr_accessor :resource_client, :role_map, :env_claims_key,
25
+ attr_accessor :env_claims_key,
26
26
  :realm_roles_path, :resource_roles_path,
27
27
  :permission_role_scope, :permission_resource_clients,
28
28
  :expose_helper_method
29
29
 
30
+ attr_reader :role_map
31
+ attr_writer :resource_client
32
+
33
+ # Set the role map, normalizing keys to symbols for consistent lookup.
34
+ #
35
+ # @param value [Hash]
36
+ # @return [void]
37
+ def role_map=(value)
38
+ @role_map = normalize_role_map(value)
39
+ end
40
+
41
+ # Returns the resource client, falling back to ENV['KEYCLOAK_RESOURCE_CLIENT'] if not set.
42
+ #
43
+ # @return [String]
44
+ def resource_client
45
+ @resource_client || ENV.fetch('KEYCLOAK_RESOURCE_CLIENT', 'rails-api')
46
+ end
47
+
30
48
  # Build a new configuration, optionally copying values from another
31
49
  # configuration so callers can mutate a safe duplicate.
32
50
  #
@@ -77,7 +95,7 @@ module Verikloak
77
95
 
78
96
  # Populate default values that mirror the gem's out-of-the-box behavior.
79
97
  def initialize_defaults
80
- @resource_client = 'rails-api'
98
+ @resource_client = nil # Falls back to ENV['KEYCLOAK_RESOURCE_CLIENT'] or 'rails-api'
81
99
  @role_map = {} # e.g., { admin: :manage_all }
82
100
  @env_claims_key = 'verikloak.user'
83
101
  @realm_roles_path = %w[realm_access roles]
@@ -90,12 +108,23 @@ module Verikloak
90
108
  @expose_helper_method = true
91
109
  end
92
110
 
111
+ # Normalize role_map keys to symbols for consistent lookup.
112
+ #
113
+ # @param map [Hash, nil]
114
+ # @return [Hash]
115
+ def normalize_role_map(map)
116
+ return {} unless map.is_a?(Hash)
117
+
118
+ map.transform_keys(&:to_sym)
119
+ end
120
+
93
121
  # Copy configuration fields from another instance, duplicating mutable
94
122
  # structures so future writes do not leak across instances.
95
123
  #
96
124
  # @param other [Configuration]
97
125
  def initialize_from(other)
98
- @resource_client = dup_string(other.resource_client)
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))
99
128
  @role_map = dup_hash(other.role_map)
100
129
  @env_claims_key = dup_string(other.env_claims_key)
101
130
  @realm_roles_path = dup_array(other.realm_roles_path)
@@ -14,9 +14,10 @@ module Verikloak
14
14
  end
15
15
 
16
16
  # Pundit hook returning the UserContext built from Rack env claims.
17
+ # Memoized to avoid creating multiple instances per request.
17
18
  # @return [UserContext]
18
19
  def pundit_user
19
- Verikloak::Pundit::UserContext.from_env(request.env)
20
+ @pundit_user ||= Verikloak::Pundit::UserContext.from_env(request.env)
20
21
  end
21
22
 
22
23
  # Access raw Verikloak claims from Rack env.
@@ -12,6 +12,35 @@ module Verikloak
12
12
  include Verikloak::Pundit::Controller
13
13
  end
14
14
  end
15
+
16
+ # Synchronize configuration with verikloak-rails when available.
17
+ # Runs after verikloak-rails configuration is applied.
18
+ initializer 'verikloak_pundit.sync_configuration', after: 'verikloak.configure' do
19
+ Verikloak::Pundit::Railtie.sync_with_verikloak_rails if defined?(Verikloak::Rails)
20
+ end
21
+
22
+ class << self
23
+ # Sync env_claims_key with verikloak-rails configuration.
24
+ # Only applies if env_claims_key has not been explicitly set.
25
+ #
26
+ # @return [void]
27
+ def sync_with_verikloak_rails
28
+ return unless Verikloak::Rails.respond_to?(:config)
29
+
30
+ rails_config = Verikloak::Rails.config
31
+ return unless rails_config.respond_to?(:user_env_key) && rails_config.user_env_key
32
+
33
+ current_key = Verikloak::Pundit.config.env_claims_key
34
+ return unless current_key == 'verikloak.user'
35
+
36
+ # Use configure to properly update frozen config
37
+ Verikloak::Pundit.configure do |c|
38
+ c.env_claims_key = rails_config.user_env_key
39
+ end
40
+ rescue StandardError => e
41
+ warn "[verikloak-pundit] Failed to sync with verikloak-rails: #{e.message}"
42
+ end
43
+ end
15
44
  end
16
45
  end
17
46
  end
@@ -43,7 +43,7 @@ module Verikloak
43
43
  def realm_roles
44
44
  @realm_roles ||= begin
45
45
  path = resolve_path(config.realm_roles_path)
46
- Array(claims.dig(*path)).map(&:to_s).uniq.freeze
46
+ extract_roles(path)
47
47
  end
48
48
  end
49
49
 
@@ -55,7 +55,7 @@ module Verikloak
55
55
  client = client.to_s
56
56
  (@resource_roles_cache ||= {})[client] ||= begin
57
57
  path = resolve_path(config.resource_roles_path, client: client)
58
- Array(claims.dig(*path)).map(&:to_s).uniq.freeze
58
+ extract_roles(path)
59
59
  end
60
60
  end
61
61
 
@@ -107,6 +107,14 @@ module Verikloak
107
107
 
108
108
  private
109
109
 
110
+ # Extract roles from claims at the given path.
111
+ #
112
+ # @param path [Array<String>]
113
+ # @return [Array<String>]
114
+ def extract_roles(path)
115
+ Array(claims.dig(*path)).map(&:to_s).uniq.freeze
116
+ end
117
+
110
118
  # Resolve a configured path into concrete dig segments.
111
119
  #
112
120
  # @param path_config [Array<String, Proc>]
@@ -146,6 +154,7 @@ module Verikloak
146
154
  access = claims[CLAIM_RESOURCE_ACCESS]
147
155
  if access.is_a?(Hash)
148
156
  roles = access.each_with_object([]) do |(client_id, entry), acc|
157
+ next unless entry.is_a?(Hash)
149
158
  next unless permission_client_allowed?(client_id)
150
159
 
151
160
  acc.concat(Array(entry[CLAIM_ROLES]))
@@ -5,6 +5,6 @@ module Verikloak
5
5
  # Gem version for verikloak-pundit.
6
6
  #
7
7
  # @return [String]
8
- VERSION = '0.2.3'
8
+ VERSION = '0.3.0'
9
9
  end
10
10
  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.2.3
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - taiyaky
@@ -94,7 +94,7 @@ metadata:
94
94
  source_code_uri: https://github.com/taiyaky/verikloak-pundit
95
95
  changelog_uri: https://github.com/taiyaky/verikloak-pundit/blob/main/CHANGELOG.md
96
96
  bug_tracker_uri: https://github.com/taiyaky/verikloak-pundit/issues
97
- documentation_uri: https://rubydoc.info/gems/verikloak-pundit/0.2.3
97
+ documentation_uri: https://rubydoc.info/gems/verikloak-pundit/0.3.0
98
98
  rubygems_mfa_required: 'true'
99
99
  rdoc_options: []
100
100
  require_paths: