Skip to content

Commit

Permalink
Add keycloak::partial_import resource (#301)
Browse files Browse the repository at this point in the history
Fixes #229
  • Loading branch information
treydock authored Jul 16, 2023
1 parent dc2360a commit b99c30d
Show file tree
Hide file tree
Showing 9 changed files with 250 additions and 4 deletions.
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

0 comments on commit b99c30d

Please sign in to comment.