Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add keycloak::partial_import resource #301

Merged
merged 1 commit into from
Jul 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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.
Expand Down
16 changes: 14 additions & 2 deletions manifests/config.pp
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,26 @@
# - $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',
content => template('keycloak/kcadm-wrapper.sh.erb'),
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,
Expand All @@ -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',
Expand Down
18 changes: 16 additions & 2 deletions manifests/init.pp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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'],
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down
78 changes: 78 additions & 0 deletions manifests/partial_import.pp
Original file line number Diff line number Diff line change
@@ -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}"],
}
}
}
3 changes: 3 additions & 0 deletions manifests/resources.pp
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
}
}
13 changes: 13 additions & 0 deletions spec/acceptance/2_realm_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
64 changes: 64 additions & 0 deletions spec/defines/partial_import_spec.rb
Original file line number Diff line number Diff line change
@@ -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
44 changes: 44 additions & 0 deletions spec/fixtures/partial-import.json
Original file line number Diff line number Diff line change
@@ -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"
]
}
]
}
1 change: 1 addition & 0 deletions spec/spec_helper_acceptance_setup.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
---
Expand Down