diff --git a/lib/puppet/provider/keycloak_client/kcadm.rb b/lib/puppet/provider/keycloak_client/kcadm.rb index fc67c08..3a2e87b 100644 --- a/lib/puppet/provider/keycloak_client/kcadm.rb +++ b/lib/puppet/provider/keycloak_client/kcadm.rb @@ -1,322 +1,373 @@ 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 self.auth_flow_properties + { + browser_flow: 'browser', + direct_grant_flow: 'direct_grant', + } + end + + def auth_flow_properties + self.class.auth_flow_properties + 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'] || {} + auth_flows = d['authenticationFlowBindingOverrides'] || {} 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] + elsif auth_flows.key?(auth_flow_properties[property]) + flow_alias = flow_ids(realm)[auth_flows[auth_flow_properties[property]]] + value = flow_alias 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 self.flow_ids(realm) + @flow_ids = {} unless @flow_ids + return @flow_ids[realm] if @flow_ids[realm] + output = kcadm('get', 'authentication/flows', realm, nil, ['id', 'alias']) + begin + data = JSON.parse(output) + rescue JSON::ParserError + Puppet.debug('Unable to parse output from kcadm get authentication/flows') + return {} + end + @flow_ids[realm] = {} + data.each do |d| + @flow_ids[realm][d['alias']] = d['id'] + @flow_ids[realm][d['id']] = d['alias'] + end + @flow_ids[realm] + end + + def flow_ids + @flow_ids = {} unless @flow_ids + return @flow_ids unless @flow_ids.empty? + self.class.instance_variable_set(:@flow_ids, nil) + @flow_ids = self.class.flow_ids(resource[:realm]) + @flow_ids + 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 + elsif auth_flow_properties.include?(property) + unless data.key?(:authenticationFlowBindingOverrides) + data[:authenticationFlowBindingOverrides] = {} + end + flow_id = flow_ids[value] + data[:authenticationFlowBindingOverrides][auth_flow_properties[property]] = flow_id 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 if resource[:login_theme] check_theme_exists(resource[:login_theme], "Keycloak_client[#{resource[:name]}]") end 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] + data[:authenticationFlowBindingOverrides] = {} 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 + elsif auth_flow_properties.include?(property) + flow_id = value.nil? ? nil : flow_ids[value] + data[:authenticationFlowBindingOverrides][auth_flow_properties[property]] = flow_id 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 if @property_flush[:login_theme] check_theme_exists(@property_flush[:login_theme], "Keycloak_client[#{resource[:name]}]") end 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 c8eae5a..ddd6bae 100644 --- a/lib/puppet/type/keycloak_client.rb +++ b/lib/puppet/type/keycloak_client.rb @@ -1,230 +1,255 @@ 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/" 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 + newproperty(:browser_flow) do + desc 'authenticationFlowBindingOverrides.browser (Use flow alias, not ID)' + defaultto :absent + end + + newproperty(:direct_grant_flow) do + desc 'authenticationFlowBindingOverrides.direct_grant (Use flow alias, not ID)' + defaultto :absent + 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 + autorequire(:keycloak_flow) do + requires = [] + catalog.resources.each do |resource| + next unless resource.class.to_s == 'Puppet::Type::Keycloak_flow' + next if self[:realm] != resource[:realm] + if self[:browser_flow] == resource[:alias] + requires << resource.name + end + if self[:direct_grant_flow] == resource[:alias] + 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 d7191b8..72098dd 100644 --- a/spec/acceptance/5_client_spec.rb +++ b/spec/acceptance/5_client_spec.rb @@ -1,94 +1,144 @@ 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_flow { 'foo on 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', authorization_services_enabled => false, service_accounts_enabled => true, + browser_flow => 'foo', + } + keycloak_client { 'test.foo.baz': + 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, + browser_flow => 'foo', } 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) + expect(data['authenticationFlowBindingOverrides']['browser']).to eq('foo-test') + end + end + + it 'has created a client2' do + on hosts, '/opt/keycloak/bin/kcadm-wrapper.sh get clients/test.foo.baz -r test' do + data = JSON.parse(stdout) + expect(data['authenticationFlowBindingOverrides']['browser']).to eq('foo-test') 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', authorization_services_enabled => true, service_accounts_enabled => true, } + keycloak_client { 'test.foo.baz': + 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, + browser_flow => 'browser', + } 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) + expect(data['authenticationFlowBindingOverrides']).to eq({}) + end + end + + it 'has updated a client flow' do + browser_id = nil + on hosts, "/opt/keycloak/bin/kcadm-wrapper.sh get authentication/flows -r test --fields 'id,alias'" do + data = JSON.parse(stdout) + data.each do |d| + if d['alias'] == 'browser' + browser_id = d['id'] + break + end + end + end + on hosts, '/opt/keycloak/bin/kcadm-wrapper.sh get clients/test.foo.baz -r test' do + data = JSON.parse(stdout) + expect(data['authenticationFlowBindingOverrides']['browser']).to eq(browser_id) 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 diff --git a/spec/fixtures/unit/puppet/provider/keycloak_client/kcadm/get-flows.out b/spec/fixtures/unit/puppet/provider/keycloak_client/kcadm/get-flows.out new file mode 100644 index 0000000..2a926f7 --- /dev/null +++ b/spec/fixtures/unit/puppet/provider/keycloak_client/kcadm/get-flows.out @@ -0,0 +1,37 @@ +[ { + "id" : "179892ab-2a2c-43f4-9ddb-08312df71b03", + "alias" : "direct grant" +}, { + "id" : "5a6bbfbb-5096-4d7b-bdcd-819145a10bb1", + "alias" : "browser" +}, { + "id" : "6a54fc22-389c-421c-94eb-13aa8a6bc89f", + "alias" : "registration" +}, { + "id" : "80412938-9327-4f98-bda0-3a8a2b20b6f7", + "alias" : "reset credentials" +}, { + "id" : "9d85d580-841a-47c5-8e5d-c30d0fa62d8f", + "alias" : "first broker login" +}, { + "id" : "a63b4f41-1eca-4ec1-a6f0-3f0fe22af31d", + "alias" : "cilogon-flow2" +}, { + "id" : "browser-with-duo-osc", + "alias" : "browser-with-duo" +}, { + "id" : "c40f1064-dfb9-4af2-85d3-5179b4316c5d", + "alias" : "copy-browser-with-duo" +}, { + "id" : "d4b835f9-f5ab-4508-9006-22d074a05487", + "alias" : "clients" +}, { + "id" : "e033144f-6148-499b-bb3c-01a7c065d0c2", + "alias" : "docker auth" +}, { + "id" : "fac37945-1aa2-40cb-8bb4-fbcbe74215bc", + "alias" : "cilogon-flow" +}, { + "id" : "identity-provider-duo-osc", + "alias" : "identity-provider-duo" +} ] \ No newline at end of file diff --git a/spec/fixtures/unit/puppet/provider/keycloak_client/kcadm/get-test.out b/spec/fixtures/unit/puppet/provider/keycloak_client/kcadm/get-test.out index 6df6f43..47fd5f1 100644 --- a/spec/fixtures/unit/puppet/provider/keycloak_client/kcadm/get-test.out +++ b/spec/fixtures/unit/puppet/provider/keycloak_client/kcadm/get-test.out @@ -1,579 +1,582 @@ [ { "id" : "example.com", "clientId" : "example.com", "surrogateAuthRequired" : false, "enabled" : true, "clientAuthenticatorType" : "client-secret", "redirectUris" : [ "https://example.com", "https://example.com/oidc" ], "webOrigins" : [ ], "notBefore" : 0, "bearerOnly" : false, "consentRequired" : false, "standardFlowEnabled" : true, "implicitFlowEnabled" : false, "directAccessGrantsEnabled" : true, "serviceAccountsEnabled" : false, "publicClient" : false, "frontchannelLogout" : false, "protocol" : "openid-connect", "attributes" : { }, "fullScopeAllowed" : true, "nodeReRegistrationTimeout" : -1, "clientTemplate" : "oidc-clients", "useTemplateConfig" : false, "useTemplateScope" : true, - "useTemplateMappers" : true + "useTemplateMappers" : true, + "authenticationFlowBindingOverrides" : { + "browser" : "5a6bbfbb-5096-4d7b-bdcd-819145a10bb1" + } }, { "id" : "c34ca6ea-f447-4948-9f5e-78020af6755f", "clientId" : "account", "name" : "${client_account}", "baseUrl" : "/auth/realms/awesim/account", "surrogateAuthRequired" : false, "enabled" : true, "clientAuthenticatorType" : "client-secret", "defaultRoles" : [ "view-profile", "manage-account" ], "redirectUris" : [ "/auth/realms/awesim/account/*" ], "webOrigins" : [ ], "notBefore" : 0, "bearerOnly" : false, "consentRequired" : false, "standardFlowEnabled" : true, "implicitFlowEnabled" : false, "directAccessGrantsEnabled" : false, "serviceAccountsEnabled" : false, "publicClient" : false, "frontchannelLogout" : false, "attributes" : { }, "fullScopeAllowed" : false, "nodeReRegistrationTimeout" : 0, "protocolMappers" : [ { "id" : "ae5dcc15-f062-4bd5-b65e-88d3db925cfd", "name" : "family name", "protocol" : "openid-connect", "protocolMapper" : "oidc-usermodel-property-mapper", "consentRequired" : true, "consentText" : "${familyName}", "config" : { "userinfo.token.claim" : "true", "user.attribute" : "lastName", "id.token.claim" : "true", "access.token.claim" : "true", "claim.name" : "family_name", "jsonType.label" : "String" } }, { "id" : "b0c0b52d-ea95-441d-b90d-0b0c6ac9c35e", "name" : "role list", "protocol" : "saml", "protocolMapper" : "saml-role-list-mapper", "consentRequired" : false, "config" : { "single" : "false", "attribute.nameformat" : "Basic", "attribute.name" : "Role" } }, { "id" : "88b7fc8f-2961-41a0-b006-f7671c20404d", "name" : "given name", "protocol" : "openid-connect", "protocolMapper" : "oidc-usermodel-property-mapper", "consentRequired" : true, "consentText" : "${givenName}", "config" : { "userinfo.token.claim" : "true", "user.attribute" : "firstName", "id.token.claim" : "true", "access.token.claim" : "true", "claim.name" : "given_name", "jsonType.label" : "String" } }, { "id" : "53bcc377-07a0-4cdb-96fc-a73ee192d01d", "name" : "email", "protocol" : "openid-connect", "protocolMapper" : "oidc-usermodel-property-mapper", "consentRequired" : true, "consentText" : "${email}", "config" : { "userinfo.token.claim" : "true", "user.attribute" : "email", "id.token.claim" : "true", "access.token.claim" : "true", "claim.name" : "email", "jsonType.label" : "String" } }, { "id" : "82bb98f8-bfa8-4fec-9edb-1fe6eb39f24d", "name" : "username", "protocol" : "openid-connect", "protocolMapper" : "oidc-usermodel-property-mapper", "consentRequired" : true, "consentText" : "${username}", "config" : { "userinfo.token.claim" : "true", "user.attribute" : "username", "id.token.claim" : "true", "access.token.claim" : "true", "claim.name" : "preferred_username", "jsonType.label" : "String" } }, { "id" : "a259e3ac-6556-4636-a984-9ec2b9818649", "name" : "full name", "protocol" : "openid-connect", "protocolMapper" : "oidc-full-name-mapper", "consentRequired" : true, "consentText" : "${fullName}", "config" : { "id.token.claim" : "true", "access.token.claim" : "true" } } ], "useTemplateConfig" : false, "useTemplateScope" : false, "useTemplateMappers" : false }, { "id" : "f8af08a7-dfcd-45ac-a179-0a7dd50ef0ad", "clientId" : "admin-cli", "name" : "${client_admin-cli}", "surrogateAuthRequired" : false, "enabled" : true, "clientAuthenticatorType" : "client-secret", "redirectUris" : [ ], "webOrigins" : [ ], "notBefore" : 0, "bearerOnly" : false, "consentRequired" : false, "standardFlowEnabled" : false, "implicitFlowEnabled" : false, "directAccessGrantsEnabled" : true, "serviceAccountsEnabled" : false, "publicClient" : true, "frontchannelLogout" : false, "attributes" : { }, "fullScopeAllowed" : false, "nodeReRegistrationTimeout" : 0, "protocolMappers" : [ { "id" : "c9c207f9-e65a-4723-887a-1c80b9e3fe32", "name" : "full name", "protocol" : "openid-connect", "protocolMapper" : "oidc-full-name-mapper", "consentRequired" : true, "consentText" : "${fullName}", "config" : { "id.token.claim" : "true", "access.token.claim" : "true" } }, { "id" : "36a885a9-ef62-494d-af4d-57452626a69d", "name" : "given name", "protocol" : "openid-connect", "protocolMapper" : "oidc-usermodel-property-mapper", "consentRequired" : true, "consentText" : "${givenName}", "config" : { "userinfo.token.claim" : "true", "user.attribute" : "firstName", "id.token.claim" : "true", "access.token.claim" : "true", "claim.name" : "given_name", "jsonType.label" : "String" } }, { "id" : "e49bb6d5-f33f-4baa-9908-897b7ff0777e", "name" : "email", "protocol" : "openid-connect", "protocolMapper" : "oidc-usermodel-property-mapper", "consentRequired" : true, "consentText" : "${email}", "config" : { "userinfo.token.claim" : "true", "user.attribute" : "email", "id.token.claim" : "true", "access.token.claim" : "true", "claim.name" : "email", "jsonType.label" : "String" } }, { "id" : "cabaab54-e812-4477-876d-ecbaf3503c76", "name" : "family name", "protocol" : "openid-connect", "protocolMapper" : "oidc-usermodel-property-mapper", "consentRequired" : true, "consentText" : "${familyName}", "config" : { "userinfo.token.claim" : "true", "user.attribute" : "lastName", "id.token.claim" : "true", "access.token.claim" : "true", "claim.name" : "family_name", "jsonType.label" : "String" } }, { "id" : "0bb553dd-3ff8-450f-a197-8a2a7cbe6e73", "name" : "role list", "protocol" : "saml", "protocolMapper" : "saml-role-list-mapper", "consentRequired" : false, "config" : { "single" : "false", "attribute.nameformat" : "Basic", "attribute.name" : "Role" } }, { "id" : "86b08f09-ca74-4827-a881-9e06d1845758", "name" : "username", "protocol" : "openid-connect", "protocolMapper" : "oidc-usermodel-property-mapper", "consentRequired" : true, "consentText" : "${username}", "config" : { "userinfo.token.claim" : "true", "user.attribute" : "username", "id.token.claim" : "true", "access.token.claim" : "true", "claim.name" : "preferred_username", "jsonType.label" : "String" } } ], "useTemplateConfig" : false, "useTemplateScope" : false, "useTemplateMappers" : false }, { "id" : "d04141ac-9fb5-4ebc-9cf8-d008fd5091a5", "clientId" : "realm-management", "name" : "${client_realm-management}", "surrogateAuthRequired" : false, "enabled" : true, "clientAuthenticatorType" : "client-secret", "redirectUris" : [ ], "webOrigins" : [ ], "notBefore" : 0, "bearerOnly" : true, "consentRequired" : false, "standardFlowEnabled" : true, "implicitFlowEnabled" : false, "directAccessGrantsEnabled" : false, "serviceAccountsEnabled" : false, "publicClient" : false, "frontchannelLogout" : false, "attributes" : { }, "fullScopeAllowed" : false, "nodeReRegistrationTimeout" : 0, "protocolMappers" : [ { "id" : "5db6d3ce-7361-4d98-b371-2cbfc968c118", "name" : "full name", "protocol" : "openid-connect", "protocolMapper" : "oidc-full-name-mapper", "consentRequired" : true, "consentText" : "${fullName}", "config" : { "id.token.claim" : "true", "access.token.claim" : "true" } }, { "id" : "85b5ae6d-82b5-4fa3-8cb8-021ffc033078", "name" : "family name", "protocol" : "openid-connect", "protocolMapper" : "oidc-usermodel-property-mapper", "consentRequired" : true, "consentText" : "${familyName}", "config" : { "userinfo.token.claim" : "true", "user.attribute" : "lastName", "id.token.claim" : "true", "access.token.claim" : "true", "claim.name" : "family_name", "jsonType.label" : "String" } }, { "id" : "8bd9c845-5b39-4ac2-aa65-52ef51ba5268", "name" : "role list", "protocol" : "saml", "protocolMapper" : "saml-role-list-mapper", "consentRequired" : false, "config" : { "single" : "false", "attribute.nameformat" : "Basic", "attribute.name" : "Role" } }, { "id" : "57e20306-2c10-44db-9f06-fef9f8e527a0", "name" : "email", "protocol" : "openid-connect", "protocolMapper" : "oidc-usermodel-property-mapper", "consentRequired" : true, "consentText" : "${email}", "config" : { "userinfo.token.claim" : "true", "user.attribute" : "email", "id.token.claim" : "true", "access.token.claim" : "true", "claim.name" : "email", "jsonType.label" : "String" } }, { "id" : "f5881ad1-d377-449b-b3f0-6bb4c014a863", "name" : "username", "protocol" : "openid-connect", "protocolMapper" : "oidc-usermodel-property-mapper", "consentRequired" : true, "consentText" : "${username}", "config" : { "userinfo.token.claim" : "true", "user.attribute" : "username", "id.token.claim" : "true", "access.token.claim" : "true", "claim.name" : "preferred_username", "jsonType.label" : "String" } }, { "id" : "da0e6915-4f0f-4dc1-8639-c539777c626f", "name" : "given name", "protocol" : "openid-connect", "protocolMapper" : "oidc-usermodel-property-mapper", "consentRequired" : true, "consentText" : "${givenName}", "config" : { "userinfo.token.claim" : "true", "user.attribute" : "firstName", "id.token.claim" : "true", "access.token.claim" : "true", "claim.name" : "given_name", "jsonType.label" : "String" } } ], "useTemplateConfig" : false, "useTemplateScope" : false, "useTemplateMappers" : false }, { "id" : "ab1e62eb-9f41-4eba-964f-422d1db77290", "clientId" : "broker", "name" : "${client_broker}", "surrogateAuthRequired" : false, "enabled" : true, "clientAuthenticatorType" : "client-secret", "redirectUris" : [ ], "webOrigins" : [ ], "notBefore" : 0, "bearerOnly" : false, "consentRequired" : false, "standardFlowEnabled" : true, "implicitFlowEnabled" : false, "directAccessGrantsEnabled" : false, "serviceAccountsEnabled" : false, "publicClient" : false, "frontchannelLogout" : false, "attributes" : { }, "fullScopeAllowed" : false, "nodeReRegistrationTimeout" : 0, "protocolMappers" : [ { "id" : "b60f758b-9a12-41cb-963e-368ecc49db9e", "name" : "email", "protocol" : "openid-connect", "protocolMapper" : "oidc-usermodel-property-mapper", "consentRequired" : true, "consentText" : "${email}", "config" : { "userinfo.token.claim" : "true", "user.attribute" : "email", "id.token.claim" : "true", "access.token.claim" : "true", "claim.name" : "email", "jsonType.label" : "String" } }, { "id" : "a1f27feb-1328-45d4-9011-103a3e633533", "name" : "role list", "protocol" : "saml", "protocolMapper" : "saml-role-list-mapper", "consentRequired" : false, "config" : { "single" : "false", "attribute.nameformat" : "Basic", "attribute.name" : "Role" } }, { "id" : "a54ac2a4-b41a-46e4-ab9e-ab0c11144cf6", "name" : "full name", "protocol" : "openid-connect", "protocolMapper" : "oidc-full-name-mapper", "consentRequired" : true, "consentText" : "${fullName}", "config" : { "id.token.claim" : "true", "access.token.claim" : "true" } }, { "id" : "45169a5f-a449-43f2-8702-80b0b4f69e3a", "name" : "given name", "protocol" : "openid-connect", "protocolMapper" : "oidc-usermodel-property-mapper", "consentRequired" : true, "consentText" : "${givenName}", "config" : { "userinfo.token.claim" : "true", "user.attribute" : "firstName", "id.token.claim" : "true", "access.token.claim" : "true", "claim.name" : "given_name", "jsonType.label" : "String" } }, { "id" : "64ff407e-313c-4ec8-aaae-1dba4a1fceef", "name" : "username", "protocol" : "openid-connect", "protocolMapper" : "oidc-usermodel-property-mapper", "consentRequired" : true, "consentText" : "${username}", "config" : { "userinfo.token.claim" : "true", "user.attribute" : "username", "id.token.claim" : "true", "access.token.claim" : "true", "claim.name" : "preferred_username", "jsonType.label" : "String" } }, { "id" : "1346e1f5-3897-4bb1-9906-3a341333f832", "name" : "family name", "protocol" : "openid-connect", "protocolMapper" : "oidc-usermodel-property-mapper", "consentRequired" : true, "consentText" : "${familyName}", "config" : { "userinfo.token.claim" : "true", "user.attribute" : "lastName", "id.token.claim" : "true", "access.token.claim" : "true", "claim.name" : "family_name", "jsonType.label" : "String" } } ], "useTemplateConfig" : false, "useTemplateScope" : false, "useTemplateMappers" : false }, { "id" : "f6db0adb-aaab-4fcc-aff4-c2cfabb88ec6", "clientId" : "security-admin-console", "name" : "${client_security-admin-console}", "baseUrl" : "/auth/admin/awesim/console/index.html", "surrogateAuthRequired" : false, "enabled" : true, "clientAuthenticatorType" : "client-secret", "redirectUris" : [ "/auth/admin/awesim/console/*" ], "webOrigins" : [ ], "notBefore" : 0, "bearerOnly" : false, "consentRequired" : false, "standardFlowEnabled" : true, "implicitFlowEnabled" : false, "directAccessGrantsEnabled" : false, "serviceAccountsEnabled" : false, "publicClient" : true, "frontchannelLogout" : false, "attributes" : { }, "fullScopeAllowed" : false, "nodeReRegistrationTimeout" : 0, "protocolMappers" : [ { "id" : "2b4ea3b7-f3c6-4e9a-bfc4-988f08419412", "name" : "family name", "protocol" : "openid-connect", "protocolMapper" : "oidc-usermodel-property-mapper", "consentRequired" : true, "consentText" : "${familyName}", "config" : { "userinfo.token.claim" : "true", "user.attribute" : "lastName", "id.token.claim" : "true", "access.token.claim" : "true", "claim.name" : "family_name", "jsonType.label" : "String" } }, { "id" : "587e5eff-8f70-405c-891c-9d26e39bda98", "name" : "username", "protocol" : "openid-connect", "protocolMapper" : "oidc-usermodel-property-mapper", "consentRequired" : true, "consentText" : "${username}", "config" : { "userinfo.token.claim" : "true", "user.attribute" : "username", "id.token.claim" : "true", "access.token.claim" : "true", "claim.name" : "preferred_username", "jsonType.label" : "String" } }, { "id" : "4e48da37-ed0e-4ce0-8529-7ed6efb751de", "name" : "full name", "protocol" : "openid-connect", "protocolMapper" : "oidc-full-name-mapper", "consentRequired" : true, "consentText" : "${fullName}", "config" : { "id.token.claim" : "true", "access.token.claim" : "true" } }, { "id" : "2a861ba4-b84a-40e2-a348-cbae5ae14224", "name" : "locale", "protocol" : "openid-connect", "protocolMapper" : "oidc-usermodel-attribute-mapper", "consentRequired" : false, "consentText" : "${locale}", "config" : { "userinfo.token.claim" : "true", "user.attribute" : "locale", "id.token.claim" : "true", "access.token.claim" : "true", "claim.name" : "locale", "jsonType.label" : "String" } }, { "id" : "476788d9-97a7-420e-a8fc-faff9aef4ea4", "name" : "role list", "protocol" : "saml", "protocolMapper" : "saml-role-list-mapper", "consentRequired" : false, "config" : { "single" : "false", "attribute.nameformat" : "Basic", "attribute.name" : "Role" } }, { "id" : "d9960282-54b2-4767-80ad-92086494d2ce", "name" : "given name", "protocol" : "openid-connect", "protocolMapper" : "oidc-usermodel-property-mapper", "consentRequired" : true, "consentText" : "${givenName}", "config" : { "userinfo.token.claim" : "true", "user.attribute" : "firstName", "id.token.claim" : "true", "access.token.claim" : "true", "claim.name" : "given_name", "jsonType.label" : "String" } }, { "id" : "e1520868-6914-4867-b244-e7ca852ed5d3", "name" : "email", "protocol" : "openid-connect", "protocolMapper" : "oidc-usermodel-property-mapper", "consentRequired" : true, "consentText" : "${email}", "config" : { "userinfo.token.claim" : "true", "user.attribute" : "email", "id.token.claim" : "true", "access.token.claim" : "true", "claim.name" : "email", "jsonType.label" : "String" } } ], "useTemplateConfig" : false, "useTemplateScope" : false, "useTemplateMappers" : false } ] diff --git a/spec/unit/puppet/provider/keycloak_client/kcadm_spec.rb b/spec/unit/puppet/provider/keycloak_client/kcadm_spec.rb index 5419af4..a31253d 100644 --- a/spec/unit/puppet/provider/keycloak_client/kcadm_spec.rb +++ b/spec/unit/puppet/provider/keycloak_client/kcadm_spec.rb @@ -1,106 +1,130 @@ require 'spec_helper' describe Puppet::Type.type(:keycloak_client).provider(:kcadm) do let(:type) do Puppet::Type.type(:keycloak_client) end let(:resource) do type.new(name: 'foo', realm: 'test', default_client_scopes: ['profile']) end describe 'self.instances' do it 'creates instances' do allow(described_class).to receive(:realms).and_return(['test', 'master']) allow(described_class).to receive(:kcadm).with('get', 'clients', 'master').and_return(my_fixture_read('get-master.out')) allow(described_class).to receive(:kcadm).with('get', 'clients', 'test').and_return(my_fixture_read('get-test.out')) allow(described_class).to receive(:kcadm).with('get', 'clients/example.com/client-secret', 'test').and_return(my_fixture_read('get-client-secret.out')) + allow(described_class).to receive(:kcadm).with('get', 'authentication/flows', 'master', nil, ['id', 'alias']).and_return('[]') + allow(described_class).to receive(:kcadm).with('get', 'authentication/flows', 'test', nil, ['id', 'alias']).and_return(my_fixture_read('get-flows.out')) expect(described_class.instances.length).to eq(7 + 6) end it 'returns the resource for a fileset' do allow(described_class).to receive(:realms).and_return(['test', 'master']) allow(described_class).to receive(:kcadm).with('get', 'clients', 'master').and_return(my_fixture_read('get-master.out')) allow(described_class).to receive(:kcadm).with('get', 'clients', 'test').and_return(my_fixture_read('get-test.out')) allow(described_class).to receive(:kcadm).with('get', 'clients/example.com/client-secret', 'test').and_return(my_fixture_read('get-client-secret.out')) + allow(described_class).to receive(:kcadm).with('get', 'authentication/flows', 'master', nil, ['id', 'alias']).and_return('[]') + allow(described_class).to receive(:kcadm).with('get', 'authentication/flows', 'test', nil, ['id', 'alias']).and_return(my_fixture_read('get-flows.out')) property_hash = described_class.instances[0].instance_variable_get('@property_hash') expect(property_hash[:name]).to eq('example.com on test') + expect(property_hash[:browser_flow]).to eq('browser') end end # describe 'self.prefetch' do # let(:instances) do # all_realms.map { |f| described_class.new(f) } # end # let(:resources) do # all_realms.each_with_object({}) do |f, h| # h[f[:name]] = type.new(f.reject {|k,v| v.nil?}) # end # end # # before(:each) do # allow(described_class).to receive(:instances).and_return(instances) # end # # it 'should prefetch' do # resources.keys.each do |r| # expect(resources[r]).to receive(:provider=).with(described_class) # end # described_class.prefetch(resources) # end # end describe 'create' do it 'creates a realm' do + resource[:browser_flow] = 'browser' temp = Tempfile.new('keycloak_client') allow(Tempfile).to receive(:new).with('keycloak_client').and_return(temp) allow(resource.provider).to receive(:kcadm).with('get', 'client-scopes', 'test', nil, ['id', 'name']).and_return(my_fixture_read('get-scopes.out')) expect(resource.provider).to receive(:kcadm).with('create', 'clients', 'test', temp.path).and_return(my_fixture_read('get-client.out')) expect(resource.provider).to receive(:kcadm).with('delete', 'clients/foo/default-client-scopes/b8ebafcc-485f-44d2-9fe6-f4ed0da80980', 'test') expect(resource.provider).to receive(:kcadm).with('delete', 'clients/foo/default-client-scopes/3e40378d-d26d-471f-b2c7-7a3d9651e588', 'test') expect(resource.provider).to receive(:kcadm).with('delete', 'clients/foo/optional-client-scopes/96f8b56b-7b3a-44cf-82a5-ffbda49271bd', 'test') expect(resource.provider).to receive(:kcadm).with('delete', 'clients/foo/optional-client-scopes/a83d9575-d122-4af1-afb0-10edb851798e', 'test') expect(resource.provider).to receive(:kcadm).with('delete', 'clients/foo/optional-client-scopes/dbd3b1c1-9159-46d9-a879-9602972f1994', 'test') expect(resource.provider).to receive(:kcadm).with('update', 'clients/foo/default-client-scopes/ee85ec64-4853-4fd4-a2f4-ff578016c9b5', 'test') + allow(described_class).to receive(:kcadm).with('get', 'authentication/flows', 'test', nil, ['id', 'alias']).and_return(my_fixture_read('get-flows.out')) resource.provider.create property_hash = resource.provider.instance_variable_get('@property_hash') expect(property_hash[:ensure]).to eq(:present) + f = File.read(temp.path) + data = JSON.parse(f) + expect(data['authenticationFlowBindingOverrides']['browser']).to eq('5a6bbfbb-5096-4d7b-bdcd-819145a10bb1') end end describe 'destroy' do it 'deletes a realm' do hash = resource.to_hash resource.provider.instance_variable_set(:@property_hash, hash) expect(resource.provider).to receive(:kcadm).with('delete', 'clients/foo', 'test') resource.provider.destroy property_hash = resource.provider.instance_variable_get('@property_hash') expect(property_hash).to eq({}) end end describe 'flush' do it 'updates a realm' do hash = resource.to_hash resource.provider.instance_variable_set(:@property_hash, hash) temp = Tempfile.new('keycloak_client') allow(Tempfile).to receive(:new).with('keycloak_client').and_return(temp) expect(resource.provider).to receive(:kcadm).with('update', 'clients/foo', 'test', temp.path) resource.provider.redirect_uris = ['foobar'] resource.provider.flush end it 'updates default_client_scopes' do hash = resource.to_hash resource.provider.instance_variable_set(:@property_hash, hash) temp = Tempfile.new('keycloak_client') allow(Tempfile).to receive(:new).with('keycloak_client').and_return(temp) allow(resource.provider).to receive(:kcadm).with('get', 'client-scopes', 'test', nil, ['id', 'name']).and_return(my_fixture_read('get-scopes.out')) expect(resource.provider).to receive(:kcadm).with('update', 'clients/foo', 'test', temp.path) expect(resource.provider).to receive(:kcadm).with('delete', 'clients/foo/default-client-scopes/ee85ec64-4853-4fd4-a2f4-ff578016c9b5', 'test') expect(resource.provider).to receive(:kcadm).with('update', 'clients/foo/default-client-scopes/openid-connect-clients', 'test') resource.provider.default_client_scopes = ['openid-connect-clients'] resource.provider.redirect_uris = ['foobar'] resource.provider.flush end + + it 'updates flow' do + hash = resource.to_hash + resource.provider.instance_variable_set(:@property_hash, hash) + temp = Tempfile.new('keycloak_client') + allow(Tempfile).to receive(:new).with('keycloak_client').and_return(temp) + allow(described_class).to receive(:kcadm).with('get', 'authentication/flows', 'test', nil, ['id', 'alias']).and_return(my_fixture_read('get-flows.out')) + expect(resource.provider).to receive(:kcadm).with('update', 'clients/foo', 'test', temp.path) + resource.provider.browser_flow = 'browser' + resource.provider.flush + f = File.read(temp.path) + data = JSON.parse(f) + expect(data['authenticationFlowBindingOverrides']['browser']).to eq('5a6bbfbb-5096-4d7b-bdcd-819145a10bb1') + end end end diff --git a/spec/unit/puppet/type/keycloak_client_spec.rb b/spec/unit/puppet/type/keycloak_client_spec.rb index d72cfff..8813dd7 100644 --- a/spec/unit/puppet/type/keycloak_client_spec.rb +++ b/spec/unit/puppet/type/keycloak_client_spec.rb @@ -1,206 +1,221 @@ require 'spec_helper' describe Puppet::Type.type(:keycloak_client) do let(:default_config) do { name: 'foo', realm: 'test', } end let(:config) do default_config end let(:resource) do described_class.new(config) end it 'adds to catalog without raising an error' do catalog = Puppet::Resource::Catalog.new expect { catalog.add_resource resource }.not_to raise_error end it 'has a name' do expect(resource[:name]).to eq('foo') end it 'has client_id default to name' do expect(resource[:client_id]).to eq('foo') end it 'has id default to name' do expect(resource[:id]).to eq('foo') end it 'has realm' do expect(resource[:realm]).to eq('test') end it 'handles componsite name' do component = described_class.new(name: 'foo on test') expect(component[:name]).to eq('foo on test') expect(component[:client_id]).to eq('foo') expect(component[:realm]).to eq('test') end it 'defaults to client_authenticator_type=client-secret' do expect(resource[:client_authenticator_type]).to eq('client-secret') end it 'defaults to protocol=openid-connect' do expect(resource[:protocol]).to eq('openid-connect') end it 'does not allow invalid protocol' do config[:protocol] = 'foo' expect { resource }.to raise_error(%r{foo}) end defaults = { enabled: :true, standard_flow_enabled: :true, implicit_flow_enabled: :false, direct_access_grants_enabled: :true, service_accounts_enabled: :false, public_client: :false, full_scope_allowed: :true, default_client_scopes: [], optional_client_scopes: [], redirect_uris: [], web_origins: [], login_theme: 'absent', + browser_flow: :absent, + direct_grant_flow: :absent, } describe 'basic properties' do # Test basic properties [ :secret, :login_theme, :root_url, :base_url, + :browser_flow, + :direct_grant_flow, ].each do |p| it "should accept a #{p}" do config[p] = 'foo' expect(resource[p]).to eq('foo') end next unless defaults[p] it "should have default for #{p}" do expect(resource[p]).to eq(defaults[p]) end end end describe 'boolean properties' do # Test boolean properties [ :enabled, :standard_flow_enabled, :implicit_flow_enabled, :direct_access_grants_enabled, :service_accounts_enabled, :public_client, :full_scope_allowed, ].each do |p| it "should accept true for #{p}" do config[p] = true expect(resource[p]).to eq(:true) end it "should accept true for #{p} string" do config[p] = 'true' expect(resource[p]).to eq(:true) end it "should accept false for #{p}" do config[p] = false expect(resource[p]).to eq(:false) end it "should accept false for #{p} string" do config[p] = 'false' expect(resource[p]).to eq(:false) end it "should not accept strings for #{p}" do config[p] = 'foo' expect { resource }.to raise_error(%r{foo}) end next unless defaults[p] it "should have default for #{p}" do expect(resource[p]).to eq(defaults[p]) end end end describe 'array properties' do # Array properties [ :default_client_scopes, :optional_client_scopes, :redirect_uris, :web_origins, ].each do |p| it "should accept array for #{p}" do config[p] = ['foo', 'bar'] expect(resource[p]).to eq(['foo', 'bar']) end next unless defaults[p] it "should have default for #{p}" do expect(resource[p]).to eq(defaults[p]) end end end it 'autorequires keycloak_conn_validator' do keycloak_conn_validator = Puppet::Type.type(:keycloak_conn_validator).new(name: 'keycloak') catalog = Puppet::Resource::Catalog.new catalog.add_resource resource catalog.add_resource keycloak_conn_validator rel = resource.autorequire[0] expect(rel.source.ref).to eq(keycloak_conn_validator.ref) expect(rel.target.ref).to eq(resource.ref) end it 'autorequires kcadm-wrapper.sh' do file = Puppet::Type.type(:file).new(name: 'kcadm-wrapper.sh', path: '/opt/keycloak/bin/kcadm-wrapper.sh') catalog = Puppet::Resource::Catalog.new catalog.add_resource resource catalog.add_resource file rel = resource.autorequire[0] expect(rel.source.ref).to eq(file.ref) expect(rel.target.ref).to eq(resource.ref) end it 'autorequires keycloak_realm' do keycloak_realm = Puppet::Type.type(:keycloak_realm).new(name: 'test') catalog = Puppet::Resource::Catalog.new catalog.add_resource resource catalog.add_resource keycloak_realm rel = resource.autorequire[0] expect(rel.source.ref).to eq(keycloak_realm.ref) expect(rel.target.ref).to eq(resource.ref) end it 'autorequires keycloak_client_scope' do config[:default_client_scopes] = ['foo'] keycloak_client_scope = Puppet::Type.type(:keycloak_client_scope).new(name: 'foo', realm: 'test') catalog = Puppet::Resource::Catalog.new catalog.add_resource resource catalog.add_resource keycloak_client_scope rel = resource.autorequire[0] expect(rel.source.ref).to eq(keycloak_client_scope.ref) expect(rel.target.ref).to eq(resource.ref) end it 'autorequires client_scope protocol mappers' do config[:default_client_scopes] = ['foo'] keycloak_protocol_mapper = Puppet::Type.type(:keycloak_protocol_mapper).new(name: 'bar', realm: 'test', client_scope: 'foo') catalog = Puppet::Resource::Catalog.new catalog.add_resource resource catalog.add_resource keycloak_protocol_mapper rel = resource.autorequire[0] expect(rel.source.ref).to eq(keycloak_protocol_mapper.ref) expect(rel.target.ref).to eq(resource.ref) end + + it 'autorequires browser flow' do + config[:browser_flow] = 'foo' + flow = Puppet::Type.type(:keycloak_flow).new(name: 'foo', realm: 'test') + catalog = Puppet::Resource::Catalog.new + catalog.add_resource resource + catalog.add_resource flow + rel = resource.autorequire[0] + expect(rel.source.ref).to eq(flow.ref) + expect(rel.target.ref).to eq(resource.ref) + end end