Class: Verikloak::Pundit::Configuration

Inherits:
Object
  • Object
show all
Defined in:
lib/verikloak/pundit/configuration.rb

Overview

Runtime configuration for verikloak-pundit.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(copy_from = nil) ⇒ Configuration

Build a new configuration populated with default values, optionally copying values from another configuration so callers can mutate a safe duplicate. The copy_from parameter is kept for backward compatibility with the v1.0.0 signature; dup is the idiomatic way to copy.

Parameters:



66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
# File 'lib/verikloak/pundit/configuration.rb', line 66

def initialize(copy_from = nil)
  @resource_client   = nil # Falls back to ENV['KEYCLOAK_RESOURCE_CLIENT'] or 'rails-api'
  @role_map          = {} # e.g., { admin: :manage_all }
  @env_claims_key    = 'verikloak.user'
  @realm_roles_path  = %w[realm_access roles]
  # The lambda receives (config, client) so that an explicitly requested
  # client (e.g. resource_role?(:other, :role)) resolves to that client's
  # entry instead of always falling back to the default resource client.
  @resource_roles_path = ['resource_access', ->(cfg, client) { client || cfg.resource_client }, 'roles']
  # :default_resource (realm + default client), :all_resources (realm + all clients)
  @permission_role_scope = :default_resource
  @permission_resource_clients = nil
  @strict_permissions = false
  @expose_helper_method = true
  copy_state_from(copy_from) if copy_from
end

Instance Attribute Details

#env_claims_keyString

Returns Rack env key where claims are stored (when using verikloak/verikloak-rails).

Returns:

  • (String)

    Rack env key where claims are stored (when using verikloak/verikloak-rails)



30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
# File 'lib/verikloak/pundit/configuration.rb', line 30

class Configuration
  attr_accessor :env_claims_key,
                :realm_roles_path, :resource_roles_path,
                :permission_role_scope, :permission_resource_clients,
                :strict_permissions, :expose_helper_method

  attr_reader :role_map
  attr_writer :resource_client

  # Set the role map, normalizing keys to symbols for consistent lookup.
  # Values must be Symbols, Strings, or nil (an explicit nil revokes the
  # role's implicit permission); anything else raises at assignment time
  # so misconfiguration surfaces at boot instead of as silently missing
  # permissions.
  #
  # @param value [Hash]
  # @return [void]
  # @raise [ArgumentError] when a value is neither Symbol, String, nor nil
  def role_map=(value)
    @role_map = normalize_role_map(value)
  end

  # Returns the resource client, falling back to ENV['KEYCLOAK_RESOURCE_CLIENT'] if not set.
  #
  # @return [String]
  def resource_client
    @resource_client || ENV.fetch('KEYCLOAK_RESOURCE_CLIENT', 'rails-api')
  end

  # Build a new configuration populated with default values, optionally
  # copying values from another configuration so callers can mutate a
  # safe duplicate. The `copy_from` parameter is kept for backward
  # compatibility with the v1.0.0 signature; `dup` is the idiomatic way
  # to copy.
  #
  # @param copy_from [Configuration, nil]
  def initialize(copy_from = nil)
    @resource_client   = nil # Falls back to ENV['KEYCLOAK_RESOURCE_CLIENT'] or 'rails-api'
    @role_map          = {} # e.g., { admin: :manage_all }
    @env_claims_key    = 'verikloak.user'
    @realm_roles_path  = %w[realm_access roles]
    # The lambda receives (config, client) so that an explicitly requested
    # client (e.g. resource_role?(:other, :role)) resolves to that client's
    # entry instead of always falling back to the default resource client.
    @resource_roles_path = ['resource_access', ->(cfg, client) { client || cfg.resource_client }, 'roles']
    # :default_resource (realm + default client), :all_resources (realm + all clients)
    @permission_role_scope = :default_resource
    @permission_resource_clients = nil
    @strict_permissions = false
    @expose_helper_method = true
    copy_state_from(copy_from) if copy_from
  end

  # Duplicate the configuration via Ruby's `dup`/`clone`, ensuring the new
  # instance receives freshly-copied (and unfrozen) nested state.
  #
  # @param other [Configuration]
  def initialize_copy(other)
    super
    copy_state_from(other)
  end

  # Freeze the configuration and its nested structures to prevent runtime
  # mutations once it is published to the global state. Returns `self` to
  # allow chaining inside callers.
  #
  # @return [Configuration]
  def finalize!
    @resource_client = freeze_string(@resource_client)
    @env_claims_key = freeze_string(@env_claims_key)
    @role_map = deep_dup(@role_map).freeze
    @realm_roles_path = deep_dup(@realm_roles_path).freeze
    @resource_roles_path = deep_dup(@resource_roles_path).freeze
    @permission_resource_clients = freeze_permission_clients(@permission_resource_clients)
    # Coerce flags to strict booleans based on truthiness
    @strict_permissions = @strict_permissions ? true : false
    @expose_helper_method = @expose_helper_method ? true : false
    freeze
  end

  private

  # Copy all configuration state from another instance, deep-duplicating
  # nested structures (and reading the raw @resource_client rather than
  # the getter, to preserve its ENV fallback behavior) so the copy can be
  # mutated without affecting the source.
  #
  # @param other [Configuration]
  # @return [void]
  def copy_state_from(other)
    @resource_client = deep_dup(other.instance_variable_get(:@resource_client))
    @role_map = deep_dup(other.role_map)
    @env_claims_key = deep_dup(other.env_claims_key)
    @realm_roles_path = deep_dup(other.realm_roles_path)
    @resource_roles_path = deep_dup(other.resource_roles_path)
    @permission_role_scope = other.permission_role_scope
    @permission_resource_clients = deep_dup(other.permission_resource_clients)
    @strict_permissions = other.strict_permissions
    @expose_helper_method = other.expose_helper_method
  end

  # Normalize role_map keys to symbols and validate value types.
  #
  # @param map [Hash, nil]
  # @return [Hash]
  # @raise [ArgumentError] when a value is neither Symbol, String, nor nil
  def normalize_role_map(map)
    return {} unless map.is_a?(Hash)

    map.each do |key, value|
      next if value.nil? || value.is_a?(Symbol) || value.is_a?(String)

      raise ArgumentError,
            "role_map value for #{key.inspect} must be a Symbol, a String, " \
            "or nil (explicit revocation); got #{value.class}"
    end
    map.transform_keys(&:to_sym)
  end

  # Duplicate and freeze a string value, returning `nil` when appropriate.
  #
  # @param value [String, nil]
  # @return [String, nil]
  def freeze_string(value)
    return nil if value.nil?

    deep_dup(value).freeze
  end

  # Deep duplicate any object, handling nested structures recursively.
  #
  # @param value [Object] The value to duplicate
  # @return [Object] A deep copy of the value
  def deep_dup(value)
    case value
    when nil
      nil
    when Hash
      value.each_with_object({}) do |(key, element), copy|
        copy[deep_dup(key)] = deep_dup(element)
      end
    when Array
      value.map { |element| deep_dup(element) }
    when String
      value.dup
    else
      duplicable?(value) ? value.dup : value
    end
  end

  # Check whether a value can be safely duplicated using `dup`.
  #
  # @param value [Object]
  # @return [Boolean]
  def duplicable?(value)
    return false if value.nil?
    return false if [true, false].include?(value)
    return false if value.is_a?(Symbol) || value.is_a?(Numeric) || value.is_a?(Proc)

    value.respond_to?(:dup)
  end

  # Normalize and freeze the configured permission clients list.
  #
  # @param value [Array<String, Symbol>, nil]
  # @return [Array<String>, nil]
  def freeze_permission_clients(value)
    array = deep_dup(value)
    return nil if array.nil?

    array.compact.map(&:to_s).uniq.freeze
  end
end

#expose_helper_methodBoolean

Returns whether verikloak_claims is exposed to Rails views.

Returns:

  • (Boolean)

    whether verikloak_claims is exposed to Rails views



30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
# File 'lib/verikloak/pundit/configuration.rb', line 30

class Configuration
  attr_accessor :env_claims_key,
                :realm_roles_path, :resource_roles_path,
                :permission_role_scope, :permission_resource_clients,
                :strict_permissions, :expose_helper_method

  attr_reader :role_map
  attr_writer :resource_client

  # Set the role map, normalizing keys to symbols for consistent lookup.
  # Values must be Symbols, Strings, or nil (an explicit nil revokes the
  # role's implicit permission); anything else raises at assignment time
  # so misconfiguration surfaces at boot instead of as silently missing
  # permissions.
  #
  # @param value [Hash]
  # @return [void]
  # @raise [ArgumentError] when a value is neither Symbol, String, nor nil
  def role_map=(value)
    @role_map = normalize_role_map(value)
  end

  # Returns the resource client, falling back to ENV['KEYCLOAK_RESOURCE_CLIENT'] if not set.
  #
  # @return [String]
  def resource_client
    @resource_client || ENV.fetch('KEYCLOAK_RESOURCE_CLIENT', 'rails-api')
  end

  # Build a new configuration populated with default values, optionally
  # copying values from another configuration so callers can mutate a
  # safe duplicate. The `copy_from` parameter is kept for backward
  # compatibility with the v1.0.0 signature; `dup` is the idiomatic way
  # to copy.
  #
  # @param copy_from [Configuration, nil]
  def initialize(copy_from = nil)
    @resource_client   = nil # Falls back to ENV['KEYCLOAK_RESOURCE_CLIENT'] or 'rails-api'
    @role_map          = {} # e.g., { admin: :manage_all }
    @env_claims_key    = 'verikloak.user'
    @realm_roles_path  = %w[realm_access roles]
    # The lambda receives (config, client) so that an explicitly requested
    # client (e.g. resource_role?(:other, :role)) resolves to that client's
    # entry instead of always falling back to the default resource client.
    @resource_roles_path = ['resource_access', ->(cfg, client) { client || cfg.resource_client }, 'roles']
    # :default_resource (realm + default client), :all_resources (realm + all clients)
    @permission_role_scope = :default_resource
    @permission_resource_clients = nil
    @strict_permissions = false
    @expose_helper_method = true
    copy_state_from(copy_from) if copy_from
  end

  # Duplicate the configuration via Ruby's `dup`/`clone`, ensuring the new
  # instance receives freshly-copied (and unfrozen) nested state.
  #
  # @param other [Configuration]
  def initialize_copy(other)
    super
    copy_state_from(other)
  end

  # Freeze the configuration and its nested structures to prevent runtime
  # mutations once it is published to the global state. Returns `self` to
  # allow chaining inside callers.
  #
  # @return [Configuration]
  def finalize!
    @resource_client = freeze_string(@resource_client)
    @env_claims_key = freeze_string(@env_claims_key)
    @role_map = deep_dup(@role_map).freeze
    @realm_roles_path = deep_dup(@realm_roles_path).freeze
    @resource_roles_path = deep_dup(@resource_roles_path).freeze
    @permission_resource_clients = freeze_permission_clients(@permission_resource_clients)
    # Coerce flags to strict booleans based on truthiness
    @strict_permissions = @strict_permissions ? true : false
    @expose_helper_method = @expose_helper_method ? true : false
    freeze
  end

  private

  # Copy all configuration state from another instance, deep-duplicating
  # nested structures (and reading the raw @resource_client rather than
  # the getter, to preserve its ENV fallback behavior) so the copy can be
  # mutated without affecting the source.
  #
  # @param other [Configuration]
  # @return [void]
  def copy_state_from(other)
    @resource_client = deep_dup(other.instance_variable_get(:@resource_client))
    @role_map = deep_dup(other.role_map)
    @env_claims_key = deep_dup(other.env_claims_key)
    @realm_roles_path = deep_dup(other.realm_roles_path)
    @resource_roles_path = deep_dup(other.resource_roles_path)
    @permission_role_scope = other.permission_role_scope
    @permission_resource_clients = deep_dup(other.permission_resource_clients)
    @strict_permissions = other.strict_permissions
    @expose_helper_method = other.expose_helper_method
  end

  # Normalize role_map keys to symbols and validate value types.
  #
  # @param map [Hash, nil]
  # @return [Hash]
  # @raise [ArgumentError] when a value is neither Symbol, String, nor nil
  def normalize_role_map(map)
    return {} unless map.is_a?(Hash)

    map.each do |key, value|
      next if value.nil? || value.is_a?(Symbol) || value.is_a?(String)

      raise ArgumentError,
            "role_map value for #{key.inspect} must be a Symbol, a String, " \
            "or nil (explicit revocation); got #{value.class}"
    end
    map.transform_keys(&:to_sym)
  end

  # Duplicate and freeze a string value, returning `nil` when appropriate.
  #
  # @param value [String, nil]
  # @return [String, nil]
  def freeze_string(value)
    return nil if value.nil?

    deep_dup(value).freeze
  end

  # Deep duplicate any object, handling nested structures recursively.
  #
  # @param value [Object] The value to duplicate
  # @return [Object] A deep copy of the value
  def deep_dup(value)
    case value
    when nil
      nil
    when Hash
      value.each_with_object({}) do |(key, element), copy|
        copy[deep_dup(key)] = deep_dup(element)
      end
    when Array
      value.map { |element| deep_dup(element) }
    when String
      value.dup
    else
      duplicable?(value) ? value.dup : value
    end
  end

  # Check whether a value can be safely duplicated using `dup`.
  #
  # @param value [Object]
  # @return [Boolean]
  def duplicable?(value)
    return false if value.nil?
    return false if [true, false].include?(value)
    return false if value.is_a?(Symbol) || value.is_a?(Numeric) || value.is_a?(Proc)

    value.respond_to?(:dup)
  end

  # Normalize and freeze the configured permission clients list.
  #
  # @param value [Array<String, Symbol>, nil]
  # @return [Array<String>, nil]
  def freeze_permission_clients(value)
    array = deep_dup(value)
    return nil if array.nil?

    array.compact.map(&:to_s).uniq.freeze
  end
end

#permission_resource_clientsArray<String>?

Returns list of resource clients allowed when #permission_role_scope is :all_resources. nil permits every client.

Returns:

  • (Array<String>, nil)

    list of resource clients allowed when #permission_role_scope is :all_resources. nil permits every client.



30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
# File 'lib/verikloak/pundit/configuration.rb', line 30

class Configuration
  attr_accessor :env_claims_key,
                :realm_roles_path, :resource_roles_path,
                :permission_role_scope, :permission_resource_clients,
                :strict_permissions, :expose_helper_method

  attr_reader :role_map
  attr_writer :resource_client

  # Set the role map, normalizing keys to symbols for consistent lookup.
  # Values must be Symbols, Strings, or nil (an explicit nil revokes the
  # role's implicit permission); anything else raises at assignment time
  # so misconfiguration surfaces at boot instead of as silently missing
  # permissions.
  #
  # @param value [Hash]
  # @return [void]
  # @raise [ArgumentError] when a value is neither Symbol, String, nor nil
  def role_map=(value)
    @role_map = normalize_role_map(value)
  end

  # Returns the resource client, falling back to ENV['KEYCLOAK_RESOURCE_CLIENT'] if not set.
  #
  # @return [String]
  def resource_client
    @resource_client || ENV.fetch('KEYCLOAK_RESOURCE_CLIENT', 'rails-api')
  end

  # Build a new configuration populated with default values, optionally
  # copying values from another configuration so callers can mutate a
  # safe duplicate. The `copy_from` parameter is kept for backward
  # compatibility with the v1.0.0 signature; `dup` is the idiomatic way
  # to copy.
  #
  # @param copy_from [Configuration, nil]
  def initialize(copy_from = nil)
    @resource_client   = nil # Falls back to ENV['KEYCLOAK_RESOURCE_CLIENT'] or 'rails-api'
    @role_map          = {} # e.g., { admin: :manage_all }
    @env_claims_key    = 'verikloak.user'
    @realm_roles_path  = %w[realm_access roles]
    # The lambda receives (config, client) so that an explicitly requested
    # client (e.g. resource_role?(:other, :role)) resolves to that client's
    # entry instead of always falling back to the default resource client.
    @resource_roles_path = ['resource_access', ->(cfg, client) { client || cfg.resource_client }, 'roles']
    # :default_resource (realm + default client), :all_resources (realm + all clients)
    @permission_role_scope = :default_resource
    @permission_resource_clients = nil
    @strict_permissions = false
    @expose_helper_method = true
    copy_state_from(copy_from) if copy_from
  end

  # Duplicate the configuration via Ruby's `dup`/`clone`, ensuring the new
  # instance receives freshly-copied (and unfrozen) nested state.
  #
  # @param other [Configuration]
  def initialize_copy(other)
    super
    copy_state_from(other)
  end

  # Freeze the configuration and its nested structures to prevent runtime
  # mutations once it is published to the global state. Returns `self` to
  # allow chaining inside callers.
  #
  # @return [Configuration]
  def finalize!
    @resource_client = freeze_string(@resource_client)
    @env_claims_key = freeze_string(@env_claims_key)
    @role_map = deep_dup(@role_map).freeze
    @realm_roles_path = deep_dup(@realm_roles_path).freeze
    @resource_roles_path = deep_dup(@resource_roles_path).freeze
    @permission_resource_clients = freeze_permission_clients(@permission_resource_clients)
    # Coerce flags to strict booleans based on truthiness
    @strict_permissions = @strict_permissions ? true : false
    @expose_helper_method = @expose_helper_method ? true : false
    freeze
  end

  private

  # Copy all configuration state from another instance, deep-duplicating
  # nested structures (and reading the raw @resource_client rather than
  # the getter, to preserve its ENV fallback behavior) so the copy can be
  # mutated without affecting the source.
  #
  # @param other [Configuration]
  # @return [void]
  def copy_state_from(other)
    @resource_client = deep_dup(other.instance_variable_get(:@resource_client))
    @role_map = deep_dup(other.role_map)
    @env_claims_key = deep_dup(other.env_claims_key)
    @realm_roles_path = deep_dup(other.realm_roles_path)
    @resource_roles_path = deep_dup(other.resource_roles_path)
    @permission_role_scope = other.permission_role_scope
    @permission_resource_clients = deep_dup(other.permission_resource_clients)
    @strict_permissions = other.strict_permissions
    @expose_helper_method = other.expose_helper_method
  end

  # Normalize role_map keys to symbols and validate value types.
  #
  # @param map [Hash, nil]
  # @return [Hash]
  # @raise [ArgumentError] when a value is neither Symbol, String, nor nil
  def normalize_role_map(map)
    return {} unless map.is_a?(Hash)

    map.each do |key, value|
      next if value.nil? || value.is_a?(Symbol) || value.is_a?(String)

      raise ArgumentError,
            "role_map value for #{key.inspect} must be a Symbol, a String, " \
            "or nil (explicit revocation); got #{value.class}"
    end
    map.transform_keys(&:to_sym)
  end

  # Duplicate and freeze a string value, returning `nil` when appropriate.
  #
  # @param value [String, nil]
  # @return [String, nil]
  def freeze_string(value)
    return nil if value.nil?

    deep_dup(value).freeze
  end

  # Deep duplicate any object, handling nested structures recursively.
  #
  # @param value [Object] The value to duplicate
  # @return [Object] A deep copy of the value
  def deep_dup(value)
    case value
    when nil
      nil
    when Hash
      value.each_with_object({}) do |(key, element), copy|
        copy[deep_dup(key)] = deep_dup(element)
      end
    when Array
      value.map { |element| deep_dup(element) }
    when String
      value.dup
    else
      duplicable?(value) ? value.dup : value
    end
  end

  # Check whether a value can be safely duplicated using `dup`.
  #
  # @param value [Object]
  # @return [Boolean]
  def duplicable?(value)
    return false if value.nil?
    return false if [true, false].include?(value)
    return false if value.is_a?(Symbol) || value.is_a?(Numeric) || value.is_a?(Proc)

    value.respond_to?(:dup)
  end

  # Normalize and freeze the configured permission clients list.
  #
  # @param value [Array<String, Symbol>, nil]
  # @return [Array<String>, nil]
  def freeze_permission_clients(value)
    array = deep_dup(value)
    return nil if array.nil?

    array.compact.map(&:to_s).uniq.freeze
  end
end

#permission_role_scopeSymbol

Returns :default_resource or :all_resources for permission mapping scope.

Returns:

  • (Symbol)

    :default_resource or :all_resources for permission mapping scope



30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
# File 'lib/verikloak/pundit/configuration.rb', line 30

class Configuration
  attr_accessor :env_claims_key,
                :realm_roles_path, :resource_roles_path,
                :permission_role_scope, :permission_resource_clients,
                :strict_permissions, :expose_helper_method

  attr_reader :role_map
  attr_writer :resource_client

  # Set the role map, normalizing keys to symbols for consistent lookup.
  # Values must be Symbols, Strings, or nil (an explicit nil revokes the
  # role's implicit permission); anything else raises at assignment time
  # so misconfiguration surfaces at boot instead of as silently missing
  # permissions.
  #
  # @param value [Hash]
  # @return [void]
  # @raise [ArgumentError] when a value is neither Symbol, String, nor nil
  def role_map=(value)
    @role_map = normalize_role_map(value)
  end

  # Returns the resource client, falling back to ENV['KEYCLOAK_RESOURCE_CLIENT'] if not set.
  #
  # @return [String]
  def resource_client
    @resource_client || ENV.fetch('KEYCLOAK_RESOURCE_CLIENT', 'rails-api')
  end

  # Build a new configuration populated with default values, optionally
  # copying values from another configuration so callers can mutate a
  # safe duplicate. The `copy_from` parameter is kept for backward
  # compatibility with the v1.0.0 signature; `dup` is the idiomatic way
  # to copy.
  #
  # @param copy_from [Configuration, nil]
  def initialize(copy_from = nil)
    @resource_client   = nil # Falls back to ENV['KEYCLOAK_RESOURCE_CLIENT'] or 'rails-api'
    @role_map          = {} # e.g., { admin: :manage_all }
    @env_claims_key    = 'verikloak.user'
    @realm_roles_path  = %w[realm_access roles]
    # The lambda receives (config, client) so that an explicitly requested
    # client (e.g. resource_role?(:other, :role)) resolves to that client's
    # entry instead of always falling back to the default resource client.
    @resource_roles_path = ['resource_access', ->(cfg, client) { client || cfg.resource_client }, 'roles']
    # :default_resource (realm + default client), :all_resources (realm + all clients)
    @permission_role_scope = :default_resource
    @permission_resource_clients = nil
    @strict_permissions = false
    @expose_helper_method = true
    copy_state_from(copy_from) if copy_from
  end

  # Duplicate the configuration via Ruby's `dup`/`clone`, ensuring the new
  # instance receives freshly-copied (and unfrozen) nested state.
  #
  # @param other [Configuration]
  def initialize_copy(other)
    super
    copy_state_from(other)
  end

  # Freeze the configuration and its nested structures to prevent runtime
  # mutations once it is published to the global state. Returns `self` to
  # allow chaining inside callers.
  #
  # @return [Configuration]
  def finalize!
    @resource_client = freeze_string(@resource_client)
    @env_claims_key = freeze_string(@env_claims_key)
    @role_map = deep_dup(@role_map).freeze
    @realm_roles_path = deep_dup(@realm_roles_path).freeze
    @resource_roles_path = deep_dup(@resource_roles_path).freeze
    @permission_resource_clients = freeze_permission_clients(@permission_resource_clients)
    # Coerce flags to strict booleans based on truthiness
    @strict_permissions = @strict_permissions ? true : false
    @expose_helper_method = @expose_helper_method ? true : false
    freeze
  end

  private

  # Copy all configuration state from another instance, deep-duplicating
  # nested structures (and reading the raw @resource_client rather than
  # the getter, to preserve its ENV fallback behavior) so the copy can be
  # mutated without affecting the source.
  #
  # @param other [Configuration]
  # @return [void]
  def copy_state_from(other)
    @resource_client = deep_dup(other.instance_variable_get(:@resource_client))
    @role_map = deep_dup(other.role_map)
    @env_claims_key = deep_dup(other.env_claims_key)
    @realm_roles_path = deep_dup(other.realm_roles_path)
    @resource_roles_path = deep_dup(other.resource_roles_path)
    @permission_role_scope = other.permission_role_scope
    @permission_resource_clients = deep_dup(other.permission_resource_clients)
    @strict_permissions = other.strict_permissions
    @expose_helper_method = other.expose_helper_method
  end

  # Normalize role_map keys to symbols and validate value types.
  #
  # @param map [Hash, nil]
  # @return [Hash]
  # @raise [ArgumentError] when a value is neither Symbol, String, nor nil
  def normalize_role_map(map)
    return {} unless map.is_a?(Hash)

    map.each do |key, value|
      next if value.nil? || value.is_a?(Symbol) || value.is_a?(String)

      raise ArgumentError,
            "role_map value for #{key.inspect} must be a Symbol, a String, " \
            "or nil (explicit revocation); got #{value.class}"
    end
    map.transform_keys(&:to_sym)
  end

  # Duplicate and freeze a string value, returning `nil` when appropriate.
  #
  # @param value [String, nil]
  # @return [String, nil]
  def freeze_string(value)
    return nil if value.nil?

    deep_dup(value).freeze
  end

  # Deep duplicate any object, handling nested structures recursively.
  #
  # @param value [Object] The value to duplicate
  # @return [Object] A deep copy of the value
  def deep_dup(value)
    case value
    when nil
      nil
    when Hash
      value.each_with_object({}) do |(key, element), copy|
        copy[deep_dup(key)] = deep_dup(element)
      end
    when Array
      value.map { |element| deep_dup(element) }
    when String
      value.dup
    else
      duplicable?(value) ? value.dup : value
    end
  end

  # Check whether a value can be safely duplicated using `dup`.
  #
  # @param value [Object]
  # @return [Boolean]
  def duplicable?(value)
    return false if value.nil?
    return false if [true, false].include?(value)
    return false if value.is_a?(Symbol) || value.is_a?(Numeric) || value.is_a?(Proc)

    value.respond_to?(:dup)
  end

  # Normalize and freeze the configured permission clients list.
  #
  # @param value [Array<String, Symbol>, nil]
  # @return [Array<String>, nil]
  def freeze_permission_clients(value)
    array = deep_dup(value)
    return nil if array.nil?

    array.compact.map(&:to_s).uniq.freeze
  end
end

#realm_roles_pathArray<String,Proc>

Returns path inside JWT claims to reach realm roles.

Returns:

  • (Array<String,Proc>)

    path inside JWT claims to reach realm roles



30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
# File 'lib/verikloak/pundit/configuration.rb', line 30

class Configuration
  attr_accessor :env_claims_key,
                :realm_roles_path, :resource_roles_path,
                :permission_role_scope, :permission_resource_clients,
                :strict_permissions, :expose_helper_method

  attr_reader :role_map
  attr_writer :resource_client

  # Set the role map, normalizing keys to symbols for consistent lookup.
  # Values must be Symbols, Strings, or nil (an explicit nil revokes the
  # role's implicit permission); anything else raises at assignment time
  # so misconfiguration surfaces at boot instead of as silently missing
  # permissions.
  #
  # @param value [Hash]
  # @return [void]
  # @raise [ArgumentError] when a value is neither Symbol, String, nor nil
  def role_map=(value)
    @role_map = normalize_role_map(value)
  end

  # Returns the resource client, falling back to ENV['KEYCLOAK_RESOURCE_CLIENT'] if not set.
  #
  # @return [String]
  def resource_client
    @resource_client || ENV.fetch('KEYCLOAK_RESOURCE_CLIENT', 'rails-api')
  end

  # Build a new configuration populated with default values, optionally
  # copying values from another configuration so callers can mutate a
  # safe duplicate. The `copy_from` parameter is kept for backward
  # compatibility with the v1.0.0 signature; `dup` is the idiomatic way
  # to copy.
  #
  # @param copy_from [Configuration, nil]
  def initialize(copy_from = nil)
    @resource_client   = nil # Falls back to ENV['KEYCLOAK_RESOURCE_CLIENT'] or 'rails-api'
    @role_map          = {} # e.g., { admin: :manage_all }
    @env_claims_key    = 'verikloak.user'
    @realm_roles_path  = %w[realm_access roles]
    # The lambda receives (config, client) so that an explicitly requested
    # client (e.g. resource_role?(:other, :role)) resolves to that client's
    # entry instead of always falling back to the default resource client.
    @resource_roles_path = ['resource_access', ->(cfg, client) { client || cfg.resource_client }, 'roles']
    # :default_resource (realm + default client), :all_resources (realm + all clients)
    @permission_role_scope = :default_resource
    @permission_resource_clients = nil
    @strict_permissions = false
    @expose_helper_method = true
    copy_state_from(copy_from) if copy_from
  end

  # Duplicate the configuration via Ruby's `dup`/`clone`, ensuring the new
  # instance receives freshly-copied (and unfrozen) nested state.
  #
  # @param other [Configuration]
  def initialize_copy(other)
    super
    copy_state_from(other)
  end

  # Freeze the configuration and its nested structures to prevent runtime
  # mutations once it is published to the global state. Returns `self` to
  # allow chaining inside callers.
  #
  # @return [Configuration]
  def finalize!
    @resource_client = freeze_string(@resource_client)
    @env_claims_key = freeze_string(@env_claims_key)
    @role_map = deep_dup(@role_map).freeze
    @realm_roles_path = deep_dup(@realm_roles_path).freeze
    @resource_roles_path = deep_dup(@resource_roles_path).freeze
    @permission_resource_clients = freeze_permission_clients(@permission_resource_clients)
    # Coerce flags to strict booleans based on truthiness
    @strict_permissions = @strict_permissions ? true : false
    @expose_helper_method = @expose_helper_method ? true : false
    freeze
  end

  private

  # Copy all configuration state from another instance, deep-duplicating
  # nested structures (and reading the raw @resource_client rather than
  # the getter, to preserve its ENV fallback behavior) so the copy can be
  # mutated without affecting the source.
  #
  # @param other [Configuration]
  # @return [void]
  def copy_state_from(other)
    @resource_client = deep_dup(other.instance_variable_get(:@resource_client))
    @role_map = deep_dup(other.role_map)
    @env_claims_key = deep_dup(other.env_claims_key)
    @realm_roles_path = deep_dup(other.realm_roles_path)
    @resource_roles_path = deep_dup(other.resource_roles_path)
    @permission_role_scope = other.permission_role_scope
    @permission_resource_clients = deep_dup(other.permission_resource_clients)
    @strict_permissions = other.strict_permissions
    @expose_helper_method = other.expose_helper_method
  end

  # Normalize role_map keys to symbols and validate value types.
  #
  # @param map [Hash, nil]
  # @return [Hash]
  # @raise [ArgumentError] when a value is neither Symbol, String, nor nil
  def normalize_role_map(map)
    return {} unless map.is_a?(Hash)

    map.each do |key, value|
      next if value.nil? || value.is_a?(Symbol) || value.is_a?(String)

      raise ArgumentError,
            "role_map value for #{key.inspect} must be a Symbol, a String, " \
            "or nil (explicit revocation); got #{value.class}"
    end
    map.transform_keys(&:to_sym)
  end

  # Duplicate and freeze a string value, returning `nil` when appropriate.
  #
  # @param value [String, nil]
  # @return [String, nil]
  def freeze_string(value)
    return nil if value.nil?

    deep_dup(value).freeze
  end

  # Deep duplicate any object, handling nested structures recursively.
  #
  # @param value [Object] The value to duplicate
  # @return [Object] A deep copy of the value
  def deep_dup(value)
    case value
    when nil
      nil
    when Hash
      value.each_with_object({}) do |(key, element), copy|
        copy[deep_dup(key)] = deep_dup(element)
      end
    when Array
      value.map { |element| deep_dup(element) }
    when String
      value.dup
    else
      duplicable?(value) ? value.dup : value
    end
  end

  # Check whether a value can be safely duplicated using `dup`.
  #
  # @param value [Object]
  # @return [Boolean]
  def duplicable?(value)
    return false if value.nil?
    return false if [true, false].include?(value)
    return false if value.is_a?(Symbol) || value.is_a?(Numeric) || value.is_a?(Proc)

    value.respond_to?(:dup)
  end

  # Normalize and freeze the configured permission clients list.
  #
  # @param value [Array<String, Symbol>, nil]
  # @return [Array<String>, nil]
  def freeze_permission_clients(value)
    array = deep_dup(value)
    return nil if array.nil?

    array.compact.map(&:to_s).uniq.freeze
  end
end

#resource_clientString

Returns the resource client, falling back to ENV if not set.

Returns:

  • (String)


30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
# File 'lib/verikloak/pundit/configuration.rb', line 30

class Configuration
  attr_accessor :env_claims_key,
                :realm_roles_path, :resource_roles_path,
                :permission_role_scope, :permission_resource_clients,
                :strict_permissions, :expose_helper_method

  attr_reader :role_map
  attr_writer :resource_client

  # Set the role map, normalizing keys to symbols for consistent lookup.
  # Values must be Symbols, Strings, or nil (an explicit nil revokes the
  # role's implicit permission); anything else raises at assignment time
  # so misconfiguration surfaces at boot instead of as silently missing
  # permissions.
  #
  # @param value [Hash]
  # @return [void]
  # @raise [ArgumentError] when a value is neither Symbol, String, nor nil
  def role_map=(value)
    @role_map = normalize_role_map(value)
  end

  # Returns the resource client, falling back to ENV['KEYCLOAK_RESOURCE_CLIENT'] if not set.
  #
  # @return [String]
  def resource_client
    @resource_client || ENV.fetch('KEYCLOAK_RESOURCE_CLIENT', 'rails-api')
  end

  # Build a new configuration populated with default values, optionally
  # copying values from another configuration so callers can mutate a
  # safe duplicate. The `copy_from` parameter is kept for backward
  # compatibility with the v1.0.0 signature; `dup` is the idiomatic way
  # to copy.
  #
  # @param copy_from [Configuration, nil]
  def initialize(copy_from = nil)
    @resource_client   = nil # Falls back to ENV['KEYCLOAK_RESOURCE_CLIENT'] or 'rails-api'
    @role_map          = {} # e.g., { admin: :manage_all }
    @env_claims_key    = 'verikloak.user'
    @realm_roles_path  = %w[realm_access roles]
    # The lambda receives (config, client) so that an explicitly requested
    # client (e.g. resource_role?(:other, :role)) resolves to that client's
    # entry instead of always falling back to the default resource client.
    @resource_roles_path = ['resource_access', ->(cfg, client) { client || cfg.resource_client }, 'roles']
    # :default_resource (realm + default client), :all_resources (realm + all clients)
    @permission_role_scope = :default_resource
    @permission_resource_clients = nil
    @strict_permissions = false
    @expose_helper_method = true
    copy_state_from(copy_from) if copy_from
  end

  # Duplicate the configuration via Ruby's `dup`/`clone`, ensuring the new
  # instance receives freshly-copied (and unfrozen) nested state.
  #
  # @param other [Configuration]
  def initialize_copy(other)
    super
    copy_state_from(other)
  end

  # Freeze the configuration and its nested structures to prevent runtime
  # mutations once it is published to the global state. Returns `self` to
  # allow chaining inside callers.
  #
  # @return [Configuration]
  def finalize!
    @resource_client = freeze_string(@resource_client)
    @env_claims_key = freeze_string(@env_claims_key)
    @role_map = deep_dup(@role_map).freeze
    @realm_roles_path = deep_dup(@realm_roles_path).freeze
    @resource_roles_path = deep_dup(@resource_roles_path).freeze
    @permission_resource_clients = freeze_permission_clients(@permission_resource_clients)
    # Coerce flags to strict booleans based on truthiness
    @strict_permissions = @strict_permissions ? true : false
    @expose_helper_method = @expose_helper_method ? true : false
    freeze
  end

  private

  # Copy all configuration state from another instance, deep-duplicating
  # nested structures (and reading the raw @resource_client rather than
  # the getter, to preserve its ENV fallback behavior) so the copy can be
  # mutated without affecting the source.
  #
  # @param other [Configuration]
  # @return [void]
  def copy_state_from(other)
    @resource_client = deep_dup(other.instance_variable_get(:@resource_client))
    @role_map = deep_dup(other.role_map)
    @env_claims_key = deep_dup(other.env_claims_key)
    @realm_roles_path = deep_dup(other.realm_roles_path)
    @resource_roles_path = deep_dup(other.resource_roles_path)
    @permission_role_scope = other.permission_role_scope
    @permission_resource_clients = deep_dup(other.permission_resource_clients)
    @strict_permissions = other.strict_permissions
    @expose_helper_method = other.expose_helper_method
  end

  # Normalize role_map keys to symbols and validate value types.
  #
  # @param map [Hash, nil]
  # @return [Hash]
  # @raise [ArgumentError] when a value is neither Symbol, String, nor nil
  def normalize_role_map(map)
    return {} unless map.is_a?(Hash)

    map.each do |key, value|
      next if value.nil? || value.is_a?(Symbol) || value.is_a?(String)

      raise ArgumentError,
            "role_map value for #{key.inspect} must be a Symbol, a String, " \
            "or nil (explicit revocation); got #{value.class}"
    end
    map.transform_keys(&:to_sym)
  end

  # Duplicate and freeze a string value, returning `nil` when appropriate.
  #
  # @param value [String, nil]
  # @return [String, nil]
  def freeze_string(value)
    return nil if value.nil?

    deep_dup(value).freeze
  end

  # Deep duplicate any object, handling nested structures recursively.
  #
  # @param value [Object] The value to duplicate
  # @return [Object] A deep copy of the value
  def deep_dup(value)
    case value
    when nil
      nil
    when Hash
      value.each_with_object({}) do |(key, element), copy|
        copy[deep_dup(key)] = deep_dup(element)
      end
    when Array
      value.map { |element| deep_dup(element) }
    when String
      value.dup
    else
      duplicable?(value) ? value.dup : value
    end
  end

  # Check whether a value can be safely duplicated using `dup`.
  #
  # @param value [Object]
  # @return [Boolean]
  def duplicable?(value)
    return false if value.nil?
    return false if [true, false].include?(value)
    return false if value.is_a?(Symbol) || value.is_a?(Numeric) || value.is_a?(Proc)

    value.respond_to?(:dup)
  end

  # Normalize and freeze the configured permission clients list.
  #
  # @param value [Array<String, Symbol>, nil]
  # @return [Array<String>, nil]
  def freeze_permission_clients(value)
    array = deep_dup(value)
    return nil if array.nil?

    array.compact.map(&:to_s).uniq.freeze
  end
end

#resource_roles_pathArray<String,Proc>

Returns path inside JWT claims to reach resource roles.

Returns:

  • (Array<String,Proc>)

    path inside JWT claims to reach resource roles



30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
# File 'lib/verikloak/pundit/configuration.rb', line 30

class Configuration
  attr_accessor :env_claims_key,
                :realm_roles_path, :resource_roles_path,
                :permission_role_scope, :permission_resource_clients,
                :strict_permissions, :expose_helper_method

  attr_reader :role_map
  attr_writer :resource_client

  # Set the role map, normalizing keys to symbols for consistent lookup.
  # Values must be Symbols, Strings, or nil (an explicit nil revokes the
  # role's implicit permission); anything else raises at assignment time
  # so misconfiguration surfaces at boot instead of as silently missing
  # permissions.
  #
  # @param value [Hash]
  # @return [void]
  # @raise [ArgumentError] when a value is neither Symbol, String, nor nil
  def role_map=(value)
    @role_map = normalize_role_map(value)
  end

  # Returns the resource client, falling back to ENV['KEYCLOAK_RESOURCE_CLIENT'] if not set.
  #
  # @return [String]
  def resource_client
    @resource_client || ENV.fetch('KEYCLOAK_RESOURCE_CLIENT', 'rails-api')
  end

  # Build a new configuration populated with default values, optionally
  # copying values from another configuration so callers can mutate a
  # safe duplicate. The `copy_from` parameter is kept for backward
  # compatibility with the v1.0.0 signature; `dup` is the idiomatic way
  # to copy.
  #
  # @param copy_from [Configuration, nil]
  def initialize(copy_from = nil)
    @resource_client   = nil # Falls back to ENV['KEYCLOAK_RESOURCE_CLIENT'] or 'rails-api'
    @role_map          = {} # e.g., { admin: :manage_all }
    @env_claims_key    = 'verikloak.user'
    @realm_roles_path  = %w[realm_access roles]
    # The lambda receives (config, client) so that an explicitly requested
    # client (e.g. resource_role?(:other, :role)) resolves to that client's
    # entry instead of always falling back to the default resource client.
    @resource_roles_path = ['resource_access', ->(cfg, client) { client || cfg.resource_client }, 'roles']
    # :default_resource (realm + default client), :all_resources (realm + all clients)
    @permission_role_scope = :default_resource
    @permission_resource_clients = nil
    @strict_permissions = false
    @expose_helper_method = true
    copy_state_from(copy_from) if copy_from
  end

  # Duplicate the configuration via Ruby's `dup`/`clone`, ensuring the new
  # instance receives freshly-copied (and unfrozen) nested state.
  #
  # @param other [Configuration]
  def initialize_copy(other)
    super
    copy_state_from(other)
  end

  # Freeze the configuration and its nested structures to prevent runtime
  # mutations once it is published to the global state. Returns `self` to
  # allow chaining inside callers.
  #
  # @return [Configuration]
  def finalize!
    @resource_client = freeze_string(@resource_client)
    @env_claims_key = freeze_string(@env_claims_key)
    @role_map = deep_dup(@role_map).freeze
    @realm_roles_path = deep_dup(@realm_roles_path).freeze
    @resource_roles_path = deep_dup(@resource_roles_path).freeze
    @permission_resource_clients = freeze_permission_clients(@permission_resource_clients)
    # Coerce flags to strict booleans based on truthiness
    @strict_permissions = @strict_permissions ? true : false
    @expose_helper_method = @expose_helper_method ? true : false
    freeze
  end

  private

  # Copy all configuration state from another instance, deep-duplicating
  # nested structures (and reading the raw @resource_client rather than
  # the getter, to preserve its ENV fallback behavior) so the copy can be
  # mutated without affecting the source.
  #
  # @param other [Configuration]
  # @return [void]
  def copy_state_from(other)
    @resource_client = deep_dup(other.instance_variable_get(:@resource_client))
    @role_map = deep_dup(other.role_map)
    @env_claims_key = deep_dup(other.env_claims_key)
    @realm_roles_path = deep_dup(other.realm_roles_path)
    @resource_roles_path = deep_dup(other.resource_roles_path)
    @permission_role_scope = other.permission_role_scope
    @permission_resource_clients = deep_dup(other.permission_resource_clients)
    @strict_permissions = other.strict_permissions
    @expose_helper_method = other.expose_helper_method
  end

  # Normalize role_map keys to symbols and validate value types.
  #
  # @param map [Hash, nil]
  # @return [Hash]
  # @raise [ArgumentError] when a value is neither Symbol, String, nor nil
  def normalize_role_map(map)
    return {} unless map.is_a?(Hash)

    map.each do |key, value|
      next if value.nil? || value.is_a?(Symbol) || value.is_a?(String)

      raise ArgumentError,
            "role_map value for #{key.inspect} must be a Symbol, a String, " \
            "or nil (explicit revocation); got #{value.class}"
    end
    map.transform_keys(&:to_sym)
  end

  # Duplicate and freeze a string value, returning `nil` when appropriate.
  #
  # @param value [String, nil]
  # @return [String, nil]
  def freeze_string(value)
    return nil if value.nil?

    deep_dup(value).freeze
  end

  # Deep duplicate any object, handling nested structures recursively.
  #
  # @param value [Object] The value to duplicate
  # @return [Object] A deep copy of the value
  def deep_dup(value)
    case value
    when nil
      nil
    when Hash
      value.each_with_object({}) do |(key, element), copy|
        copy[deep_dup(key)] = deep_dup(element)
      end
    when Array
      value.map { |element| deep_dup(element) }
    when String
      value.dup
    else
      duplicable?(value) ? value.dup : value
    end
  end

  # Check whether a value can be safely duplicated using `dup`.
  #
  # @param value [Object]
  # @return [Boolean]
  def duplicable?(value)
    return false if value.nil?
    return false if [true, false].include?(value)
    return false if value.is_a?(Symbol) || value.is_a?(Numeric) || value.is_a?(Proc)

    value.respond_to?(:dup)
  end

  # Normalize and freeze the configured permission clients list.
  #
  # @param value [Array<String, Symbol>, nil]
  # @return [Array<String>, nil]
  def freeze_permission_clients(value)
    array = deep_dup(value)
    return nil if array.nil?

    array.compact.map(&:to_s).uniq.freeze
  end
end

#role_mapHash{Symbol=>Symbol,String,nil}

Returns mapping from roles to permissions; a nil value explicitly revokes the role's implicit permission.

Returns:

  • (Hash{Symbol=>Symbol,String,nil})

    mapping from roles to permissions; a nil value explicitly revokes the role's implicit permission



30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
# File 'lib/verikloak/pundit/configuration.rb', line 30

class Configuration
  attr_accessor :env_claims_key,
                :realm_roles_path, :resource_roles_path,
                :permission_role_scope, :permission_resource_clients,
                :strict_permissions, :expose_helper_method

  attr_reader :role_map
  attr_writer :resource_client

  # Set the role map, normalizing keys to symbols for consistent lookup.
  # Values must be Symbols, Strings, or nil (an explicit nil revokes the
  # role's implicit permission); anything else raises at assignment time
  # so misconfiguration surfaces at boot instead of as silently missing
  # permissions.
  #
  # @param value [Hash]
  # @return [void]
  # @raise [ArgumentError] when a value is neither Symbol, String, nor nil
  def role_map=(value)
    @role_map = normalize_role_map(value)
  end

  # Returns the resource client, falling back to ENV['KEYCLOAK_RESOURCE_CLIENT'] if not set.
  #
  # @return [String]
  def resource_client
    @resource_client || ENV.fetch('KEYCLOAK_RESOURCE_CLIENT', 'rails-api')
  end

  # Build a new configuration populated with default values, optionally
  # copying values from another configuration so callers can mutate a
  # safe duplicate. The `copy_from` parameter is kept for backward
  # compatibility with the v1.0.0 signature; `dup` is the idiomatic way
  # to copy.
  #
  # @param copy_from [Configuration, nil]
  def initialize(copy_from = nil)
    @resource_client   = nil # Falls back to ENV['KEYCLOAK_RESOURCE_CLIENT'] or 'rails-api'
    @role_map          = {} # e.g., { admin: :manage_all }
    @env_claims_key    = 'verikloak.user'
    @realm_roles_path  = %w[realm_access roles]
    # The lambda receives (config, client) so that an explicitly requested
    # client (e.g. resource_role?(:other, :role)) resolves to that client's
    # entry instead of always falling back to the default resource client.
    @resource_roles_path = ['resource_access', ->(cfg, client) { client || cfg.resource_client }, 'roles']
    # :default_resource (realm + default client), :all_resources (realm + all clients)
    @permission_role_scope = :default_resource
    @permission_resource_clients = nil
    @strict_permissions = false
    @expose_helper_method = true
    copy_state_from(copy_from) if copy_from
  end

  # Duplicate the configuration via Ruby's `dup`/`clone`, ensuring the new
  # instance receives freshly-copied (and unfrozen) nested state.
  #
  # @param other [Configuration]
  def initialize_copy(other)
    super
    copy_state_from(other)
  end

  # Freeze the configuration and its nested structures to prevent runtime
  # mutations once it is published to the global state. Returns `self` to
  # allow chaining inside callers.
  #
  # @return [Configuration]
  def finalize!
    @resource_client = freeze_string(@resource_client)
    @env_claims_key = freeze_string(@env_claims_key)
    @role_map = deep_dup(@role_map).freeze
    @realm_roles_path = deep_dup(@realm_roles_path).freeze
    @resource_roles_path = deep_dup(@resource_roles_path).freeze
    @permission_resource_clients = freeze_permission_clients(@permission_resource_clients)
    # Coerce flags to strict booleans based on truthiness
    @strict_permissions = @strict_permissions ? true : false
    @expose_helper_method = @expose_helper_method ? true : false
    freeze
  end

  private

  # Copy all configuration state from another instance, deep-duplicating
  # nested structures (and reading the raw @resource_client rather than
  # the getter, to preserve its ENV fallback behavior) so the copy can be
  # mutated without affecting the source.
  #
  # @param other [Configuration]
  # @return [void]
  def copy_state_from(other)
    @resource_client = deep_dup(other.instance_variable_get(:@resource_client))
    @role_map = deep_dup(other.role_map)
    @env_claims_key = deep_dup(other.env_claims_key)
    @realm_roles_path = deep_dup(other.realm_roles_path)
    @resource_roles_path = deep_dup(other.resource_roles_path)
    @permission_role_scope = other.permission_role_scope
    @permission_resource_clients = deep_dup(other.permission_resource_clients)
    @strict_permissions = other.strict_permissions
    @expose_helper_method = other.expose_helper_method
  end

  # Normalize role_map keys to symbols and validate value types.
  #
  # @param map [Hash, nil]
  # @return [Hash]
  # @raise [ArgumentError] when a value is neither Symbol, String, nor nil
  def normalize_role_map(map)
    return {} unless map.is_a?(Hash)

    map.each do |key, value|
      next if value.nil? || value.is_a?(Symbol) || value.is_a?(String)

      raise ArgumentError,
            "role_map value for #{key.inspect} must be a Symbol, a String, " \
            "or nil (explicit revocation); got #{value.class}"
    end
    map.transform_keys(&:to_sym)
  end

  # Duplicate and freeze a string value, returning `nil` when appropriate.
  #
  # @param value [String, nil]
  # @return [String, nil]
  def freeze_string(value)
    return nil if value.nil?

    deep_dup(value).freeze
  end

  # Deep duplicate any object, handling nested structures recursively.
  #
  # @param value [Object] The value to duplicate
  # @return [Object] A deep copy of the value
  def deep_dup(value)
    case value
    when nil
      nil
    when Hash
      value.each_with_object({}) do |(key, element), copy|
        copy[deep_dup(key)] = deep_dup(element)
      end
    when Array
      value.map { |element| deep_dup(element) }
    when String
      value.dup
    else
      duplicable?(value) ? value.dup : value
    end
  end

  # Check whether a value can be safely duplicated using `dup`.
  #
  # @param value [Object]
  # @return [Boolean]
  def duplicable?(value)
    return false if value.nil?
    return false if [true, false].include?(value)
    return false if value.is_a?(Symbol) || value.is_a?(Numeric) || value.is_a?(Proc)

    value.respond_to?(:dup)
  end

  # Normalize and freeze the configured permission clients list.
  #
  # @param value [Array<String, Symbol>, nil]
  # @return [Array<String>, nil]
  def freeze_permission_clients(value)
    array = deep_dup(value)
    return nil if array.nil?

    array.compact.map(&:to_s).uniq.freeze
  end
end

#strict_permissionsBoolean

Returns when true, has_permission? only grants permissions that appear as values in #role_map; unmapped role names no longer act as implicit permissions.

Returns:

  • (Boolean)

    when true, has_permission? only grants permissions that appear as values in #role_map; unmapped role names no longer act as implicit permissions



30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
# File 'lib/verikloak/pundit/configuration.rb', line 30

class Configuration
  attr_accessor :env_claims_key,
                :realm_roles_path, :resource_roles_path,
                :permission_role_scope, :permission_resource_clients,
                :strict_permissions, :expose_helper_method

  attr_reader :role_map
  attr_writer :resource_client

  # Set the role map, normalizing keys to symbols for consistent lookup.
  # Values must be Symbols, Strings, or nil (an explicit nil revokes the
  # role's implicit permission); anything else raises at assignment time
  # so misconfiguration surfaces at boot instead of as silently missing
  # permissions.
  #
  # @param value [Hash]
  # @return [void]
  # @raise [ArgumentError] when a value is neither Symbol, String, nor nil
  def role_map=(value)
    @role_map = normalize_role_map(value)
  end

  # Returns the resource client, falling back to ENV['KEYCLOAK_RESOURCE_CLIENT'] if not set.
  #
  # @return [String]
  def resource_client
    @resource_client || ENV.fetch('KEYCLOAK_RESOURCE_CLIENT', 'rails-api')
  end

  # Build a new configuration populated with default values, optionally
  # copying values from another configuration so callers can mutate a
  # safe duplicate. The `copy_from` parameter is kept for backward
  # compatibility with the v1.0.0 signature; `dup` is the idiomatic way
  # to copy.
  #
  # @param copy_from [Configuration, nil]
  def initialize(copy_from = nil)
    @resource_client   = nil # Falls back to ENV['KEYCLOAK_RESOURCE_CLIENT'] or 'rails-api'
    @role_map          = {} # e.g., { admin: :manage_all }
    @env_claims_key    = 'verikloak.user'
    @realm_roles_path  = %w[realm_access roles]
    # The lambda receives (config, client) so that an explicitly requested
    # client (e.g. resource_role?(:other, :role)) resolves to that client's
    # entry instead of always falling back to the default resource client.
    @resource_roles_path = ['resource_access', ->(cfg, client) { client || cfg.resource_client }, 'roles']
    # :default_resource (realm + default client), :all_resources (realm + all clients)
    @permission_role_scope = :default_resource
    @permission_resource_clients = nil
    @strict_permissions = false
    @expose_helper_method = true
    copy_state_from(copy_from) if copy_from
  end

  # Duplicate the configuration via Ruby's `dup`/`clone`, ensuring the new
  # instance receives freshly-copied (and unfrozen) nested state.
  #
  # @param other [Configuration]
  def initialize_copy(other)
    super
    copy_state_from(other)
  end

  # Freeze the configuration and its nested structures to prevent runtime
  # mutations once it is published to the global state. Returns `self` to
  # allow chaining inside callers.
  #
  # @return [Configuration]
  def finalize!
    @resource_client = freeze_string(@resource_client)
    @env_claims_key = freeze_string(@env_claims_key)
    @role_map = deep_dup(@role_map).freeze
    @realm_roles_path = deep_dup(@realm_roles_path).freeze
    @resource_roles_path = deep_dup(@resource_roles_path).freeze
    @permission_resource_clients = freeze_permission_clients(@permission_resource_clients)
    # Coerce flags to strict booleans based on truthiness
    @strict_permissions = @strict_permissions ? true : false
    @expose_helper_method = @expose_helper_method ? true : false
    freeze
  end

  private

  # Copy all configuration state from another instance, deep-duplicating
  # nested structures (and reading the raw @resource_client rather than
  # the getter, to preserve its ENV fallback behavior) so the copy can be
  # mutated without affecting the source.
  #
  # @param other [Configuration]
  # @return [void]
  def copy_state_from(other)
    @resource_client = deep_dup(other.instance_variable_get(:@resource_client))
    @role_map = deep_dup(other.role_map)
    @env_claims_key = deep_dup(other.env_claims_key)
    @realm_roles_path = deep_dup(other.realm_roles_path)
    @resource_roles_path = deep_dup(other.resource_roles_path)
    @permission_role_scope = other.permission_role_scope
    @permission_resource_clients = deep_dup(other.permission_resource_clients)
    @strict_permissions = other.strict_permissions
    @expose_helper_method = other.expose_helper_method
  end

  # Normalize role_map keys to symbols and validate value types.
  #
  # @param map [Hash, nil]
  # @return [Hash]
  # @raise [ArgumentError] when a value is neither Symbol, String, nor nil
  def normalize_role_map(map)
    return {} unless map.is_a?(Hash)

    map.each do |key, value|
      next if value.nil? || value.is_a?(Symbol) || value.is_a?(String)

      raise ArgumentError,
            "role_map value for #{key.inspect} must be a Symbol, a String, " \
            "or nil (explicit revocation); got #{value.class}"
    end
    map.transform_keys(&:to_sym)
  end

  # Duplicate and freeze a string value, returning `nil` when appropriate.
  #
  # @param value [String, nil]
  # @return [String, nil]
  def freeze_string(value)
    return nil if value.nil?

    deep_dup(value).freeze
  end

  # Deep duplicate any object, handling nested structures recursively.
  #
  # @param value [Object] The value to duplicate
  # @return [Object] A deep copy of the value
  def deep_dup(value)
    case value
    when nil
      nil
    when Hash
      value.each_with_object({}) do |(key, element), copy|
        copy[deep_dup(key)] = deep_dup(element)
      end
    when Array
      value.map { |element| deep_dup(element) }
    when String
      value.dup
    else
      duplicable?(value) ? value.dup : value
    end
  end

  # Check whether a value can be safely duplicated using `dup`.
  #
  # @param value [Object]
  # @return [Boolean]
  def duplicable?(value)
    return false if value.nil?
    return false if [true, false].include?(value)
    return false if value.is_a?(Symbol) || value.is_a?(Numeric) || value.is_a?(Proc)

    value.respond_to?(:dup)
  end

  # Normalize and freeze the configured permission clients list.
  #
  # @param value [Array<String, Symbol>, nil]
  # @return [Array<String>, nil]
  def freeze_permission_clients(value)
    array = deep_dup(value)
    return nil if array.nil?

    array.compact.map(&:to_s).uniq.freeze
  end
end

Instance Method Details

#finalize!Configuration

Freeze the configuration and its nested structures to prevent runtime mutations once it is published to the global state. Returns self to allow chaining inside callers.

Returns:



97
98
99
100
101
102
103
104
105
106
107
108
# File 'lib/verikloak/pundit/configuration.rb', line 97

def finalize!
  @resource_client = freeze_string(@resource_client)
  @env_claims_key = freeze_string(@env_claims_key)
  @role_map = deep_dup(@role_map).freeze
  @realm_roles_path = deep_dup(@realm_roles_path).freeze
  @resource_roles_path = deep_dup(@resource_roles_path).freeze
  @permission_resource_clients = freeze_permission_clients(@permission_resource_clients)
  # Coerce flags to strict booleans based on truthiness
  @strict_permissions = @strict_permissions ? true : false
  @expose_helper_method = @expose_helper_method ? true : false
  freeze
end

#initialize_copy(other) ⇒ Object

Duplicate the configuration via Ruby's dup/clone, ensuring the new instance receives freshly-copied (and unfrozen) nested state.

Parameters:



87
88
89
90
# File 'lib/verikloak/pundit/configuration.rb', line 87

def initialize_copy(other)
  super
  copy_state_from(other)
end