diff --git a/README.md b/README.md index 9fb1cf4..7cf7074 100644 --- a/README.md +++ b/README.md @@ -1,308 +1,310 @@ # puppet-module-keycloak [![Puppet Forge](http://img.shields.io/puppetforge/v/treydock/keycloak.svg)](https://forge.puppetlabs.com/treydock/keycloak) [![Build Status](https://travis-ci.org/treydock/puppet-module-keycloak.png)](https://travis-ci.org/treydock/puppet-module-keycloak) #### Table of Contents 1. [Overview](#overview) * [Supported Versions of Keycloak](#supported-versions-of-keycloak) 2. [Usage - Configuration options](#usage) 3. [Reference - Parameter and detailed reference to all options](#reference) 4. [Limitations - OS compatibility, etc.](#limitations) ## Overview The keycloak module allows easy installation and management of Keycloak. ### Supported Versions of Keycloak | Keycloak Version | Keycloak Puppet module versions | | ---------------- | ------------------------------- | | 3.x | 2.x | | 4.x - 6.x | 3.x | | 6.x - 8.x | 4.x - 5.x | | 8.x | 6.x | ## Usage ### keycloak Install Keycloak using default `h2` database storage. ```puppet class { 'keycloak': } ``` Install a specific version of Keycloak. ```puppet class { 'keycloak': version => '6.0.1', datasource_driver => 'mysql', } ``` Upgrading Keycloak version works by changing `version` parameter as long as the `datasource_driver` is not the default of `h2`. An upgrade involves installing the new version without touching the old version, updating the symlink which defaults to `/opt/keycloak`, applying all changes to new version and then restarting the `keycloak` service. If the previous `version` was `6.0.1` using the following will upgrade to `7.0.0`: ```puppet class { 'keycloak': version => '7.0.0', datasource_driver => 'mysql', } ``` Install keycloak and use a local MySQL server for database storage ```puppet include mysql::server class { 'keycloak': datasource_driver => 'mysql', datasource_host => 'localhost', datasource_port => 3306, datasource_dbname => 'keycloak', datasource_username => 'keycloak', datasource_password => 'foobar', } ``` The following example can be used to configure keycloak with a local PostgreSQL server. ```puppet include postgresql::server class { 'keycloak': datasource_driver => 'postgresql', datasource_host => 'localhost', datasource_port => 5432, datasource_dbname => 'keycloak', datasource_username => 'keycloak', datasource_password => 'foobar', } ``` Configure a SSL certificate truststore and add a LDAP server's certificate to the truststore. ```puppet class { 'keycloak': truststore => true, truststore_password => 'supersecret', truststore_hostname_verification_policy => 'STRICT', } keycloak::truststore::host { 'ldap1.example.com': certificate => '/etc/openldap/certs/0a00000.0', } ``` Setup Keycloak to proxy through Apache HTTPS. ```puppet class { 'keycloak': proxy_https => true } apache::vhost { 'idp.example.com': servername => 'idp.example.com', port => '443', ssl => true, manage_docroot => false, docroot => '/var/www/html', proxy_preserve_host => true, proxy_pass => [ {'path' => '/', 'url' => 'http://localhost:8080/'} ], request_headers => [ 'set X-Forwarded-Proto "https"', 'set X-Forwarded-Port "443"' ], ssl_cert => '/etc/pki/tls/certs/idp.example.com/crt', ssl_key => '/etc/pki/tls/private/idp.example.com.key', } ``` Setup a host for theme development so that theme changes don't require a service restart, not recommended for production. ```puppet class { 'keycloak': theme_static_max_age => -1, theme_cache_themes => false, theme_cache_templates => false, } ``` Run Keycloak using standalone clustered mode: ```puppet class { 'keycloak': operating_mode => 'clustered', } ``` ### keycloak_realm Define a Keycloak realm that uses username and not email for login and to use a local branded theme. ```puppet keycloak_realm { 'test': ensure => 'present', remember_me => true, login_with_email_allowed => false, login_theme => 'my_theme', } ``` +**NOTE:** If the flow properties such as `browser_flow` are changed from their defaults then this value will not be set when a realm is first created. The value will also not be updated if the flow does not exist. For new realms you will have to run Puppet twice in order to create the flows then update the realm setting. + ### keycloak\_ldap\_user_provider Define a LDAP user provider so that authentication can be performed against LDAP. The example below uses two LDAP servers, disables importing of users and assumes the SSL certificates are trusted and do not require being in the truststore. ```puppet keycloak_ldap_user_provider { 'LDAP on test': ensure => 'present', users_dn => 'ou=People,dc=example,dc=com', connection_url => 'ldaps://ldap1.example.com:636 ldaps://ldap2.example.com:636', import_enabled => false, use_truststore_spi => 'never', } ``` **NOTE** The `Id` for the above resource would be `LDAP-test` where the format is `${resource_name}-${realm}`. ### keycloak\_ldap_mapper Use the LDAP attribute 'gecos' as the full name attribute. ```puppet keycloak_ldap_mapper { 'full name for LDAP-test on test: ensure => 'present', resource_name => 'full name', type => 'full-name-ldap-mapper', ldap_attribute => 'gecos', } ``` ### keycloak\_sssd\_user\_provider Define SSSD user provider. **NOTE** This type requires that SSSD be properly configured and Keycloak service restarted after SSSD ifp service is setup. Also requires `keycloak` class be called with `with_sssd_support` set to `true`. ```puppet keycloak_sssd_user_provider { 'SSSD on test': ensure => 'present', } ``` ### keycloak_client Register a client. ```puppet keycloak_client { 'www.example.com': ensure => 'present', realm => 'test', redirect_uris => [ "https://www.example.com/oidc", "https://www.example.com", ], client_template => 'oidc-clients', secret => 'supersecret', } ``` ### keycloak::client_scope::oidc Defined type that can be used to define both `keycloak_client_scope` and `keycloak_protocol_mapper` resources for OpenID Connect. ```puppet keycloak::client_scope::oidc { 'oidc-clients': realm => 'test', } ``` ### keycloak::client_scope::saml Defined type that can be used to define both `keycloak_client_scope` and `keycloak_protocol_mapper` resources for SAML. ```puppet keycloak::client_scope::saml { 'saml-clients': realm => 'test', } ``` ### keycloak\_client_scope Define a Client Scope of `email` for realm `test` in Keycloak: ```puppet keycloak_client_scope { 'email on test': protocol => 'openid-connect', } ``` ### keycloak\_protocol_mapper Associate a Protocol Mapper to a given Client Scope. The name in the following example will add the `email` protocol mapper to client scope `oidc-email` in the realm `test`. ```puppet keycloak_protocol_mapper { "email for oidc-email on test": claim_name => 'email', user_attribute => 'email', } ``` ### keycloak\_client\_protocol\_mapper Add `email` protocol mapper to `test.example.com` client in realm `test` ```puppet keycloak_client_protocol_mapper { "email for test.example.com on test": claim_name => 'email', user_attribute => 'email', } ``` ### keycloak\_identity\_provider Add `cilogon` identity provider to `test` realm ```puppet keycloak_identity_provider { 'cilogon on test': ensure => 'present', display_name => 'CILogon', provider_id => 'oidc', first_broker_login_flow_alias => 'browser', client_id => 'cilogon:/client_id/foobar', client_secret => 'supersecret', user_info_url => 'https://cilogon.org/oauth2/userinfo', token_url => 'https://cilogon.org/oauth2/token', authorization_url => 'https://cilogon.org/authorize', } ``` ### keycloak\_api The keycloak_api type can be used to define how this module's types access the Keycloak API if this module is only used for the types/providers and the module's `kcadm-wrapper.sh` is not installed. ```puppet keycloak_api { 'keycloak' install_dir => '/opt/keycloak', server => 'http://localhost:8080/auth', realm => 'master', user => 'admin', password => 'changeme', } ``` The path for `install_dir` will be joined with `bin/kcadm.sh` to produce the full path to `kcadm.sh`. ## Reference [http://treydock.github.io/puppet-module-keycloak/](http://treydock.github.io/puppet-module-keycloak/) ## Limitations This module has been tested on: * CentOS 7 x86_64 * RedHat 7 x86_64 * Debian 9 x86_64 * Ubuntu 18.04 x86_64 diff --git a/lib/puppet/provider/keycloak_realm/kcadm.rb b/lib/puppet/provider/keycloak_realm/kcadm.rb index c1944fa..0b36612 100644 --- a/lib/puppet/provider/keycloak_realm/kcadm.rb +++ b/lib/puppet/provider/keycloak_realm/kcadm.rb @@ -1,283 +1,311 @@ require File.expand_path(File.join(File.dirname(__FILE__), '..', 'keycloak_api')) Puppet::Type.type(:keycloak_realm).provide(:kcadm, parent: Puppet::Provider::KeycloakAPI) do desc '' mk_resource_methods + def flow_properties + [ + :browser_flow, + :registration_flow, + :direct_grant_flow, + :reset_credentials_flow, + :client_authentication_flow, + :docker_authentication_flow, + ] + end + def self.get_client_scopes(realm, type) output = kcadm('get', "realms/#{realm}/default-#{type}-client-scopes") Puppet.debug("Realms #{realm} #{type} client scopes: #{output}") data = JSON.parse(output) scopes = {} data.each do |d| scopes[d['name']] = d['id'] end Puppet.debug("Returned scopes: #{scopes}") scopes end def get_client_scopes(*args) self.class.get_client_scopes(*args) end def self.get_events_config(realm) output = kcadm('get', 'events/config', realm) Puppet.debug("#{realm} events/config: #{output}") begin data = JSON.parse(output) rescue JSON::ParserError Puppet.debug('Unable to parse output from kcadm get events/config') data = {} end data.delete('enabledEventTypes') data end + def available_flows(realm) + output = kcadm('get', 'authentication/flows', realm, nil, ['alias']) + Puppet.debug("#{realm} authentication/flows: #{output}") + begin + data = JSON.parse(output) + rescue JSON::ParserError + Puppet.debug('Unable to parse output from kcadm get authentication/flows') + return [] + end + data.map { |f| f['alias'] } + end + def self.instances output = kcadm('get', 'realms') Puppet.debug("Realms: #{output}") begin data = JSON.parse(output) rescue JSON::ParserError Puppet.debug('Unable to parse output from kcadm get realms') data = [] end data.map do |d| realm = {} realm[:ensure] = :present realm[:id] = d['id'] 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) value = if property.to_s =~ %r{events} events_config[camelize(property)] else d[camelize(property)] end if !!value == value # rubocop:disable Style/DoubleNegation value = value.to_s.to_sym end realm[property.to_sym] = value end default_scopes = get_client_scopes(realm[:name], 'default') 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 } new(realm) end end def self.prefetch(resources) realms = instances resources.keys.each do |name| provider = realms.find { |realm| realm.name == name } if provider resources[name].provider = provider end end end def create data = {} events_config = {} data[:id] = resource[:id] 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) if property.to_s =~ %r{events} events_config[camelize(property)] = convert_property_value(resource[property.to_sym]) elsif resource[property.to_sym] data[camelize(property)] = convert_property_value(resource[property.to_sym]) end end t = Tempfile.new('keycloak_realm') t.write(JSON.pretty_generate(data)) t.close Puppet.debug(IO.read(t.path)) begin kcadm('create', 'realms', nil, t.path) rescue Puppet::ExecutionFailure => e raise Puppet::Error, "kcadm create realm failed\nError message: #{e.message}" end scope_id = nil if resource[:default_client_scopes] default_scopes = default_scopes ||= get_client_scopes(resource[:name], 'default') remove_default_scopes = default_scopes.keys - resource[:default_client_scopes] begin remove_default_scopes.each do |s| scope_id = default_scopes[s] kcadm('delete', "realms/#{resource[:name]}/default-default-client-scopes/#{scope_id}") end rescue Puppet::ExecutionFailure => e raise Puppet::Error, "kcadm delete realms/#{resource[:name]}/default-default-client-scopes/#{scope_id}: #{e.message}" end end if resource[:optional_client_scopes] optional_scopes = optional_scopes ||= get_client_scopes(resource[:name], 'optional') remove_optional_scopes = optional_scopes.keys - resource[:optional_client_scopes] begin remove_optional_scopes.each do |s| scope_id = optional_scopes[s] kcadm('delete', "realms/#{resource[:name]}/default-optional-client-scopes/#{scope_id}") end rescue Puppet::ExecutionFailure => e raise Puppet::Error, "kcadm delete realms/#{resource[:name]}/default-optional-client-scopes/#{scope_id}: #{e.message}" end end if resource[:default_client_scopes] default_scopes = default_scopes ||= get_client_scopes(resource[:name], 'default') add_default_scopes = resource[:default_client_scopes] - default_scopes.keys begin add_default_scopes.each do |s| scope_id = default_scopes[s] kcadm('update', "realms/#{resource[:name]}/default-default-client-scopes/#{scope_id}") end rescue Puppet::ExecutionFailure => e raise Puppet::Error, "kcadm update realms/#{resource[:name]}/default-default-client-scopes/#{scope_id}: #{e.message}" end end if resource[:optional_client_scopes] optional_scopes = optional_scopes ||= get_client_scopes(resource[:name], 'optional') add_optional_scopes = resource[:optional_client_scopes] - optional_scopes.keys begin add_optional_scopes.each do |s| scope_id = optional_scopes[s] kcadm('update', "realms/#{resource[:name]}/default-optional-client-scopes/#{scope_id}") end rescue Puppet::ExecutionFailure => e raise Puppet::Error, "kcadm update realms/#{resource[:name]}/default-optional-client-scopes/#{scope_id}: #{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)) events_config_t.close Puppet.debug(IO.read(events_config_t.path)) begin kcadm('update', 'events/config', resource[:name], events_config_t.path) rescue Puppet::ExecutionFailure => e raise Puppet::Error, "kcadm update events config failed\nError message: #{e.message}" end end @property_hash[:ensure] = :present end def destroy begin kcadm('delete', "realms/#{resource[:name]}") 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? data = {} events_config = {} type_properties.each do |property| next if [:default_client_scopes, :optional_client_scopes].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 + end if @property_flush[property.to_sym] # || resource[property.to_sym] data[camelize(property)] = convert_property_value(resource[property.to_sym]) end if property.to_s =~ %r{events} events_config[camelize(property)] = convert_property_value(resource[property.to_sym]) end end unless data.empty? t = Tempfile.new('keycloak_realm') t.write(JSON.pretty_generate(data)) t.close Puppet.debug(IO.read(t.path)) begin kcadm('update', "realms/#{resource[:name]}", nil, t.path) rescue Puppet::ExecutionFailure => e raise Puppet::Error, "kcadm update realm failed\nError message: #{e.message}" end end scope_id = nil if @property_flush[:default_client_scopes] default_scopes = default_scopes ||= get_client_scopes(resource[:name], 'default') remove_default_scopes = default_scopes.keys - @property_flush[:default_client_scopes] begin remove_default_scopes.each do |s| scope_id = default_scopes[s] kcadm('delete', "realms/#{resource[:name]}/default-default-client-scopes/#{scope_id}") end rescue Puppet::ExecutionFailure => e raise Puppet::Error, "kcadm delete realms/#{resource[:name]}/default-default-client-scopes/#{scope_id}: #{e.message}" end end if @property_flush[:optional_client_scopes] optional_scopes = optional_scopes ||= get_client_scopes(resource[:name], 'optional') remove_optional_scopes = optional_scopes.keys - @property_flush[:optional_client_scopes] begin remove_optional_scopes.each do |s| scope_id = optional_scopes[s] kcadm('delete', "realms/#{resource[:name]}/default-optional-client-scopes/#{scope_id}") end rescue Puppet::ExecutionFailure => e raise Puppet::Error, "kcadm delete realms/#{resource[:name]}/default-optional-client-scopes/#{scope_id}: #{e.message}" end end if @property_flush[:default_client_scopes] default_scopes = default_scopes ||= get_client_scopes(resource[:name], 'default') add_default_scopes = @property_flush[:default_client_scopes] - default_scopes.keys begin add_default_scopes.each do |s| scope_id = default_scopes[s] kcadm('update', "realms/#{resource[:name]}/default-default-client-scopes/#{scope_id}") end rescue Puppet::ExecutionFailure => e raise Puppet::Error, "kcadm update realms/#{resource[:name]}/default-default-client-scopes/#{scope_id}: #{e.message}" end end if @property_flush[:optional_client_scopes] optional_scopes = optional_scopes ||= get_client_scopes(resource[:name], 'optional') add_optional_scopes = @property_flush[:optional_client_scopes] - optional_scopes.keys begin add_optional_scopes.each do |s| scope_id = optional_scopes[s] kcadm('update', "realms/#{resource[:name]}/default-optional-client-scopes/#{scope_id}") end rescue Puppet::ExecutionFailure => e raise Puppet::Error, "kcadm update realms/#{resource[:name]}/default-optional-client-scopes/#{scope_id}: #{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)) events_config_t.close Puppet.debug(IO.read(events_config_t.path)) begin kcadm('update', 'events/config', resource[:name], events_config_t.path) rescue Puppet::ExecutionFailure => e raise Puppet::Error, "kcadm update events config failed\nError message: #{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_realm.rb b/lib/puppet/type/keycloak_realm.rb index 3ee170e..6585abd 100644 --- a/lib/puppet/type/keycloak_realm.rb +++ b/lib/puppet/type/keycloak_realm.rb @@ -1,160 +1,166 @@ require_relative '../../puppet_x/keycloak/type' require_relative '../../puppet_x/keycloak/array_property' Puppet::Type.newtype(:keycloak_realm) do desc <<-DESC Manage Keycloak realms @example Add a realm with a custom theme keycloak_realm { 'test': ensure => 'present', remember_me => true, login_with_email_allowed => false, login_theme => 'my_theme', } DESC extend PuppetX::Keycloak::Type add_autorequires(false) ensurable newparam(:name, namevar: true) do desc 'The realm name' end newparam(:id) do desc 'Id. Default to `name`.' defaultto do @resource[:name] end end newproperty(:display_name) do desc 'displayName' end newproperty(:display_name_html) do desc 'displayNameHtml' end newproperty(:login_theme) do desc 'loginTheme' defaultto 'keycloak' end newproperty(:account_theme) do desc 'accountTheme' defaultto 'keycloak' end newproperty(:admin_theme) do desc 'adminTheme' defaultto 'keycloak' end newproperty(:email_theme) do desc 'emailTheme' defaultto 'keycloak' end newproperty(:access_code_lifespan_user_action) do desc 'accessCodeLifespanUserAction' validate do |property| raise Puppet::Error, 'Property access_code_lifespan_user_action must be an integer' unless property.is_a?(Integer) end end newproperty(:access_token_lifespan_for_implicit_flow) do desc 'accessTokenLifespanForImplicitFlow' validate do |property| raise Puppet::Error, 'Property access_token_lifespan_for_implicit_flow must be an integer' unless property.is_a?(Integer) end end newproperty(:enabled, boolean: true) do desc 'enabled' newvalues(:true, :false) defaultto :true end newproperty(:remember_me, boolean: true) do desc 'rememberMe' newvalues(:true, :false) defaultto :false end newproperty(:login_with_email_allowed, boolean: true) do desc 'loginWithEmailAllowed' newvalues(:true, :false) defaultto :true end newproperty(:browser_flow) do desc 'browserFlow' defaultto('browser') + munge { |v| v.to_s } end newproperty(:registration_flow) do desc 'registrationFlow' defaultto('registration') + munge { |v| v.to_s } end newproperty(:direct_grant_flow) do desc 'directGrantFlow' defaultto('direct grant') + munge { |v| v.to_s } end newproperty(:reset_credentials_flow) do desc 'resetCredentialsFlow' defaultto('reset credentials') + munge { |v| v.to_s } end newproperty(:client_authentication_flow) do desc 'clientAuthenticationFlow' defaultto('clients') + munge { |v| v.to_s } end newproperty(:docker_authentication_flow) do desc 'dockerAuthenticationFlow' defaultto('docker auth') + munge { |v| v.to_s } end newproperty(:default_client_scopes, array_matching: :all, parent: PuppetX::Keycloak::ArrayProperty) do desc 'Default Client Scopes' end newproperty(:optional_client_scopes, array_matching: :all, parent: PuppetX::Keycloak::ArrayProperty) do desc 'Optional Client Scopes' end newproperty(:supported_locales, array_matching: :all, parent: PuppetX::Keycloak::ArrayProperty) do desc 'Supported Locales' end newproperty(:events_enabled, boolean: true) do desc 'eventsEnabled' newvalues(:true, :false) defaultto :false end newproperty(:events_expiration) do desc 'eventsExpiration' end newproperty(:events_listeners, array_matching: :all, parent: PuppetX::Keycloak::ArrayProperty) do desc 'eventsListeners' defaultto ['jboss-logging'] end newproperty(:admin_events_enabled, boolean: true) do desc 'adminEventsEnabled' newvalues(:true, :false) defaultto :false end newproperty(:admin_events_details_enabled, boolean: true) do desc 'adminEventsDetailsEnabled' newvalues(:true, :false) defaultto :false end end diff --git a/spec/acceptance/2_realm_spec.rb b/spec/acceptance/2_realm_spec.rb index 2e5c358..bd294af 100644 --- a/spec/acceptance/2_realm_spec.rb +++ b/spec/acceptance/2_realm_spec.rb @@ -1,105 +1,132 @@ require 'spec_helper_acceptance' describe 'keycloak_realm:', if: RSpec.configuration.keycloak_full do context 'creates realm' do it 'runs successfully' do pp = <<-EOS include mysql::server class { 'keycloak': datasource_driver => 'mysql', } - keycloak_realm { 'test': ensure => 'present' } + keycloak_realm { 'test': + ensure => 'present', + } EOS apply_manifest(pp, catch_failures: true) apply_manifest(pp, catch_changes: true) end it 'has created a realm' do on hosts, '/opt/keycloak/bin/kcadm-wrapper.sh get realms/test' do data = JSON.parse(stdout) expect(data['id']).to eq('test') end end it 'has left default-client-scopes' do on hosts, '/opt/keycloak/bin/kcadm-wrapper.sh get realms/test/default-default-client-scopes' do data = JSON.parse(stdout) names = data.map { |d| d['name'] }.sort expect(names).to include('email') expect(names).to include('profile') expect(names).to include('role_list') end end it 'has left optional-client-scopes' do on hosts, '/opt/keycloak/bin/kcadm-wrapper.sh get realms/test/default-optional-client-scopes' do data = JSON.parse(stdout) names = data.map { |d| d['name'] }.sort expect(names).to include('address') expect(names).to include('offline_access') expect(names).to include('phone') end end it 'has default events config' do on hosts, '/opt/keycloak/bin/kcadm-wrapper.sh get events/config -r test' do data = JSON.parse(stdout) expect(data['eventsEnabled']).to eq(false) expect(data['eventsExpiration']).to be_nil expect(data['eventsListeners']).to eq(['jboss-logging']) expect(data['adminEventsEnabled']).to eq(false) expect(data['adminEventsDetailsEnabled']).to eq(false) end end end context 'updates realm' do it 'runs successfully' do pp = <<-EOS include mysql::server class { 'keycloak': datasource_driver => 'mysql', } keycloak_realm { 'test': ensure => 'present', remember_me => true, default_client_scopes => ['profile'], events_enabled => true, events_expiration => 2678400, admin_events_enabled => true, admin_events_details_enabled => true, } EOS apply_manifest(pp, catch_failures: true) apply_manifest(pp, catch_changes: true) end it 'has updated the realm' do on hosts, '/opt/keycloak/bin/kcadm-wrapper.sh get realms/test' do data = JSON.parse(stdout) expect(data['rememberMe']).to eq(true) end end it 'has updated the realm default-client-scopes' do on hosts, '/opt/keycloak/bin/kcadm-wrapper.sh get realms/test/default-default-client-scopes' do data = JSON.parse(stdout) names = data.map { |d| d['name'] } expect(names).to eq(['profile']) end end it 'has updated events config' do on hosts, '/opt/keycloak/bin/kcadm-wrapper.sh get events/config -r test' do data = JSON.parse(stdout) expect(data['eventsEnabled']).to eq(true) expect(data['eventsExpiration']).to eq(2_678_400) expect(data['eventsListeners']).to eq(['jboss-logging']) expect(data['adminEventsEnabled']).to eq(true) expect(data['adminEventsDetailsEnabled']).to eq(true) end end end + + context 'creates realm with invalid browser flow' do + it 'runs successfully' do + pp = <<-EOS + include mysql::server + class { 'keycloak': + datasource_driver => 'mysql', + } + keycloak_realm { 'test2': + ensure => 'present', + browser_flow => 'Copy of browser', + } + EOS + + apply_manifest(pp, catch_failures: true) + apply_manifest(pp, expect_changes: true) + end + + it 'has created a realm' do + on hosts, '/opt/keycloak/bin/kcadm-wrapper.sh get realms/test2' do + data = JSON.parse(stdout) + expect(data['browserFlow']).to eq('browser') + end + 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 index a492e18..7da69da 100644 --- a/spec/unit/puppet/provider/keycloak_realm/kcadm_spec.rb +++ b/spec/unit/puppet/provider/keycloak_realm/kcadm_spec.rb @@ -1,94 +1,95 @@ require 'spec_helper' describe Puppet::Type.type(:keycloak_realm).provider(:kcadm) do let(:type) do Puppet::Type.type(:keycloak_realm) end let(:resource) do type.new(name: 'test') end describe 'self.instances' do it 'creates instances' do allow(described_class).to receive(:kcadm).with('get', 'realms').and_return(my_fixture_read('get.out')) allow(described_class).to receive(:get_client_scopes).with('test', 'default').and_return('profile' => '8a6759cb-3950-48a2-b29b-c2c06fc3379b') allow(described_class).to receive(:get_client_scopes).with('test', 'optional').and_return('address' => '1cda5a52-aa2c-4b07-b620-30b703619581') allow(described_class).to receive(:get_client_scopes).with('master', 'default').and_return('profile' => '8a6759cb-3950-48a2-b29b-c2c06fc3379b') 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({}) expect(described_class.instances.length).to eq(2) end it 'returns the resource for a fileset' do allow(described_class).to receive(:kcadm).with('get', 'realms').and_return(my_fixture_read('get.out')) allow(described_class).to receive(:get_client_scopes).with('test', 'default').and_return('profile' => '8a6759cb-3950-48a2-b29b-c2c06fc3379b') allow(described_class).to receive(:get_client_scopes).with('test', 'optional').and_return('address' => '1cda5a52-aa2c-4b07-b620-30b703619581') allow(described_class).to receive(:get_client_scopes).with('master', 'default').and_return('profile' => '8a6759cb-3950-48a2-b29b-c2c06fc3379b') 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({}) 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']) 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 temp = Tempfile.new('keycloak_realm') etemp = Tempfile.new('keycloak_events_config') allow(Tempfile).to receive(:new).with('keycloak_realm').and_return(temp) allow(Tempfile).to receive(:new).with('keycloak_events_config').and_return(etemp) expect(resource.provider).to receive(:kcadm).with('create', 'realms', nil, temp.path) expect(resource.provider).to receive(:kcadm).with('update', 'events/config', 'test', etemp.path) resource.provider.create property_hash = resource.provider.instance_variable_get('@property_hash') expect(property_hash[:ensure]).to eq(:present) end end describe 'destroy' do it 'deletes a realm' do expect(resource.provider).to receive(:kcadm).with('delete', 'realms/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 temp = Tempfile.new('keycloak_realm') etemp = Tempfile.new('keycloak_events_config') allow(Tempfile).to receive(:new).with('keycloak_realm').and_return(temp) allow(Tempfile).to receive(:new).with('keycloak_events_config').and_return(etemp) + 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('update', 'events/config', 'test', etemp.path) resource.provider.login_with_email_allowed = :false resource.provider.flush end end end