diff --git a/CHANGELOG.md b/CHANGELOG.md --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,40 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org). +## [v6.19.0](https://github.com/treydock/puppet-module-keycloak/tree/v6.19.0) (2020-10-07) + +[Full Changelog](https://github.com/treydock/puppet-module-keycloak/compare/v6.18.0...v6.19.0) + +### Added + +- Enable roles management at realm and client level [\#164](https://github.com/treydock/puppet-module-keycloak/pull/164) ([anlambert](https://github.com/anlambert)) +- Add more realm login related properties [\#163](https://github.com/treydock/puppet-module-keycloak/pull/163) ([anlambert](https://github.com/anlambert)) + +## [v6.18.0](https://github.com/treydock/puppet-module-keycloak/tree/v6.18.0) (2020-09-25) + +[Full Changelog](https://github.com/treydock/puppet-module-keycloak/compare/v6.17.0...v6.18.0) + +### Added + +- Support flow overrides on clients [\#161](https://github.com/treydock/puppet-module-keycloak/pull/161) ([treydock](https://github.com/treydock)) +- Add registration\_allowed to keycloak\_realm [\#160](https://github.com/treydock/puppet-module-keycloak/pull/160) ([anlambert](https://github.com/anlambert)) +- Have realms and identity providers auto require their configured flows [\#159](https://github.com/treydock/puppet-module-keycloak/pull/159) ([treydock](https://github.com/treydock)) + +### Fixed + +- Realm can not depend on flow that depends on realm [\#162](https://github.com/treydock/puppet-module-keycloak/pull/162) ([treydock](https://github.com/treydock)) + +## [v6.17.0](https://github.com/treydock/puppet-module-keycloak/tree/v6.17.0) (2020-09-24) + +[Full Changelog](https://github.com/treydock/puppet-module-keycloak/compare/v6.16.0...v6.17.0) + +### Added + +- Improved unit and acceptance tests for recent changes [\#158](https://github.com/treydock/puppet-module-keycloak/pull/158) ([treydock](https://github.com/treydock)) +- add bruteForceProtected [\#157](https://github.com/treydock/puppet-module-keycloak/pull/157) ([aba-rechsteiner](https://github.com/aba-rechsteiner)) +- add trustEmail [\#156](https://github.com/treydock/puppet-module-keycloak/pull/156) ([aba-rechsteiner](https://github.com/aba-rechsteiner)) +- add keycloak-oidc providerid and other new parameters [\#155](https://github.com/treydock/puppet-module-keycloak/pull/155) ([aba-rechsteiner](https://github.com/aba-rechsteiner)) + ## [v6.16.0](https://github.com/treydock/puppet-module-keycloak/tree/v6.16.0) (2020-08-21) [Full Changelog](https://github.com/treydock/puppet-module-keycloak/compare/v6.15.0...v6.16.0) @@ -465,9 +499,7 @@ - Use puppet-strings for documentation [\#30](https://github.com/treydock/puppet-module-keycloak/pull/30) ([treydock](https://github.com/treydock)) - Add search\_scope and custom\_user\_search\_filter properties to keycloak\_ldap\_user\_provider type [\#29](https://github.com/treydock/puppet-module-keycloak/pull/29) ([treydock](https://github.com/treydock)) -- Explicitly define all type properties [\#27](https://github.com/treydock/puppet-module-keycloak/pull/27) ([treydock](https://github.com/treydock)) - Improve acceptance tests [\#26](https://github.com/treydock/puppet-module-keycloak/pull/26) ([treydock](https://github.com/treydock)) -- Add keycloak\_api configuration type [\#22](https://github.com/treydock/puppet-module-keycloak/pull/22) ([treydock](https://github.com/treydock)) ### Fixed @@ -479,12 +511,17 @@ ### Added +- Explicitly define all type properties [\#27](https://github.com/treydock/puppet-module-keycloak/pull/27) ([treydock](https://github.com/treydock)) - Support setting auth\_type=simple related properties for keycloak\_ldap\_user\_provider type [\#24](https://github.com/treydock/puppet-module-keycloak/pull/24) ([treydock](https://github.com/treydock)) ## [2.4.0](https://github.com/treydock/puppet-module-keycloak/tree/2.4.0) (2018-06-04) [Full Changelog](https://github.com/treydock/puppet-module-keycloak/compare/2.3.1...2.4.0) +### Added + +- Add keycloak\_api configuration type [\#22](https://github.com/treydock/puppet-module-keycloak/pull/22) ([treydock](https://github.com/treydock)) + ## [2.3.1](https://github.com/treydock/puppet-module-keycloak/tree/2.3.1) (2018-03-10) [Full Changelog](https://github.com/treydock/puppet-module-keycloak/compare/2.3.0...2.3.1) diff --git a/REFERENCE.md b/REFERENCE.md --- a/REFERENCE.md +++ b/REFERENCE.md @@ -1242,6 +1242,24 @@ access.token.lifespan +##### `browser_flow` + +authenticationFlowBindingOverrides.browser (Use flow alias, not ID) + +Default value: absent + +##### `direct_grant_flow` + +authenticationFlowBindingOverrides.direct_grant (Use flow alias, not ID) + +Default value: absent + +##### `roles` + +roles + +Default value: [] + #### Parameters The following parameters are available in the `keycloak_client` type. @@ -1854,6 +1872,10 @@ Default value: false +##### `gui_order` + +guiOrder + ##### `first_broker_login_flow_alias` firstBrokerLoginFlowAlias @@ -1864,6 +1886,14 @@ postBrokerLoginFlowAlias +##### `sync_mode` + +Valid values: IMPORT, LEGACY, FORCE + +syncMode + +Default value: IMPORT + ##### `hide_on_login_page` Valid values: `true`, `false` @@ -1928,6 +1958,10 @@ Default value: true +##### `jwks_url` + +jwksUrl + ##### `login_hint` Valid values: `true`, `false` @@ -1998,7 +2032,7 @@ ##### `provider_id` -Valid values: oidc +Valid values: oidc, keycloak-oidc providerId @@ -2340,6 +2374,26 @@ Default value: absent +##### `trust_email` + +Valid values: `true`, `false` + +trustEmail + +Default value: false + +##### `full_sync_period` + +fullSyncPeriod + +Default value: -1 + +##### `changed_sync_period` + +changedSyncPeriod + +Default value: -1 + #### Parameters The following parameters are available in the `keycloak_ldap_user_provider` type. @@ -2605,6 +2659,14 @@ Default value: false +##### `registration_allowed` + +Valid values: `true`, `false` + +registrationAllowed + +Default value: false + ##### `login_with_email_allowed` Valid values: `true`, `false` @@ -2613,6 +2675,22 @@ Default value: true +##### `reset_password_allowed` + +Valid values: `true`, `false` + +resetPasswordAllowed + +Default value: false + +##### `verify_email` + +Valid values: `true`, `false` + +verifyEmail + +Default value: false + ##### `browser_flow` browserFlow @@ -2755,6 +2833,18 @@ smtpServer replyToDisplayName +##### `brute_force_protected` + +Valid values: `true`, `false` + +bruteForceProtected + +##### `roles` + +roles + +Default value: ['offline_access', 'uma_authorization'] + #### Parameters The following parameters are available in the `keycloak_realm` type. diff --git a/lib/puppet/provider/keycloak_client/kcadm.rb b/lib/puppet/provider/keycloak_client/kcadm.rb --- a/lib/puppet/provider/keycloak_client/kcadm.rb +++ b/lib/puppet/provider/keycloak_client/kcadm.rb @@ -18,6 +18,17 @@ ] 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('_', '.') @@ -26,6 +37,26 @@ end end + def self.get_client_roles(realm, client) + output = kcadm('get', "clients/#{client}/roles", realm) + Puppet.debug("Client #{client} in realm #{realm} roles: #{output}") + data = JSON.parse(output) + roles = [] + data.each do |d| + # filter out 'uma_protection' client role created when + # authorization_services_enabled property is set to true + if !d['composite'] && d['name'] != 'uma_protection' + roles.push(d['name']) + end + end + Puppet.debug("Returned client roles: #{roles}") + roles + end + + def get_client_roles(*args) + self.class.get_client_roles(*args) + end + def self.instances clients = [] realms.each do |realm| @@ -61,10 +92,12 @@ client[:realm] = realm client[:name] = "#{client[:client_id]} on #{client[:realm]}" type_properties.each do |property| + next if [:roles].include?(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) @@ -73,6 +106,9 @@ 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 @@ -81,6 +117,7 @@ end # The absence of a value should be 'absent' client[:login_theme] = 'absent' if client[:login_theme].nil? + client[:roles] = get_client_roles(realm, client[:id]) clients << new(client) end end @@ -113,6 +150,32 @@ @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? @@ -121,7 +184,7 @@ 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 if [:default_client_scopes, :optional_client_scopes, :roles].include?(property) next unless resource[property.to_sym] value = convert_property_value(resource[property.to_sym]) next if value == 'absent' || value == :absent || value.nil? @@ -130,6 +193,12 @@ 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 @@ -196,6 +265,33 @@ raise Puppet::Error, "kcadm update clients/#{resource[:id]}/optional-client-scopes/#{scope_id}: #{e.message}" end end + role = nil + if resource[:roles] + roles = get_client_roles(resource[:realm], resource[:id]) + remove_roles = roles - resource[:roles] + begin + remove_roles.each do |s| + role = s + kcadm('delete', "clients/#{resource[:id]}/roles/#{role}", resource[:realm]) + end + rescue Puppet::ExecutionFailure => e + raise Puppet::Error, "kcadm delete realms/#{resource[:realm]}/clients/#{resource[:id]}/roles/#{role}: #{e.message}" + end + add_roles = resource[:roles] - roles + begin + add_roles.each do |s| + role = s + role_data = { 'description' => "${role_#{role}}", 'name' => role } + role_data_t = Tempfile.new('keycloak_client_role') + role_data_t.write(JSON.pretty_generate(role_data)) + role_data_t.close + Puppet.debug(IO.read(role_data_t.path)) + kcadm('create', "clients/#{resource[:id]}/roles", resource[:realm], role_data_t.path) + end + rescue Puppet::ExecutionFailure => e + raise Puppet::Error, "kcadm create realms/#{resource[:realm]}/clients/#{resource[:id]}/roles/#{role}: #{e.message}" + end + end @property_hash[:ensure] = :present end @@ -231,8 +327,9 @@ data = {} data[:clientId] = resource[:client_id] + data[:authenticationFlowBindingOverrides] = {} type_properties.each do |property| - next if [:default_client_scopes, :optional_client_scopes].include?(property) + next if [:default_client_scopes, :optional_client_scopes, :roles].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' @@ -241,6 +338,9 @@ 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 @@ -314,6 +414,32 @@ raise Puppet::Error, "kcadm update clients/#{id}/optional-client-scopes/#{scope_id}: #{e.message}" end end + role = nil + if @property_flush[:roles] + remove_roles = @property_hash[:roles] - @property_flush[:roles] + begin + remove_roles.each do |s| + role = s + kcadm('delete', "clients/#{resource[:id]}/roles/#{role}", resource[:realm]) + end + rescue Puppet::ExecutionFailure => e + raise Puppet::Error, "kcadm delete realms/#{resource[:realm]}/clients/#{resource[:id]}/roles/#{role}: #{e.message}" + end + add_roles = @property_flush[:roles] - @property_hash[:roles] + begin + add_roles.each do |s| + role = s + role_data = { 'description' => "${role_#{role}}", 'name' => role } + role_data_t = Tempfile.new('keycloak_client_role') + role_data_t.write(JSON.pretty_generate(role_data)) + role_data_t.close + Puppet.debug(IO.read(role_data_t.path)) + kcadm('create', "clients/#{resource[:id]}/roles", resource[:realm], role_data_t.path) + end + rescue Puppet::ExecutionFailure => e + raise Puppet::Error, "kcadm create realms/#{resource[:realm]}/clients/#{resource[:id]}/roles/#{role}: #{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). diff --git a/lib/puppet/provider/keycloak_realm/kcadm.rb b/lib/puppet/provider/keycloak_realm/kcadm.rb --- a/lib/puppet/provider/keycloak_realm/kcadm.rb +++ b/lib/puppet/provider/keycloak_realm/kcadm.rb @@ -55,6 +55,25 @@ self.class.get_client_scopes(*args) end + def self.get_realm_roles(realm) + output = kcadm('get', 'roles', realm) + Puppet.debug("Realms #{realm} roles: #{output}") + data = JSON.parse(output) + roles = [] + data.each do |d| + # filter out 'create-realm' role from master realm as it should not be removed + if !d['composite'] && d['name'] != 'create-realm' + roles.push(d['name']) + end + end + Puppet.debug("Returned roles: #{roles}") + roles + end + + def get_realm_roles(*args) + self.class.get_realm_roles(*args) + end + def self.get_events_config(realm) output = kcadm('get', 'events/config', realm) Puppet.debug("#{realm} events/config: #{output}") @@ -97,7 +116,7 @@ realm[:name] = d['realm'] events_config = get_events_config(d['realm']) type_properties.each do |property| - next if [:default_client_scopes, :optional_client_scopes].include?(property) + next if [:default_client_scopes, :optional_client_scopes, :roles].include?(property) value = if property.to_s =~ %r{events} events_config[camelize(property)] elsif browser_security_headers.include?(property) @@ -116,6 +135,7 @@ realm[:default_client_scopes] = default_scopes.keys.map { |k| k.to_s } optional_scopes = get_client_scopes(realm[:name], 'optional') realm[:optional_client_scopes] = optional_scopes.keys.map { |k| k.to_s } + realm[:roles] = get_realm_roles(realm[:name]) realms << new(realm) end realms @@ -138,7 +158,7 @@ data[:realm] = resource[:name] type_properties.each do |property| next if flow_properties.include?(property) - next if [:default_client_scopes, :optional_client_scopes].include?(property) + next if [:default_client_scopes, :optional_client_scopes, :roles].include?(property) if self.class.browser_security_headers.include?(property) && !data.key?('browserSecurityHeaders') data['browserSecurityHeaders'] = {} end @@ -226,6 +246,33 @@ raise Puppet::Error, "kcadm update realms/#{resource[:name]}/default-optional-client-scopes/#{scope_id}: #{e.message}" end end + role = nil + if resource[:roles] + roles = get_realm_roles(resource[:name]) + remove_roles = roles - resource[:roles] + begin + remove_roles.each do |s| + role = s + kcadm('delete', "roles/#{role}", resource[:name]) + end + rescue Puppet::ExecutionFailure => e + raise Puppet::Error, "kcadm delete realms/#{resource[:name]}/roles/#{role}: #{e.message}" + end + add_roles = resource[:roles] - roles + begin + add_roles.each do |s| + role = s + role_data = { 'description' => "${role_#{role}}", 'name' => role } + role_data_t = Tempfile.new('keycloak_realm_role') + role_data_t.write(JSON.pretty_generate(role_data)) + role_data_t.close + Puppet.debug(IO.read(role_data_t.path)) + kcadm('create', 'roles', resource[:name], role_data_t.path) + end + rescue Puppet::ExecutionFailure => e + raise Puppet::Error, "kcadm create realms/#{resource[:name]}/roles/#{role}: #{e.message}" + end + end unless events_config.empty? events_config_t = Tempfile.new('keycloak_events_config') events_config_t.write(JSON.pretty_generate(events_config)) @@ -270,7 +317,7 @@ data = {} events_config = {} type_properties.each do |property| - next if [:default_client_scopes, :optional_client_scopes].include?(property) + next if [:default_client_scopes, :optional_client_scopes, :roles].include?(property) if flow_properties.include?(property) && !available_flows(resource[:name]).include?(resource[property.to_sym]) Puppet.warning("Keycloak_realm[#{resource[:name]}]: #{property} '#{resource[property.to_sym]}' does not exist, skipping") next @@ -365,6 +412,32 @@ raise Puppet::Error, "kcadm update realms/#{resource[:name]}/default-optional-client-scopes/#{scope_id}: #{e.message}" end end + role = nil + if @property_flush[:roles] + remove_roles = @property_hash[:roles] - @property_flush[:roles] + begin + remove_roles.each do |s| + role = s + kcadm('delete', "roles/#{role}", resource[:name]) + end + rescue Puppet::ExecutionFailure => e + raise Puppet::Error, "kcadm delete realms/#{resource[:name]}/roles/#{role}: #{e.message}" + end + add_roles = @property_flush[:roles] - @property_hash[:roles] + begin + add_roles.each do |s| + role = s + role_data = { 'description' => "${role_#{role}}", 'name' => role } + role_data_t = Tempfile.new('keycloak_realm_role') + role_data_t.write(JSON.pretty_generate(role_data)) + role_data_t.close + Puppet.debug(IO.read(role_data_t.path)) + kcadm('create', 'roles', resource[:name], role_data_t.path) + end + rescue Puppet::ExecutionFailure => e + raise Puppet::Error, "kcadm create realms/#{resource[:name]}/roles/#{role}: #{e.message}" + end + end unless events_config.empty? events_config_t = Tempfile.new('keycloak_events_config') events_config_t.write(JSON.pretty_generate(events_config)) diff --git a/lib/puppet/type/keycloak_client.rb b/lib/puppet/type/keycloak_client.rb --- a/lib/puppet/type/keycloak_client.rb +++ b/lib/puppet/type/keycloak_client.rb @@ -175,6 +175,21 @@ 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 + + newproperty(:roles, array_matching: :all, parent: PuppetX::Keycloak::ArrayProperty) do + desc 'roles' + defaultto [] + end + autorequire(:keycloak_client_scope) do requires = [] catalog.resources.each do |resource| @@ -203,6 +218,21 @@ 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" diff --git a/lib/puppet/type/keycloak_identity_provider.rb b/lib/puppet/type/keycloak_identity_provider.rb --- a/lib/puppet/type/keycloak_identity_provider.rb +++ b/lib/puppet/type/keycloak_identity_provider.rb @@ -1,5 +1,6 @@ require_relative '../../puppet_x/keycloak/type' require_relative '../../puppet_x/keycloak/array_property' +require_relative '../../puppet_x/keycloak/integer_property' Puppet::Type.newtype(:keycloak_identity_provider) do desc <<-DESC @@ -51,7 +52,7 @@ newparam(:provider_id) do desc 'providerId' - newvalues('oidc') + newvalues('oidc', 'keycloak-oidc') defaultto 'oidc' munge { |v| v } end @@ -99,6 +100,11 @@ defaultto :false end + newproperty(:gui_order, parent: PuppetX::Keycloak::IntegerProperty) do + desc 'guiOrder' + munge { |v| v.to_s } + end + newproperty(:first_broker_login_flow_alias) do desc 'firstBrokerLoginFlowAlias' defaultto 'first broker login' @@ -110,6 +116,13 @@ munge { |v| v } end + newproperty(:sync_mode) do + desc 'syncMode' + defaultto 'IMPORT' + newvalues('IMPORT', 'LEGACY', 'FORCE') + munge { |v| v } + end + # BEGIN: oidc newproperty(:hide_on_login_page, boolean: true) do @@ -191,6 +204,11 @@ defaultto :true end + newproperty(:jwks_url) do + desc 'jwksUrl' + munge { |v| v } + end + newproperty(:login_hint, boolean: true) do desc 'loginHint' newvalues(:true, :false) @@ -258,7 +276,7 @@ if self[:realm].nil? raise Puppet::Error, 'realm is required' end - if self[:ensure].to_s == 'present' && self[:provider_id] == 'oidc' + if self[:ensure].to_s == 'present' && ['oidc', 'keycloak-oidc'].include?(self[:provider_id]) if self[:authorization_url].nil? raise Puppet::Error, 'authorization_url is required' end @@ -273,4 +291,19 @@ end end 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[:first_broker_login_flow_alias] == resource[:alias] + requires << resource.name + end + if self[:post_broker_login_flow_alias] == resource[:alias] + requires << resource.name + end + end + requires + end end diff --git a/lib/puppet/type/keycloak_ldap_user_provider.rb b/lib/puppet/type/keycloak_ldap_user_provider.rb --- a/lib/puppet/type/keycloak_ldap_user_provider.rb +++ b/lib/puppet/type/keycloak_ldap_user_provider.rb @@ -203,6 +203,24 @@ ] end + newproperty(:trust_email, boolean: true) do + desc 'trustEmail' + newvalues(:true, :false) + defaultto :false + end + + newproperty(:full_sync_period, parent: PuppetX::Keycloak::IntegerProperty) do + desc 'fullSyncPeriod' + defaultto '-1' + munge { |v| v.to_s } + end + + newproperty(:changed_sync_period, parent: PuppetX::Keycloak::IntegerProperty) do + desc 'changedSyncPeriod' + defaultto '-1' + munge { |v| v.to_s } + end + validate do if self[:use_kerberos_for_password_authentication] && self[:auth_type] == 'none' raise Puppet::Error, 'use_kerberos_for_password_authentication is not valid for auth_type none' diff --git a/lib/puppet/type/keycloak_realm.rb b/lib/puppet/type/keycloak_realm.rb --- a/lib/puppet/type/keycloak_realm.rb +++ b/lib/puppet/type/keycloak_realm.rb @@ -100,12 +100,30 @@ defaultto :false end + newproperty(:registration_allowed, boolean: true) do + desc 'registrationAllowed' + newvalues(:true, :false) + defaultto :false + end + newproperty(:login_with_email_allowed, boolean: true) do desc 'loginWithEmailAllowed' newvalues(:true, :false) defaultto :true end + newproperty(:reset_password_allowed, boolean: true) do + desc 'resetPasswordAllowed' + newvalues(:true, :false) + defaultto :false + end + + newproperty(:verify_email, boolean: true) do + desc 'verifyEmail' + newvalues(:true, :false) + defaultto :false + end + newproperty(:browser_flow) do desc 'browserFlow' defaultto('browser') @@ -250,4 +268,14 @@ newproperty(:smtp_server_reply_to_display_name) do desc 'smtpServer replyToDisplayName' end + + newproperty(:brute_force_protected, boolean: true) do + desc 'bruteForceProtected' + newvalues(:true, :false) + end + + newproperty(:roles, array_matching: :all, parent: PuppetX::Keycloak::ArrayProperty) do + desc 'roles' + defaultto ['offline_access', 'uma_authorization'] + end end diff --git a/metadata.json b/metadata.json --- a/metadata.json +++ b/metadata.json @@ -1,6 +1,6 @@ { "name": "treydock-keycloak", - "version": "6.16.0", + "version": "6.19.0", "author": "treydock", "summary": "Keycloak Puppet module", "license": "Apache-2.0", diff --git a/spec/acceptance/2_realm_spec.rb b/spec/acceptance/2_realm_spec.rb --- a/spec/acceptance/2_realm_spec.rb +++ b/spec/acceptance/2_realm_spec.rb @@ -21,6 +21,8 @@ smtp_server_from_display_name => 'Keycloak', smtp_server_reply_to => 'webmaster@example.org', smtp_server_reply_to_display_name => 'Webmaster', + brute_force_protected => false, + roles => ['offline_access', 'uma_authorization', 'new_role'], } EOS @@ -32,6 +34,10 @@ on hosts, '/opt/keycloak/bin/kcadm-wrapper.sh get realms/test' do data = JSON.parse(stdout) expect(data['id']).to eq('test') + expect(data['bruteForceProtected']).to eq(false) + expect(data['registrationAllowed']).to eq(false) + expect(data['resetPasswordAllowed']).to eq(false) + expect(data['verifyEmail']).to eq(false) end end @@ -81,6 +87,20 @@ expect(data['smtpServer']['replyToDisplayName']).to eq('Webmaster') end end + + it 'has correct roles settings' do + on hosts, '/opt/keycloak/bin/kcadm-wrapper.sh get roles -r test' do + data = JSON.parse(stdout) + expected_roles = ['new_role', 'offline_access', 'uma_authorization'] + realm_roles = [] + data.each do |d| + unless d['composite'] + realm_roles.push(d['name']) + end + end + expect(expected_roles - realm_roles).to eq([]) + end + end end context 'updates realm' do @@ -93,6 +113,9 @@ keycloak_realm { 'test': ensure => 'present', remember_me => true, + registration_allowed => true, + reset_password_allowed => true, + verify_email => true, access_code_lifespan => 3600, access_token_lifespan => 3600, sso_session_idle_timeout => 3600, @@ -114,7 +137,8 @@ smtp_server_from_display_name => 'Keycloak', smtp_server_reply_to => 'webmaster@example.org', smtp_server_reply_to_display_name => 'Hostmaster', - + brute_force_protected => true, + roles => ['uma_authorization', 'new_role', 'other_new_role'], } EOS @@ -126,6 +150,9 @@ on hosts, '/opt/keycloak/bin/kcadm-wrapper.sh get realms/test' do data = JSON.parse(stdout) expect(data['rememberMe']).to eq(true) + expect(data['registrationAllowed']).to eq(true) + expect(data['resetPasswordAllowed']).to eq(true) + expect(data['verifyEmail']).to eq(true) expect(data['accessCodeLifespan']).to eq(3600) expect(data['accessTokenLifespan']).to eq(3600) expect(data['ssoSessionIdleTimeout']).to eq(3600) @@ -141,6 +168,7 @@ expect(data['smtpServer']['fromDisplayName']).to eq('Keycloak') expect(data['smtpServer']['replyTo']).to eq('webmaster@example.org') expect(data['smtpServer']['replyToDisplayName']).to eq('Hostmaster') + expect(data['bruteForceProtected']).to eq(true) end end @@ -162,6 +190,20 @@ expect(data['adminEventsDetailsEnabled']).to eq(true) end end + + it 'has updated roles settings' do + on hosts, '/opt/keycloak/bin/kcadm-wrapper.sh get roles -r test' do + data = JSON.parse(stdout) + expected_roles = ['new_role', 'other_new_role', 'uma_authorization'] + realm_roles = [] + data.each do |d| + unless d['composite'] + realm_roles.push(d['name']) + end + end + expect(expected_roles - realm_roles).to eq([]) + end + end end context 'creates realm with invalid browser flow' do diff --git a/spec/acceptance/3_ldap_spec.rb b/spec/acceptance/3_ldap_spec.rb --- a/spec/acceptance/3_ldap_spec.rb +++ b/spec/acceptance/3_ldap_spec.rb @@ -49,6 +49,9 @@ expect(data['config']['usersDn']).to eq(['ou=People,dc=test']) expect(data['config']['connectionUrl']).to eq(['ldap://localhost:389']) expect(data['config']['customUserSearchFilter']).to eq(['(objectClass=posixAccount)']) + expect(data['config']['trustEmail']).to eq(['false']) + expect(data['config']['fullSyncPeriod']).to eq(['-1']) + expect(data['config']['changedSyncPeriod']).to eq(['-1']) end end @@ -105,6 +108,9 @@ users_dn => 'ou=People,dc=test', connection_url => 'ldap://localhost:389', user_object_classes => ['posixAccount'], + trust_email => true, + full_sync_period => 60, + changed_sync_period => 30, } keycloak_ldap_mapper { 'full-name': realm => 'test', @@ -135,6 +141,9 @@ expect(data['config']['connectionUrl']).to eq(['ldap://localhost:389']) expect(data['config']['userObjectClasses']).to eq(['posixAccount']) expect(data['config'].key?('customUserSearchFilter')).to eq(false) + expect(data['config']['trustEmail']).to eq(['true']) + expect(data['config']['fullSyncPeriod']).to eq(['60']) + expect(data['config']['changedSyncPeriod']).to eq(['30']) end end diff --git a/spec/acceptance/5_client_spec.rb b/spec/acceptance/5_client_spec.rb --- a/spec/acceptance/5_client_spec.rb +++ b/spec/acceptance/5_client_spec.rb @@ -9,6 +9,7 @@ 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', @@ -18,6 +19,19 @@ login_theme => 'keycloak', authorization_services_enabled => false, service_accounts_enabled => true, + browser_flow => 'foo', + roles => ['bar_role', 'other_bar_role'], + } + 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 @@ -36,6 +50,14 @@ 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 @@ -45,6 +67,27 @@ expect(data['value']).to eq('foobar') end end + + it 'has updated roles settings for client' do + on hosts, '/opt/keycloak/bin/kcadm-wrapper.sh get clients/test.foo.bar/roles -r test' do + data = JSON.parse(stdout) + expected_roles = ['bar_role', 'other_bar_role'] + client_roles = [] + data.each do |d| + unless d['composite'] + client_roles.push(d['name']) + end + end + expect(expected_roles - client_roles).to eq([]) + end + end + + it 'has not updated roles settings for client2' do + on hosts, '/opt/keycloak/bin/kcadm-wrapper.sh get clients/test.foo.baz/roles -r test' do + data = JSON.parse(stdout) + expect(data).to eq([]) + end + end end context 'updates client' do @@ -63,6 +106,19 @@ secret => 'foobar', authorization_services_enabled => true, service_accounts_enabled => true, + roles => ['bar_role'], + } + 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', + roles => ['baz_role'], } EOS @@ -81,6 +137,24 @@ 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 @@ -90,5 +164,33 @@ expect(data['value']).to eq('foobar') end end + + it 'has updated client roles settings' do + on hosts, '/opt/keycloak/bin/kcadm-wrapper.sh get clients/test.foo.bar/roles -r test' do + data = JSON.parse(stdout) + expected_roles = ['bar_role'] + client_roles = [] + data.each do |d| + unless d['composite'] + client_roles.push(d['name']) + end + end + expect(expected_roles - client_roles).to eq([]) + end + end + + it 'has updated client2 roles settings' do + on hosts, '/opt/keycloak/bin/kcadm-wrapper.sh get clients/test.foo.baz/roles -r test' do + data = JSON.parse(stdout) + expected_roles = ['baz_role'] + client_roles = [] + data.each do |d| + unless d['composite'] + client_roles.push(d['name']) + end + end + expect(expected_roles - client_roles).to eq([]) + end + end end end diff --git a/spec/acceptance/8_identity_provider_spec.rb b/spec/acceptance/8_identity_provider_spec.rb --- a/spec/acceptance/8_identity_provider_spec.rb +++ b/spec/acceptance/8_identity_provider_spec.rb @@ -19,6 +19,20 @@ user_info_url => 'https://cilogon.org/oauth2/userinfo', token_url => 'https://cilogon.org/oauth2/token', authorization_url => 'https://cilogon.org/authorize', + jwks_url => 'https://cilogon.org/jwks', + gui_order => 1, + } + keycloak_identity_provider { 'foo on test': + ensure => 'present', + display_name => 'foo', + provider_id => 'keycloak-oidc', + first_broker_login_flow_alias => 'browser', + client_id => 'foobar', + client_secret => 'supersecret', + user_info_url => 'https://foo/oauth2/userinfo', + token_url => 'https://foo/oauth2/token', + authorization_url => 'https://foo/authorize', + gui_order => 2, } EOS @@ -31,6 +45,23 @@ data = JSON.parse(stdout) expect(data['enabled']).to eq(true) expect(data['displayName']).to eq('CILogon') + expect(data['providerId']).to eq('oidc') + expect(data['config']['jwksUrl']).to eq('https://cilogon.org/jwks') + expect(data['config']['guiOrder']).to eq('1') + expect(data['config']['syncMode']).to eq('IMPORT') + end + end + + it 'has created keycloak-oidc identity provider' do + on hosts, '/opt/keycloak/bin/kcadm-wrapper.sh get identity-provider/instances/foo -r test' do + data = JSON.parse(stdout) + expect(data['enabled']).to eq(true) + expect(data['displayName']).to eq('foo') + expect(data['providerId']).to eq('keycloak-oidc') + expect(data['config']['userInfoUrl']).to eq('https://foo/oauth2/userinfo') + expect(data['config']['tokenUrl']).to eq('https://foo/oauth2/token') + expect(data['config']['authorizationUrl']).to eq('https://foo/authorize') + expect(data['config']['guiOrder']).to eq('2') end end end @@ -53,6 +84,21 @@ user_info_url => 'https://cilogon.org/oauth2/userinfo', token_url => 'https://cilogon.org/oauth2/token', authorization_url => 'https://cilogon.org/authorize', + jwks_url => 'https://cilogon.org/jwks', + gui_order => 3, + sync_mode => 'FORCE', + } + keycloak_identity_provider { 'foo on test': + ensure => 'present', + display_name => 'foo', + provider_id => 'keycloak-oidc', + first_broker_login_flow_alias => 'browser', + client_id => 'foobar', + client_secret => 'supersecret', + user_info_url => 'https://foo/userinfo', + token_url => 'https://foo/token', + authorization_url => 'https://foo/authorize', + gui_order => 4, } EOS @@ -64,7 +110,25 @@ on hosts, '/opt/keycloak/bin/kcadm-wrapper.sh get identity-provider/instances/cilogon -r test' do data = JSON.parse(stdout) expect(data['enabled']).to eq(true) + expect(data['displayName']).to eq('CILogon') + expect(data['providerId']).to eq('oidc') + expect(data['config']['jwksUrl']).to eq('https://cilogon.org/jwks') expect(data['firstBrokerLoginFlowAlias']).to eq('first broker login') + expect(data['config']['guiOrder']).to eq('3') + expect(data['config']['syncMode']).to eq('FORCE') + end + end + + it 'has created keycloak-oidc identity provider' do + on hosts, '/opt/keycloak/bin/kcadm-wrapper.sh get identity-provider/instances/foo -r test' do + data = JSON.parse(stdout) + expect(data['enabled']).to eq(true) + expect(data['displayName']).to eq('foo') + expect(data['providerId']).to eq('keycloak-oidc') + expect(data['config']['userInfoUrl']).to eq('https://foo/userinfo') + expect(data['config']['tokenUrl']).to eq('https://foo/token') + expect(data['config']['authorizationUrl']).to eq('https://foo/authorize') + expect(data['config']['guiOrder']).to eq('4') 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 --- /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 --- a/spec/fixtures/unit/puppet/provider/keycloak_client/kcadm/get-test.out +++ b/spec/fixtures/unit/puppet/provider/keycloak_client/kcadm/get-test.out @@ -22,7 +22,10 @@ "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", diff --git a/spec/unit/puppet/provider/keycloak_client/kcadm_spec.rb b/spec/unit/puppet/provider/keycloak_client/kcadm_spec.rb --- a/spec/unit/puppet/provider/keycloak_client/kcadm_spec.rb +++ b/spec/unit/puppet/provider/keycloak_client/kcadm_spec.rb @@ -16,6 +16,16 @@ 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')) + master_clients = JSON.parse(my_fixture_read('get-master.out')) + test_clients = JSON.parse(my_fixture_read('get-test.out')) + master_clients.each do |c| + allow(described_class).to receive(:get_client_roles).with('master', c['id']).and_return([]) + end + test_clients.each do |c| + allow(described_class).to receive(:get_client_roles).with('test', c['id']).and_return([]) + end expect(described_class.instances.length).to eq(7 + 6) end @@ -24,8 +34,20 @@ 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')) + master_clients = JSON.parse(my_fixture_read('get-master.out')) + test_clients = JSON.parse(my_fixture_read('get-test.out')) + master_clients.each do |c| + allow(described_class).to receive(:get_client_roles).with('master', c['id']).and_return([]) + end + test_clients.each do |c| + allow(described_class).to receive(:get_client_roles).with('test', c['id']).and_return([]) + end 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') + expect(property_hash[:roles]).to eq([]) end end # describe 'self.prefetch' do @@ -50,10 +72,14 @@ # end # end describe 'create' do - it 'creates a realm' do + it 'creates a client' do + resource[:browser_flow] = 'browser' + resource[:roles] = ['foo_role'] temp = Tempfile.new('keycloak_client') + rtemp = Tempfile.new('keycloak_client_role') 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')) + allow(described_class).to receive(:get_client_roles).with('test', 'foo').and_return([]) 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') @@ -61,14 +87,20 @@ 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')) + allow(Tempfile).to receive(:new).with('keycloak_client_role').and_return(rtemp) + expect(resource.provider).to receive(:kcadm).with('create', 'clients/foo/roles', 'test', rtemp.path) 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 + it 'deletes a client' do hash = resource.to_hash resource.provider.instance_variable_set(:@property_hash, hash) expect(resource.provider).to receive(:kcadm).with('delete', 'clients/foo', 'test') @@ -79,13 +111,21 @@ end describe 'flush' do - it 'updates a realm' do + it 'updates a client' do hash = resource.to_hash resource.provider.instance_variable_set(:@property_hash, hash) temp = Tempfile.new('keycloak_client') + rtemp = Tempfile.new('keycloak_client_role') allow(Tempfile).to receive(:new).with('keycloak_client').and_return(temp) + allow(Tempfile).to receive(:new).with('keycloak_client_role').and_return(rtemp) + allow(described_class).to receive(:get_client_roles).with('test', 'foo').and_return(['foo_role']) expect(resource.provider).to receive(:kcadm).with('update', 'clients/foo', 'test', temp.path) + expect(resource.provider).to receive(:kcadm).with('delete', 'clients/foo/roles/foo_role', 'test') + expect(resource.provider).to receive(:kcadm).with('create', 'clients/foo/roles', 'test', rtemp.path) + property_hash = resource.provider.instance_variable_get('@property_hash') + property_hash[:roles] = ['foo_role'] resource.provider.redirect_uris = ['foobar'] + resource.provider.roles = ['new_foo_role'] resource.provider.flush end @@ -102,5 +142,19 @@ 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/provider/keycloak_realm/kcadm_spec.rb b/spec/unit/puppet/provider/keycloak_realm/kcadm_spec.rb --- a/spec/unit/puppet/provider/keycloak_realm/kcadm_spec.rb +++ b/spec/unit/puppet/provider/keycloak_realm/kcadm_spec.rb @@ -17,6 +17,8 @@ allow(described_class).to receive(:get_client_scopes).with('master', 'optional').and_return('address' => '1cda5a52-aa2c-4b07-b620-30b703619581') allow(described_class).to receive(:get_events_config).with('test').and_return({}) allow(described_class).to receive(:get_events_config).with('master').and_return({}) + allow(described_class).to receive(:get_realm_roles).with('test').and_return(['offline_access', 'uma_authorization']) + allow(described_class).to receive(:get_realm_roles).with('master').and_return(['offline_access', 'uma_authorization']) expect(described_class.instances.length).to eq(2) end @@ -28,11 +30,14 @@ allow(described_class).to receive(:get_client_scopes).with('master', 'optional').and_return('address' => '1cda5a52-aa2c-4b07-b620-30b703619581') allow(described_class).to receive(:get_events_config).with('test').and_return({}) allow(described_class).to receive(:get_events_config).with('master').and_return({}) + allow(described_class).to receive(:get_realm_roles).with('test').and_return(['offline_access', 'uma_authorization']) + allow(described_class).to receive(:get_realm_roles).with('master').and_return(['offline_access', 'uma_authorization']) property_hash = described_class.instances[0].instance_variable_get('@property_hash') expect(property_hash[:enabled]).to eq(:true) expect(property_hash[:login_with_email_allowed]).to eq(:false) expect(property_hash[:default_client_scopes]).to eq(['profile']) expect(property_hash[:optional_client_scopes]).to eq(['address']) + expect(property_hash[:roles]).to eq(['offline_access', 'uma_authorization']) end end # describe 'self.prefetch' do @@ -60,10 +65,15 @@ it 'creates a realm' do temp = Tempfile.new('keycloak_realm') etemp = Tempfile.new('keycloak_events_config') + rtemp = Tempfile.new('keycloak_realm_role') allow(Tempfile).to receive(:new).with('keycloak_realm').and_return(temp) allow(Tempfile).to receive(:new).with('keycloak_events_config').and_return(etemp) + allow(Tempfile).to receive(:new).with('keycloak_realm_role').and_return(rtemp) + allow(described_class).to receive(:get_realm_roles).with('test').and_return(['offline_access', 'uma_authorization']) expect(resource.provider).to receive(:kcadm).with('create', 'realms', nil, temp.path) + expect(resource.provider).to receive(:kcadm).with('create', 'roles', 'test', rtemp.path) expect(resource.provider).to receive(:kcadm).with('update', 'events/config', 'test', etemp.path) + resource[:roles] = ['offline_access', 'uma_authorization', 'new_role'] resource.provider.create property_hash = resource.provider.instance_variable_get('@property_hash') expect(property_hash[:ensure]).to eq(:present) @@ -83,12 +93,19 @@ it 'updates a realm' do temp = Tempfile.new('keycloak_realm') etemp = Tempfile.new('keycloak_events_config') + rtemp = Tempfile.new('keycloak_realm_role') allow(Tempfile).to receive(:new).with('keycloak_realm').and_return(temp) allow(Tempfile).to receive(:new).with('keycloak_events_config').and_return(etemp) + allow(Tempfile).to receive(:new).with('keycloak_realm_role').and_return(rtemp) allow(resource.provider).to receive(:kcadm).with('get', 'authentication/flows', 'test', nil, ['alias']).and_return('[]') expect(resource.provider).to receive(:kcadm).with('update', 'realms/test', nil, temp.path) + expect(resource.provider).to receive(:kcadm).with('delete', 'roles/offline_access', 'test') + expect(resource.provider).to receive(:kcadm).with('create', 'roles', 'test', rtemp.path) expect(resource.provider).to receive(:kcadm).with('update', 'events/config', 'test', etemp.path) + property_hash = resource.provider.instance_variable_get('@property_hash') + property_hash[:roles] = ['offline_access', 'uma_authorization'] resource.provider.login_with_email_allowed = :false + resource.provider.roles = ['uma_authorization', 'new_role'] resource.provider.flush end end diff --git a/spec/unit/puppet/type/keycloak_client_spec.rb b/spec/unit/puppet/type/keycloak_client_spec.rb --- a/spec/unit/puppet/type/keycloak_client_spec.rb +++ b/spec/unit/puppet/type/keycloak_client_spec.rb @@ -72,6 +72,8 @@ redirect_uris: [], web_origins: [], login_theme: 'absent', + browser_flow: :absent, + direct_grant_flow: :absent, } describe 'basic properties' do @@ -81,6 +83,8 @@ :login_theme, :root_url, :base_url, + :browser_flow, + :direct_grant_flow, ].each do |p| it "should accept a #{p}" do config[p] = 'foo' @@ -140,6 +144,7 @@ :optional_client_scopes, :redirect_uris, :web_origins, + :roles, ].each do |p| it "should accept array for #{p}" do config[p] = ['foo', 'bar'] @@ -203,4 +208,15 @@ 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 diff --git a/spec/unit/puppet/type/keycloak_identity_provider_spec.rb b/spec/unit/puppet/type/keycloak_identity_provider_spec.rb --- a/spec/unit/puppet/type/keycloak_identity_provider_spec.rb +++ b/spec/unit/puppet/type/keycloak_identity_provider_spec.rb @@ -52,6 +52,11 @@ expect(resource[:provider_id]).to eq('oidc') end + it 'allows keycloak-oidc' do + config[:provider_id] = 'keycloak-oidc' + expect(resource[:provider_id]).to eq('keycloak-oidc') + end + it 'does not allow invalid provider_id' do config[:provider_id] = 'foo' expect { @@ -113,6 +118,7 @@ :default_scope, :allowed_clock_skew, :forward_parameters, + :jwks_url, ].each do |p| it "should accept a #{p}" do config[p] = 'foo' @@ -125,6 +131,22 @@ end end + describe 'integer properties' do + # Test integer properties + [ + :gui_order, + ].each do |p| + it "should accept a #{p}" do + config[p] = 100 + expect(resource[p]).to eq('100') + 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 [ @@ -198,6 +220,22 @@ end end + describe 'sync_mode' do + [ + 'IMPORT', 'LEGACY', 'FORCE' + ].each do |v| + it "accepts #{v}" do + config[:sync_mode] = v + expect(resource[:sync_mode]).to eq(v) + end + end + + it 'does not accept invalid values' do + config[:sync_mode] = 'foo' + expect { resource }.to raise_error(%r{Invalid}) + 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 @@ -228,6 +266,17 @@ expect(rel.target.ref).to eq(resource.ref) end + it 'autorequires browser flow' do + config[:first_broker_login_flow_alias] = '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 + it 'requires realm' do config[:ensure] = :present config[:provider_id] = 'oidc' diff --git a/spec/unit/puppet/type/keycloak_ldap_user_provider_spec.rb b/spec/unit/puppet/type/keycloak_ldap_user_provider_spec.rb --- a/spec/unit/puppet/type/keycloak_ldap_user_provider_spec.rb +++ b/spec/unit/puppet/type/keycloak_ldap_user_provider_spec.rb @@ -177,6 +177,9 @@ uuid_ldap_attribute: 'entryUUID', import_enabled: :true, user_object_classes: ['inetOrgPerson', 'organizationalPerson'], + trust_email: :false, + full_sync_period: '-1', + changed_sync_period: '-1', } describe 'basic properties' do @@ -201,11 +204,29 @@ end end + describe 'integer properties' do + # Test integer properties + [ + :full_sync_period, + :changed_sync_period, + ].each do |p| + it "should accept a #{p}" do + config[p] = 100 + expect(resource[p]).to eq('100') + 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, :import_enabled, + :trust_email, ].each do |p| it "should accept true for #{p}" do config[p] = true diff --git a/spec/unit/puppet/type/keycloak_realm_spec.rb b/spec/unit/puppet/type/keycloak_realm_spec.rb --- a/spec/unit/puppet/type/keycloak_realm_spec.rb +++ b/spec/unit/puppet/type/keycloak_realm_spec.rb @@ -114,6 +114,9 @@ # Test boolean properties [ :remember_me, + :registration_allowed, + :reset_password_allowed, + :verify_email, :login_with_email_allowed, :internationalization_enabled, :events_enabled, @@ -122,6 +125,7 @@ :smtp_server_auth, :smtp_server_starttls, :smtp_server_ssl, + :brute_force_protected, ].each do |p| it "should accept true for #{p}" do config[p] = true @@ -159,6 +163,7 @@ :optional_client_scopes, :events_listeners, :supported_locales, + :roles, ].each do |p| it "should accept array for #{p}" do config[p] = ['foo', 'bar']