Page MenuHomeSoftware Heritage

No OneTemporary

diff --git a/lib/puppet/provider/keycloak_client/kcadm.rb b/lib/puppet/provider/keycloak_client/kcadm.rb
index 904f162..eaedcf0 100644
--- a/lib/puppet/provider/keycloak_client/kcadm.rb
+++ b/lib/puppet/provider/keycloak_client/kcadm.rb
@@ -1,310 +1,316 @@
require File.expand_path(File.join(File.dirname(__FILE__), '..', 'keycloak_api'))
Puppet::Type.type(:keycloak_client).provide(:kcadm, parent: Puppet::Provider::KeycloakAPI) do
desc ''
mk_resource_methods
def attributes_properties
[
:login_theme,
:access_token_lifespan,
]
end
def dot_attributes_properties
[
:access_token_lifespan,
]
end
def attribute_key(property)
if dot_attributes_properties.include?(property)
property.to_s.tr('_', '.')
else
property
end
end
def self.instances
clients = []
realms.each do |realm|
output = kcadm('get', 'clients', realm)
Puppet.debug("#{realm} clients: #{output}")
begin
data = JSON.parse(output)
rescue JSON::ParserError
Puppet.debug('Unable to parse output from kcadm get clients')
data = []
end
data.each do |d|
# avoid built-in clients
if d.key?('clientAuthenticatorType') &&
d['clientAuthenticatorType'] == 'client-secret' &&
!d.key?('name')
begin
secret_output = kcadm('get', "clients/#{d['id']}/client-secret", realm)
rescue
Puppet.debug("Unable to get clients/#{d['id']}/client-secret")
secret_output = '{}'
end
secret_data = JSON.parse(secret_output)
secret = secret_data['value']
else
secret = nil
end
client = {}
client[:ensure] = :present
client[:id] = d['id']
client[:client_id] = d['clientId']
client[:realm] = realm
client[:name] = "#{client[:client_id]} on #{client[:realm]}"
type_properties.each do |property|
camel_key = camelize(property)
dot_key = property.to_s.tr('_', '.')
key = property.to_s
attributes = d['attributes'] || {}
if property == :secret
value = secret
elsif d.key?(camel_key)
value = d[camel_key]
elsif attributes.key?(dot_key)
value = attributes[dot_key]
elsif attributes.key?(key)
value = attributes[key]
end
if !!value == value # rubocop:disable Style/DoubleNegation
value = value.to_s.to_sym
end
client[property.to_sym] = value
end
# The absence of a value should be 'absent'
client[:login_theme] = 'absent' if client[:login_theme].nil?
clients << new(client)
end
end
clients
end
def self.prefetch(resources)
clients = instances
resources.keys.each do |name|
provider = clients.find { |c| c.client_id == resources[name][:client_id] && c.realm == resources[name][:realm] }
if provider
resources[name].provider = provider
end
end
end
def scope_map
return @scope_map if @scope_map
output = kcadm('get', 'client-scopes', resource[:realm], nil, ['id', 'name'])
begin
data = JSON.parse(output)
rescue JSON::ParserError
Puppet.debug('Unable to parse output from kcadm get client-scopes')
return {}
end
@scope_map = {}
data.each do |d|
@scope_map[d['name']] = d['id']
end
@scope_map
end
def create
raise(Puppet::Error, "Realm is mandatory for #{resource.type} #{resource.name}") if resource[:realm].nil?
data = {}
data[:id] = resource[:id]
data[:clientId] = resource[:client_id]
data[:secret] = resource[:secret] if resource[:secret]
type_properties.each do |property|
next if [:default_client_scopes, :optional_client_scopes].include?(property)
next unless resource[property.to_sym]
value = convert_property_value(resource[property.to_sym])
next if value == 'absent' || value == :absent || value.nil?
if attributes_properties.include?(property)
unless data.key?(:attributes)
data[:attributes] = {}
end
data[:attributes][attribute_key(property)] = value
else
data[camelize(property)] = value
end
end
t = Tempfile.new('keycloak_client')
t.write(JSON.pretty_generate(data))
t.close
Puppet.debug(IO.read(t.path))
begin
output = kcadm('create', 'clients', resource[:realm], t.path)
Puppet.debug("create client output: #{output}")
rescue Puppet::ExecutionFailure => e
raise Puppet::Error, "kcadm create client failed\nError message: #{e.message}"
end
if resource[:default_client_scopes] || resource[:optional_client_scopes]
client = JSON.parse(output)
scope_id = nil
end
if resource[:default_client_scopes]
remove_default_scopes = client['defaultClientScopes'] - resource[:default_client_scopes]
begin
remove_default_scopes.each do |s|
scope_id = scope_map[s]
kcadm('delete', "clients/#{resource[:id]}/default-client-scopes/#{scope_id}", resource[:realm])
end
rescue Puppet::ExecutionFailure => e
raise Puppet::Error, "kcadm delete clients/#{resource[:id]}/default-client-scopes/#{scope_id}: #{e.message}"
end
end
if resource[:optional_client_scopes]
remove_optional_scopes = client['optionalClientScopes'] - resource[:optional_client_scopes]
begin
remove_optional_scopes.each do |s|
scope_id = scope_map[s]
kcadm('delete', "clients/#{resource[:id]}/optional-client-scopes/#{scope_id}", resource[:realm])
end
rescue Puppet::ExecutionFailure => e
raise Puppet::Error, "kcadm delete clients/#{resource[:id]}/optional-client-scopes/#{scope_id}: #{e.message}"
end
end
if resource[:default_client_scopes]
add_default_scopes = resource[:default_client_scopes] - client['defaultClientScopes']
begin
add_default_scopes.each do |s|
scope_id = scope_map[s]
kcadm('update', "clients/#{resource[:id]}/default-client-scopes/#{scope_id}", resource[:realm])
end
rescue Puppet::ExecutionFailure => e
raise Puppet::Error, "kcadm update clients/#{resource[:id]}/default-client-scopes/#{scope_id}: #{e.message}"
end
end
if resource[:optional_client_scopes]
add_optional_scopes = resource[:optional_client_scopes] - client['optionalClientScopes']
begin
add_optional_scopes.each do |s|
scope_id = scope_map[s]
kcadm('update', "clients/#{resource[:id]}/optional-client-scopes/#{scope_id}", resource[:realm])
end
rescue Puppet::ExecutionFailure => e
raise Puppet::Error, "kcadm update clients/#{resource[:id]}/optional-client-scopes/#{scope_id}: #{e.message}"
end
end
@property_hash[:ensure] = :present
end
def destroy
raise(Puppet::Error, "Realm is mandatory for #{resource.type} #{resource.name}") if resource[:realm].nil?
begin
kcadm('delete', "clients/#{id}", resource[:realm])
rescue Puppet::ExecutionFailure => e
raise Puppet::Error, "kcadm delete realm failed\nError message: #{e.message}"
end
@property_hash.clear
end
def exists?
@property_hash[:ensure] == :present
end
def initialize(value = {})
super(value)
@property_flush = {}
end
type_properties.each do |prop|
define_method "#{prop}=".to_sym do |value|
@property_flush[prop] = value
end
end
def flush
unless @property_flush.empty?
raise(Puppet::Error, "Realm is mandatory for #{resource.type} #{resource.name}") if resource[:realm].nil?
data = {}
data[:clientId] = resource[:client_id]
type_properties.each do |property|
next if [:default_client_scopes, :optional_client_scopes].include?(property)
next unless @property_flush[property.to_sym]
value = convert_property_value(@property_flush[property.to_sym])
value = nil if value.to_s == 'absent'
if attributes_properties.include?(property)
unless data.key?(:attributes)
data[:attributes] = {}
end
data[:attributes][attribute_key(property)] = value
else
data[camelize(property)] = value
end
end
+ # Keycload API requires "serviceAccountsEnabled": true to be present in
+ # the JSON when "authorizationServicesEnabled": true
+ if data['authorizationServicesEnabled'] && data['serviceAccountsEnabled'].nil?
+ data[:serviceAccountsEnabled] = true
+ end
+
# Only update if more than clientId set
if data.keys.size > 1
t = Tempfile.new('keycloak_client')
t.write(JSON.pretty_generate(data))
t.close
Puppet.debug(IO.read(t.path))
begin
kcadm('update', "clients/#{id}", resource[:realm], t.path)
rescue Puppet::ExecutionFailure => e
raise Puppet::Error, "kcadm update client failed\nError message: #{e.message}"
end
end
if @property_flush[:default_client_scopes] || @property_flush[:optional_client_scopes]
scope_id = nil
end
if @property_flush[:default_client_scopes]
remove_default_scopes = @property_hash[:default_client_scopes] - @property_flush[:default_client_scopes]
begin
remove_default_scopes.each do |s|
scope_id = scope_map[s]
kcadm('delete', "clients/#{id}/default-client-scopes/#{scope_id}", resource[:realm])
end
rescue Puppet::ExecutionFailure => e
raise Puppet::Error, "kcadm delete clients/#{id}/default-client-scopes/#{scope_id}: #{e.message}"
end
end
if @property_flush[:optional_client_scopes]
remove_optional_scopes = @property_hash[:optional_client_scopes] - @property_flush[:optional_client_scopes]
begin
remove_optional_scopes.each do |s|
scope_id = scope_map[s]
kcadm('delete', "clients/#{id}/optional-client-scopes/#{scope_id}", resource[:realm])
end
rescue Puppet::ExecutionFailure => e
raise Puppet::Error, "kcadm delete clients/#{id}/optional-client-scopes/#{scope_id}: #{e.message}"
end
end
if @property_flush[:default_client_scopes]
add_default_scopes = @property_flush[:default_client_scopes] - @property_hash[:default_client_scopes]
begin
add_default_scopes.each do |s|
scope_id = scope_map[s]
kcadm('update', "clients/#{id}/default-client-scopes/#{scope_id}", resource[:realm])
end
rescue Puppet::ExecutionFailure => e
raise Puppet::Error, "kcadm update clients/#{id}/default-client-scopes/#{scope_id}: #{e.message}"
end
end
if @property_flush[:optional_client_scopes]
add_optional_scopes = @property_flush[:optional_client_scopes] - @property_hash[:optional_client_scopes]
begin
add_optional_scopes.each do |s|
scope_id = scope_map[s]
kcadm('update', "clients/#{id}/optional-client-scopes/#{scope_id}", resource[:realm])
end
rescue Puppet::ExecutionFailure => e
raise Puppet::Error, "kcadm update clients/#{id}/optional-client-scopes/#{scope_id}: #{e.message}"
end
end
end
# Collect the resources again once they've been changed (that way `puppet
# resource` will show the correct values after changes have been made).
@property_hash = resource.to_hash
end
end
diff --git a/lib/puppet/type/keycloak_client.rb b/lib/puppet/type/keycloak_client.rb
index bfc56fa..c8eae5a 100644
--- a/lib/puppet/type/keycloak_client.rb
+++ b/lib/puppet/type/keycloak_client.rb
@@ -1,204 +1,230 @@
require_relative '../../puppet_x/keycloak/type'
require_relative '../../puppet_x/keycloak/array_property'
Puppet::Type.newtype(:keycloak_client) do
desc <<-DESC
Manage Keycloak clients
@example Add a OpenID Connect client
keycloak_client { 'www.example.com':
ensure => 'present',
realm => 'test',
redirect_uris => [
"https://www.example.com/oidc",
"https://www.example.com",
],
default_client_scopes => ['profile','email'],
secret => 'supersecret',
}
DESC
extend PuppetX::Keycloak::Type
add_autorequires
ensurable
newparam(:name, namevar: true) do
desc 'The client name'
end
newparam(:client_id, namevar: true) do
desc 'clientId. Defaults to `name`.'
defaultto do
@resource[:name]
end
end
newparam(:id) do
desc 'Id. Defaults to `client_id`'
defaultto do
@resource[:client_id]
end
end
newparam(:realm, namevar: true) do
desc 'realm'
end
newparam(:secret) do
desc 'secret'
def change_to_s(currentvalue, _newvalue)
if currentvalue == :absent
'created secret'
else
'changed secret'
end
end
def is_to_s(_currentvalue) # rubocop:disable Style/PredicateName
'[old secret redacted]'
end
def should_to_s(_newvalue)
'[new secret redacted]'
end
end
newproperty(:protocol) do
desc 'protocol'
defaultto('openid-connect')
newvalues('openid-connect', 'saml')
munge { |v| v }
end
newproperty(:client_authenticator_type) do
desc 'clientAuthenticatorType'
defaultto 'client-secret'
end
newproperty(:default_client_scopes, array_matching: :all, parent: PuppetX::Keycloak::ArrayProperty) do
desc 'defaultClientScopes'
defaultto []
end
newproperty(:optional_client_scopes, array_matching: :all, parent: PuppetX::Keycloak::ArrayProperty) do
desc 'optionalClientScopes'
defaultto []
end
newproperty(:full_scope_allowed, boolean: true) do
desc 'fullScopeAllowed'
newvalues(:true, :false)
defaultto(:true)
end
newproperty(:enabled, boolean: true) do
desc 'enabled'
newvalues(:true, :false)
defaultto :true
end
newproperty(:standard_flow_enabled, boolean: true) do
desc 'standardFlowEnabled'
newvalues(:true, :false)
defaultto :true
end
newproperty(:implicit_flow_enabled, boolean: true) do
desc 'implicitFlowEnabled'
newvalues(:true, :false)
defaultto :false
end
newproperty(:direct_access_grants_enabled, boolean: true) do
desc 'enabled'
newvalues(:true, :false)
defaultto :true
end
newproperty(:service_accounts_enabled, boolean: true) do
desc 'serviceAccountsEnabled'
newvalues(:true, :false)
defaultto :false
end
+ newproperty(:authorization_services_enabled, boolean: true) do
+ desc 'authorizationServicesEnabled'
+ newvalues(:true, :false)
+ defaultto :false
+
+ # If authorizationServicesEnabled is set to false it will not be present in
+ # "get client/<clientname>" output. Puppet will thus see it as "absent".
+ # This custom insync? implementation prevents Puppet from trying to set
+ # the property to false on every run.
+ def insync?(is)
+ if is == :true && resource[:authorization_services_enabled] == :true
+ true
+ elsif is == :absent && resource[:authorization_services_enabled] == :false
+ true
+ else
+ false
+ end
+ end
+ end
+
newproperty(:public_client, boolean: true) do
desc 'enabled'
newvalues(:true, :false)
defaultto :false
end
newproperty(:root_url) do
desc 'rootUrl'
end
newproperty(:redirect_uris, array_matching: :all, parent: PuppetX::Keycloak::ArrayProperty) do
desc 'redirectUris'
defaultto []
end
newproperty(:base_url) do
desc 'baseUrl'
end
newproperty(:web_origins, array_matching: :all, parent: PuppetX::Keycloak::ArrayProperty) do
desc 'webOrigins'
defaultto []
end
newproperty(:login_theme) do
desc 'login_theme'
defaultto 'absent'
end
newproperty(:access_token_lifespan) do
desc 'access.token.lifespan'
end
autorequire(:keycloak_client_scope) do
requires = []
catalog.resources.each do |resource|
next unless resource.class.to_s == 'Puppet::Type::Keycloak_client_scope'
if self[:default_client_scopes].include?(resource[:resource_name])
requires << resource.name
end
if self[:optional_client_scopes].include?(resource[:resource_name])
requires << resource.name
end
end
requires
end
autorequire(:keycloak_protocol_mapper) do
requires = []
catalog.resources.each do |resource|
next unless resource.class.to_s == 'Puppet::Type::Keycloak_protocol_mapper'
if self[:default_client_scopes].include?(resource[:client_scope])
requires << resource.name
end
if self[:optional_client_scopes].include?(resource[:client_scope])
requires << resource.name
end
end
requires
end
+ validate do
+ if self[:authorization_services_enabled] == :true && self[:service_accounts_enabled] == :false
+ raise "Keycloak_client[#{self[:name]}] must have service_accounts_enabled => true if authorization_services_enabled => true"
+ end
+ end
+
def self.title_patterns
[
[
%r{^((\S+) on (\S+))$},
[
[:name],
[:client_id],
[:realm],
],
],
[
%r{(.*)},
[
[:name],
],
],
]
end
end
diff --git a/spec/acceptance/5_client_spec.rb b/spec/acceptance/5_client_spec.rb
index a40ed73..d7191b8 100644
--- a/spec/acceptance/5_client_spec.rb
+++ b/spec/acceptance/5_client_spec.rb
@@ -1,86 +1,94 @@
require 'spec_helper_acceptance'
describe 'keycloak_client define:', if: RSpec.configuration.keycloak_full do
context 'creates client' do
it 'runs successfully' do
pp = <<-EOS
include mysql::server
class { 'keycloak':
datasource_driver => 'mysql',
}
keycloak_realm { 'test': ensure => 'present' }
keycloak_client { 'test.foo.bar':
- realm => 'test',
- root_url => 'https://test.foo.bar',
- redirect_uris => ['https://test.foo.bar/test1'],
- default_client_scopes => ['address'],
- secret => 'foobar',
- login_theme => 'keycloak',
+ realm => 'test',
+ root_url => 'https://test.foo.bar',
+ redirect_uris => ['https://test.foo.bar/test1'],
+ default_client_scopes => ['address'],
+ secret => 'foobar',
+ login_theme => 'keycloak',
+ authorization_services_enabled => false,
+ service_accounts_enabled => true,
}
EOS
apply_manifest(pp, catch_failures: true)
apply_manifest(pp, catch_changes: true)
end
it 'has created a client' do
on hosts, '/opt/keycloak/bin/kcadm-wrapper.sh get clients/test.foo.bar -r test' do
data = JSON.parse(stdout)
expect(data['id']).to eq('test.foo.bar')
expect(data['clientId']).to eq('test.foo.bar')
expect(data['defaultClientScopes']).to eq(['address'])
expect(data['rootUrl']).to eq('https://test.foo.bar')
expect(data['redirectUris']).to eq(['https://test.foo.bar/test1'])
expect(data['attributes']['login_theme']).to eq('keycloak')
+ expect(data['authorizationServicesEnabled']).to eq(nil)
+ expect(data['serviceAccountsEnabled']).to eq(true)
end
end
it 'has set the client secret' do
on hosts, '/opt/keycloak/bin/kcadm-wrapper.sh get clients/test.foo.bar/client-secret -r test' do
data = JSON.parse(stdout)
expect(data['value']).to eq('foobar')
end
end
end
context 'updates client' do
it 'runs successfully' do
pp = <<-EOS
include mysql::server
class { 'keycloak':
datasource_driver => 'mysql',
}
keycloak_realm { 'test': ensure => 'present' }
keycloak_client { 'test.foo.bar':
- realm => 'test',
- root_url => 'https://test.foo.bar/test',
- redirect_uris => ['https://test.foo.bar/test2'],
- default_client_scopes => ['profile', 'email'],
- secret => 'foobar',
+ realm => 'test',
+ root_url => 'https://test.foo.bar/test',
+ redirect_uris => ['https://test.foo.bar/test2'],
+ default_client_scopes => ['profile', 'email'],
+ secret => 'foobar',
+ authorization_services_enabled => true,
+ service_accounts_enabled => true,
}
EOS
apply_manifest(pp, catch_failures: true)
apply_manifest(pp, catch_changes: true)
end
it 'has updated a client' do
on hosts, '/opt/keycloak/bin/kcadm-wrapper.sh get clients/test.foo.bar -r test' do
data = JSON.parse(stdout)
expect(data['id']).to eq('test.foo.bar')
expect(data['clientId']).to eq('test.foo.bar')
expect(data['defaultClientScopes']).to eq(['profile', 'email'])
expect(data['rootUrl']).to eq('https://test.foo.bar/test')
expect(data['redirectUris']).to eq(['https://test.foo.bar/test2'])
expect(data['attributes']['login_theme']).to be_nil
+ expect(data['authorizationServicesEnabled']).to eq(true)
+ expect(data['serviceAccountsEnabled']).to eq(true)
end
end
it 'has set the same client secret' do
on hosts, '/opt/keycloak/bin/kcadm-wrapper.sh get clients/test.foo.bar/client-secret -r test' do
data = JSON.parse(stdout)
expect(data['value']).to eq('foobar')
end
end
end
end

File Metadata

Mime Type
text/x-diff
Expires
Jul 4 2025, 6:38 PM (5 w, 5 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3268705

Event Timeline