Page Menu
Home
Software Heritage
Search
Configure Global Search
Log In
Files
F9348594
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
21 KB
Subscribers
None
View Options
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
Details
Attached
Mime Type
text/x-diff
Expires
Jul 4 2025, 6:38 PM (5 w, 4 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3268705
Attached To
R212 puppet-treydock-keycloak
Event Timeline
Log In to Comment