From b00655eb2723fc5e299eb7e22b96e227189e039a Mon Sep 17 00:00:00 2001 From: Trey Dockendorf Date: Sun, 16 Jul 2023 12:17:48 -0400 Subject: [PATCH] Add keycloak::partial_import resource Fixes #229 --- README.md | 17 ++++++ manifests/config.pp | 16 +++++- manifests/init.pp | 18 ++++++- manifests/partial_import.pp | 78 ++++++++++++++++++++++++++++ manifests/resources.pp | 3 ++ spec/acceptance/2_realm_spec.rb | 13 +++++ spec/defines/partial_import_spec.rb | 64 +++++++++++++++++++++++ spec/fixtures/partial-import.json | 44 ++++++++++++++++ spec/spec_helper_acceptance_setup.rb | 1 + 9 files changed, 250 insertions(+), 4 deletions(-) create mode 100644 manifests/partial_import.pp create mode 100644 spec/defines/partial_import_spec.rb create mode 100644 spec/fixtures/partial-import.json diff --git a/README.md b/README.md index eaa99830..83c463f2 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ 2. [Usage - Configuration options](#usage) * [Keycloak](#keycloak) * [Deploy SPI](#deploy-spi) + * [Partial Import](#partial-import) * [keycloak_realm](#keycloak_realm) * [keycloak_role_mapping](#keycloak_role_mapping) * [keycloak_ldap_user_provider](#keycloak_ldap_user_provider) @@ -306,6 +307,22 @@ keycloak::spi_deployment { 'duo-spi': } ``` +### Partial Import + +This module supports [Importing data from exported JSON files](https://www.keycloak.org/docs/latest/server_admin/index.html#importing-a-realm-from-exported-json-file) via the `keycloak::partial_import` defined type. + +Example of importing a JSON file into the `test` realm: + +```puppet +keycloak::partial_import { 'mysettings': + realm => 'test', + if_resource_exists => 'SKIP', + source => 'puppet:///modules/profile/keycloak/mysettings.json', +} +``` + +**NOTE:** By default the `keycloak::partial_import` defined type will require the `Keycloak_realm` resource used for the `realm` parameter. If you manage the realm a different way, pass `require_realm => false`. + ### keycloak_realm Define a Keycloak realm that uses username and not email for login and to use a local branded theme. diff --git a/manifests/config.pp b/manifests/config.pp index c2327537..30fdb7f6 100644 --- a/manifests/config.pp +++ b/manifests/config.pp @@ -15,7 +15,7 @@ # - $keycloak::admin_user_password file { 'kcadm-wrapper.sh': ensure => 'file', - path => "${keycloak::install_base}/bin/kcadm-wrapper.sh", + path => $keycloak::wrapper_path, owner => $keycloak::user, group => $keycloak::group, mode => '0750', @@ -23,6 +23,18 @@ show_diff => false, } + file { $keycloak::conf_dir: + ensure => 'directory', + owner => $keycloak::user, + group => $keycloak::group, + mode => '0755', + purge => $keycloak::conf_dir_purge, + force => $keycloak::conf_dir_purge, + recurse => $keycloak::conf_dir_purge, + ignore => ['cache-ispn.xml', 'README.md'], + notify => Class['keycloak::service'], + } + file { $keycloak::admin_env: ensure => 'file', owner => $keycloak::user, @@ -42,7 +54,7 @@ } else { $config_content = template('keycloak/keycloak.conf.erb') } - file { "${keycloak::install_base}/conf/keycloak.conf": + file { "${keycloak::conf_dir}/keycloak.conf": owner => $keycloak::user, group => $keycloak::group, mode => '0600', diff --git a/manifests/init.pp b/manifests/init.pp index 2fce729e..3b1511f6 100644 --- a/manifests/init.pp +++ b/manifests/init.pp @@ -49,6 +49,12 @@ # Additional options added to the end of the service command-line. # @param service_environment_file # Path to the file with environment variables for the systemd service +# @param conf_dir_mode +# The mode for the configuration directory +# @param conf_dir_purge +# Purge unmanaged files in configuration directory +# @param conf_dir_purge_ignore +# The files to ignore when unmanaged files are purged from the configuration directory # @param configs # Define additional configs for keycloak.conf # @param extra_configs @@ -203,6 +209,8 @@ # Boolean that determines if SSSD should be restarted # @param spi_deployments # Hash used to define keycloak::spi_deployment resources +# @param partial_imports +# Hash used to define keycloak::partial_import resources # @param providers_purge # Purge the providers directory of unmanaged SPIs # @param custom_config_content @@ -230,6 +238,9 @@ Enum['start','start-dev'] $start_command = 'start', Optional[String] $service_extra_opts = undef, Optional[Stdlib::Absolutepath] $service_environment_file = undef, + Stdlib::Filemode $conf_dir_mode = '0755', + Boolean $conf_dir_purge = true, + Array $conf_dir_purge_ignore = ['cache-ispn.xml', 'README.md'], Keycloak::Configs $configs = {}, Hash[String, Variant[String[1],Boolean,Array]] $extra_configs = {}, Variant[Stdlib::Host, Enum['unset','UNSET']] $hostname = $facts['networking']['fqdn'], @@ -300,6 +311,7 @@ Array $sssd_ifp_user_attributes = [], Boolean $restart_sssd = true, Hash $spi_deployments = {}, + Hash $partial_imports = {}, Boolean $providers_purge = true, Optional[String] $custom_config_content = undef, Optional[Variant[String, Array]] $custom_config_source = undef, @@ -312,10 +324,12 @@ $download_url = pick($package_url, "https://github.com/keycloak/keycloak/releases/download/${version}/keycloak-${version}.tar.gz") $install_base = pick($install_dir, "/opt/keycloak-${keycloak::version}") - $admin_env = "${install_base}/conf/admin.env" - $truststore_file = "${install_base}/conf/truststore.jks" + $conf_dir = "${install_base}/conf" + $admin_env = "${conf_dir}/admin.env" + $truststore_file = "${conf_dir}/truststore.jks" $tmp_dir = "${install_base}/tmp" $providers_dir = "${install_base}/providers" + $wrapper_path = "${keycloak::install_base}/bin/kcadm-wrapper.sh" $default_config = { 'hostname' => $hostname, diff --git a/manifests/partial_import.pp b/manifests/partial_import.pp new file mode 100644 index 00000000..4c6e9d21 --- /dev/null +++ b/manifests/partial_import.pp @@ -0,0 +1,78 @@ +# @summary Perform partialImport using CLI +# +# @example Perform partial import +# keycloak::partial_import { 'mysettings': +# realm => 'test', +# if_resource_exists => 'SKIP', +# source => 'puppet:///modules/profile/keycloak/mysettings.json', +# } +# +# @param realm +# The Keycloak Realm +# @param if_resource_exists +# Behavior for when resources exist +# @param source +# The import JSON source +# @param content +# The import JSON content +# @param filename +# The filename of the stored JSON +# @param require_realm +# Determines whether to require the Keycloak_realm resource +# @param create_realm +# Determines whether to define the Keycloak_realm resource +# +define keycloak::partial_import ( + String[1] $realm, + Enum['FAIL','SKIP','OVERWRITE'] $if_resource_exists, + Optional[Variant[Stdlib::Filesource, Stdlib::HTTPSUrl]] $source = undef, + Optional[String[1]] $content = undef, + String[1] $filename = $name, + Boolean $require_realm = true, + Boolean $create_realm = false, +) { + include keycloak + + if ! $source and ! $content { + fail("keycloak::partial_import[${name}] must specify either source or content") + } + if $source and $content { + fail("keycloak::partial_import[${name}] specify either source or content, not both") + } + + $file_path = "${keycloak::conf_dir}/${filename}.json" + $command = join([ + "${keycloak::wrapper_path} create partialImport", + "-r ${realm} -s ifResourceExists=${if_resource_exists} -o", + "-f ${file_path}", + ], ' ') + + file { $file_path: + ensure => 'file', + owner => $keycloak::user, + group => $keycloak::group, + mode => '0600', + source => $source, + content => $content, + require => Class['keycloak::install'], + notify => Exec["partial-import-${name}"], + } + + exec { "partial-import-${name}": + path => '/usr/bin:/bin:/usr/sbin:/sbin', + command => "${command} || { rm -f ${file_path}; exit 1; }", + logoutput => true, + refreshonly => true, + require => Keycloak_conn_validator['keycloak'], + } + + if $require_realm { + Keycloak_realm[$realm] -> Exec["partial-import-${name}"] + } + if $create_realm { + keycloak_realm { $realm: + ensure => 'present', + before => Exec["partial-import-${name}"], + } + } +} diff --git a/manifests/resources.pp b/manifests/resources.pp index 71f8a596..4976d01e 100644 --- a/manifests/resources.pp +++ b/manifests/resources.pp @@ -106,4 +106,7 @@ $keycloak::spi_deployments.each |$name, $deployment| { keycloak::spi_deployment { $name: * => $deployment } } + $keycloak::partial_imports.each |$name, $partial_import| { + keycloak::partial_import { $name: * => $partial_import } + } } diff --git a/spec/acceptance/2_realm_spec.rb b/spec/acceptance/2_realm_spec.rb index 560cf8fd..0c0466e3 100644 --- a/spec/acceptance/2_realm_spec.rb +++ b/spec/acceptance/2_realm_spec.rb @@ -40,6 +40,11 @@ class { 'keycloak': } keycloak_realm { 'test realm': ensure => 'present', } + keycloak::partial_import { 'test': + realm => 'test', + if_resource_exists => 'OVERWRITE', + source => 'file:///tmp/partial-import.json', + } PUPPET_PP apply_manifest(pp, catch_failures: true) @@ -147,6 +152,14 @@ class { 'keycloak': } expect(expected_roles - realm_roles).to eq([]) end end + + it 'imports a client' do + on hosts, '/opt/keycloak/bin/kcadm-wrapper.sh get clients -r test' do + data = JSON.parse(stdout) + client = data.find { |d| d['clientId'] == 'test.example.com' } + expect(client['clientId']).to eq('test.example.com') + end + end end context 'when updates realm' do diff --git a/spec/defines/partial_import_spec.rb b/spec/defines/partial_import_spec.rb new file mode 100644 index 00000000..bdacf5cd --- /dev/null +++ b/spec/defines/partial_import_spec.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'keycloak::partial_import' do + on_supported_os.each do |os, facts| + context "on #{os}" do # rubocop:disable RSpec/MultipleMemoizedHelpers + let(:facts) do + facts.merge(concat_basedir: '/dne') + end + let(:version) { '21.0.1' } + let(:title) { 'test' } + let(:params) do + { + realm: 'myrealm', + if_resource_exists: 'OVERWRITE', + source: 'puppet:///modules/profile/keycloak/test.json' + } + end + let(:file_path) { "/opt/keycloak-#{version}/conf/#{title}.json" } + let(:command) do + [ + "/opt/keycloak-#{version}/bin/kcadm-wrapper.sh create partialImport", + "-r #{params[:realm]} -s ifResourceExists=#{params[:if_resource_exists]}", + "-o -f #{file_path}" + ].join(' ') + end + let(:pre_condition) do + <<-PP + keycloak_realm { #{params[:realm]}: + ensure => 'present', + } + PP + end + + it { is_expected.to compile.with_all_deps } + + it 'creates partial import JSON file' do + is_expected.to contain_file(file_path).with( + ensure: 'file', + owner: 'keycloak', + group: 'keycloak', + mode: '0600', + source: params[:source], + content: nil, + require: 'Class[Keycloak::Install]', + notify: "Exec[partial-import-#{title}]", + ) + end + + it 'creates exec for partial import' do + is_expected.to create_exec("partial-import-#{title}").with( + path: '/usr/bin:/bin:/usr/sbin:/sbin', + command: "#{command} || { rm -f #{file_path}; exit 1; }", + logoutput: 'true', + refreshonly: 'true', + require: 'Keycloak_conn_validator[keycloak]', + ) + end + + it { is_expected.to contain_keycloak_realm(params[:realm]).that_comes_before("Exec[partial-import-#{title}]") } + end + end +end diff --git a/spec/fixtures/partial-import.json b/spec/fixtures/partial-import.json new file mode 100644 index 00000000..cc69bf17 --- /dev/null +++ b/spec/fixtures/partial-import.json @@ -0,0 +1,44 @@ +{ + "clients": [ + { + "id": "test.example.com", + "clientId": "test.example.com", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "secret": "foobar", + "redirectUris": [ + "https://test.example.com", + "https://test.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": { + "post.logout.redirect.uris": "+" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": true, + "nodeReRegistrationTimeout": -1, + "defaultClientScopes": [ + "web-origins", + "roles", + "profile", + "groups", + "email" + ], + "optionalClientScopes": [ + "microprofile-jwt" + ] + } + ] +} \ No newline at end of file diff --git a/spec/spec_helper_acceptance_setup.rb b/spec/spec_helper_acceptance_setup.rb index adc760d4..7c3bce4e 100644 --- a/spec/spec_helper_acceptance_setup.rb +++ b/spec/spec_helper_acceptance_setup.rb @@ -14,6 +14,7 @@ proj_root = File.expand_path(File.join(File.dirname(__FILE__), '..')) scp_to(hosts, File.join(proj_root, 'spec/fixtures/DuoUniversalKeycloakAuthenticator-jar-with-dependencies.jar'), '/tmp/DuoUniversalKeycloakAuthenticator-jar-with-dependencies.jar') +scp_to(hosts, File.join(proj_root, 'spec/fixtures/partial-import.json'), '/tmp/partial-import.json') hiera_yaml = <<-HIERA_YAML ---