Compare commits

...

9 Commits

Author SHA1 Message Date
7d0502ebc5 feat(keycloak): implement SPOT with Realm
Replace 01_import.yml with 01_initialize.yml (KEYCLOAK_HOST_IMPORT_DIR)
Add generic 02_update.yml (kcadm updater for clients/components)
- Resolve ID → read current → merge (kc_merge_path optional)
- Preserve immutable fields; support kc_force_attrs
Update tasks/main.yml:
- Readiness via KEYCLOAK_MASTER_REALM_URL; kcadm login
- Merge LDAP component config from Realm when KEYCLOAK_LDAP_ENABLED
- Update client settings incl. frontchannel.logout.url
realm.json.j2: include ldap.json in UserStorageProvider
ldap.json.j2: use KEYCLOAK_LDAP_* vars for bindDn/credential/connectionUrl
vars/main.yml: add KEYCLOAK_* URLs/dirs and KEYCLOAK_DICTIONARY_REALM(_RAW)
docker-compose.yml.j2: mount KEYCLOAK_HOST_IMPORT_DIR
Cleanup: remove 02_update_client_redirects.yml, 03_update-ldap-bind.yml, 04_ssh_public_key.yml; drop obsolete config flag; formatting

Note: redirectUris/webOrigins ordering may still cause changed=true; consider sorting for stability in a follow-up.
2025-08-17 14:27:33 +02:00
20c8d46f54 Keycloak import templates cleanup
- Removed all static 'id' fields from realm.json.j2, ldap.json.j2, and client.json.j2
- Replaced 'desktop-secret' with correct 'client-secret' authenticator type
- Standardized Jinja filters to use 'to_json' consistently
- Corrected defaultClientScopes entry from 'web-app-origins' to built-in 'web-origins'
- Verified LDAP mapper definitions and optional realm role mapping
- Ensured realm.json.j2 contains only required scopes

References: Chat with ChatGPT (2025-08-17)
https://chatgpt.com/share/68a1aaae-1b04-800f-aa8d-8a0ef6d33cba
2025-08-17 12:11:14 +02:00
a524c52f89 Created own ldap.json.j2 for better readability in keycloak 2025-08-17 11:49:50 +02:00
5c9ca20e04 Optimized keycloak variables 2025-08-17 11:40:15 +02:00
bfe18dd83c Refactor Keycloak role:
- Replace KEYCLOAK_KCADM_PATH with KEYCLOAK_EXEC_KCADM consistently
- Externalize client.json to separate Jinja2 template and include it in realm.json
- Simplify LDAP bind update to use explicit KEYCLOAK_LDAP_* vars
- Add async/poll support for long-running kcadm updates
- Restructure vars/main.yml: clearer grouping (General, Docker, Server, Update, LDAP, API)
- Compute redirectUris/webOrigins centrally in vars
- Align post.logout.redirect.uris handling with playbook

Conversation: https://chatgpt.com/share/68a1a11f-f8ac-800f-bada-cdc99a4fa1bf
2025-08-17 11:30:33 +02:00
0a83f3159a Updated keycloak variables 2025-08-17 10:47:40 +02:00
fb7b3a3c8e Added setting of frontchannel.logout.url for keycloak 2025-08-17 10:38:25 +02:00
42f9ebad34 Solved escaping bug 2025-08-17 09:35:19 +02:00
33b2d3f582 Optimized docker2local variables and constants 2025-08-17 09:26:46 +02:00
36 changed files with 603 additions and 1047 deletions

View File

@@ -4,5 +4,5 @@ OnFailure=sys-alm-compose.{{ SOFTWARE_NAME }}@%n.service sys-cln-faild-bkps{{ SY
[Service] [Service]
Type=oneshot Type=oneshot
ExecStartPre=/bin/sh -c '/usr/bin/python {{ PATH_SYSTEM_LOCK_SCRIPT }} {{ system_maintenance_services | join(' ') }} --ignore {{system_maintenance_backup_services| join(' ') }} --timeout "{{system_maintenance_lock_timeout_backup_services}}"' ExecStartPre=/bin/sh -c '/usr/bin/python {{ PATH_SYSTEM_LOCK_SCRIPT }} {{ system_maintenance_services | join(' ') }} --ignore {{system_maintenance_backup_services| join(' ') }} --timeout "{{ system_maintenance_lock_timeout_backup_services }}"'
ExecStart=/bin/sh -c '/usr/bin/bash {{docker_backup_remote_to_local_folder}}sys-bkp-rmt-2-loc-multi-provider.sh' ExecStart=/bin/sh -c '/usr/bin/bash {{docker_backup_remote_to_local_folder}}sys-bkp-rmt-2-loc-multi-provider.sh'

View File

@@ -3,7 +3,7 @@
# GENERAL # GENERAL
## Admin (Data) ## Admin (Data)
LDAP_ADMIN_USERNAME= {{applications | get_app_conf(application_id, 'users.administrator.username', True)}} # LDAP database admin user. LDAP_ADMIN_USERNAME= {{ applications | get_app_conf(application_id, 'users.administrator.username') }} # LDAP database admin user.
LDAP_ADMIN_PASSWORD= {{ldap.bind_credential}} # LDAP database admin password. LDAP_ADMIN_PASSWORD= {{ldap.bind_credential}} # LDAP database admin password.
## Users ## Users
@@ -14,8 +14,8 @@ LDAP_ROOT= {{ldap.dn.root}} # LDAP baseDN (or su
## Admin (Config) ## Admin (Config)
LDAP_ADMIN_DN= {{ldap.dn.administrator.data}} LDAP_ADMIN_DN= {{ldap.dn.administrator.data}}
LDAP_CONFIG_ADMIN_ENABLED= yes LDAP_CONFIG_ADMIN_ENABLED= yes
LDAP_CONFIG_ADMIN_USERNAME= {{applications | get_app_conf(application_id, 'users.administrator.username', True)}} LDAP_CONFIG_ADMIN_USERNAME= {{ applications | get_app_conf(application_id, 'users.administrator.username') }}
LDAP_CONFIG_ADMIN_PASSWORD= {{applications | get_app_conf(application_id, 'credentials.administrator_password', True)}} LDAP_CONFIG_ADMIN_PASSWORD= {{ applications | get_app_conf(application_id, 'credentials.administrator_password') }}
# Network # Network
LDAP_PORT_NUMBER= {{openldap_docker_port_open}} # Route to default port LDAP_PORT_NUMBER= {{openldap_docker_port_open}} # Route to default port

View File

@@ -1,9 +1,9 @@
- name: "reload sys-bkp-docker-2-loc-everything service" - name: "reload backup docker to local (all) service"
systemd: systemd:
name: sys-bkp-docker-2-loc-everything{{ SYS_SERVICE_SUFFIX }} name: "{{ BKP_DOCKER_2_LOC_SERVICE_ALL }}"
daemon_reload: yes daemon_reload: yes
- name: "reload sys-bkp-docker-2-loc service" - name: "reload backup docker to local service"
systemd: systemd:
name: sys-bkp-docker-2-loc{{ SYS_SERVICE_SUFFIX }} name: "{{ BKP_DOCKER_2_LOC_SERVICE }}"
daemon_reload: yes daemon_reload: yes

View File

@@ -14,17 +14,17 @@
include_tasks: 03_reset.yml include_tasks: 03_reset.yml
when: MODE_RESET | bool when: MODE_RESET | bool
- name: configure sys-bkp-docker-2-loc-everything service - name: "setup '{{ BKP_DOCKER_2_LOC_SERVICE_ALL }}'"
template: template:
src: sys-bkp-docker-2-loc-everything.service.j2 src: "{{ role_name }}-everything.service.j2"
dest: /etc/systemd/system/sys-bkp-docker-2-loc-everything{{ SYS_SERVICE_SUFFIX }} dest: /etc/systemd/system/{{ BKP_DOCKER_2_LOC_SERVICE_ALL }}
notify: reload sys-bkp-docker-2-loc-everything service notify: reload backup docker to local (all) service
- name: configure sys-bkp-docker-2-loc{{ SYS_SERVICE_SUFFIX }} - name: "setup '{{ BKP_DOCKER_2_LOC_SERVICE }}'"
template: template:
src: sys-bkp-docker-2-loc.service.j2 src: "{{ role_name }}.service.j2"
dest: /etc/systemd/system/sys-bkp-docker-2-loc{{ SYS_SERVICE_SUFFIX }} dest: /etc/systemd/system/{{ BKP_DOCKER_2_LOC_SERVICE }}
notify: reload sys-bkp-docker-2-loc service notify: reload backup docker to local service
- name: "set 'service_name' to '{{ role_name }}'" - name: "set 'service_name' to '{{ role_name }}'"
set_fact: set_fact:

View File

@@ -1,12 +1,12 @@
- block: - block:
- name: "pkgmgr install {{ bkp_docker_2_loc_pkg }}" - name: "pkgmgr install {{ BKP_DOCKER_2_LOC_PKG }}"
include_role: include_role:
name: pkgmgr-install name: pkgmgr-install
vars: vars:
package_name: "{{ bkp_docker_2_loc_pkg }}" package_name: "{{ BKP_DOCKER_2_LOC_PKG }}"
- name: "Retrieve {{ bkp_docker_2_loc_pkg }} path from pkgmgr" - name: "Retrieve {{ BKP_DOCKER_2_LOC_PKG }} path from pkgmgr"
command: "pkgmgr path {{ bkp_docker_2_loc_pkg }}" command: "pkgmgr path {{ BKP_DOCKER_2_LOC_PKG }}"
register: pkgmgr_output register: pkgmgr_output
changed_when: false changed_when: false
@@ -16,4 +16,4 @@
changed_when: false changed_when: false
when: backup_docker_to_local_folder is not defined when: backup_docker_to_local_folder is not defined
vars: vars:
bkp_docker_2_loc_pkg: backup-docker-to-local BKP_DOCKER_2_LOC_PKG: backup-docker-to-local

View File

@@ -55,10 +55,10 @@
database_name is defined and database_name is defined and
database_username is defined and database_username is defined and
database_password is defined) and database_password is defined) and
run_once_bkp_docker_2_loc_file_permission is not defined run_once_sys_bkp_docker_2_loc_file_permission is not defined
register: file_permission_result register: file_permission_result
- name: run the backup_docker_to_local_file_permission tasks once - name: run the backup_docker_to_local_file_permission tasks once
set_fact: set_fact:
run_once_bkp_docker_2_loc_file_permission: true run_once_sys_bkp_docker_2_loc_file_permission: true
when: run_once_bkp_docker_2_loc_file_permission is not defined and file_permission_result is defined and file_permission_result.changed when: run_once_sys_bkp_docker_2_loc_file_permission is not defined and file_permission_result is defined and file_permission_result.changed

View File

@@ -7,4 +7,4 @@
- name: "include 04_seed-database-to-backup.yml" - name: "include 04_seed-database-to-backup.yml"
include_tasks: 04_seed-database-to-backup.yml include_tasks: 04_seed-database-to-backup.yml
when: when:
- bkp_docker_2_loc_db_enabled | bool - BKP_DOCKER_2_LOC_DB_ENABLED | bool

View File

@@ -4,6 +4,6 @@ OnFailure=sys-alm-compose.{{ SOFTWARE_NAME }}@%n.service sys-cln-faild-bkps{{ SY
[Service] [Service]
Type=oneshot Type=oneshot
ExecStartPre=/bin/sh -c '/usr/bin/python {{ PATH_SYSTEM_LOCK_SCRIPT }} {{ system_maintenance_services | join(' ') }} --ignore {{ system_maintenance_backup_services | reject('equalto', 'sys-bkp-docker-2-loc') | join(' ') }} --timeout "{{system_maintenance_lock_timeout_backup_services}}"' ExecStartPre=/bin/sh -c '/usr/bin/python {{ PATH_SYSTEM_LOCK_SCRIPT }} {{ system_maintenance_services | join(' ') }} --ignore {{ system_maintenance_backup_services | reject('equalto', role_name ) | join(' ') }} --timeout "{{ system_maintenance_lock_timeout_backup_services }}"'
ExecStart=/bin/sh -c '{{ bkp_docker_2_loc_exec }} --everything' ExecStart=/bin/sh -c '{{ BKP_DOCKER_2_LOC_EXEC }} --everything'
ExecStartPost=/bin/sh -c '/bin/systemctl start sys-rpr-docker-soft{{ SYS_SERVICE_SUFFIX }} &' ExecStartPost=/bin/sh -c '/bin/systemctl start sys-rpr-docker-soft{{ SYS_SERVICE_SUFFIX }} &'

View File

@@ -4,6 +4,6 @@ OnFailure=sys-alm-compose.{{ SOFTWARE_NAME }}@%n.service sys-cln-faild-bkps{{ SY
[Service] [Service]
Type=oneshot Type=oneshot
ExecStartPre=/bin/sh -c '/usr/bin/python {{ PATH_SYSTEM_LOCK_SCRIPT }} {{ system_maintenance_services | join(' ') }} --ignore {{ system_maintenance_backup_services | reject('equalto', 'sys-bkp-docker-2-loc-everything') | join(' ') }} --timeout "{{system_maintenance_lock_timeout_backup_services}}"' ExecStartPre=/bin/sh -c '/usr/bin/python {{ PATH_SYSTEM_LOCK_SCRIPT }} {{ system_maintenance_services | join(' ') }} --ignore {{ system_maintenance_backup_services | reject('equalto', role_name ~ '-everything') | join(' ') }} --timeout "{{ system_maintenance_lock_timeout_backup_services }}"'
ExecStart=/bin/sh -c '{{ bkp_docker_2_loc_exec }}' ExecStart=/bin/sh -c '{{ BKP_DOCKER_2_LOC_EXEC }}'
ExecStartPost=/bin/sh -c '/bin/systemctl start sys-rpr-docker-soft{{ SYS_SERVICE_SUFFIX }} &' ExecStartPost=/bin/sh -c '/bin/systemctl start sys-rpr-docker-soft{{ SYS_SERVICE_SUFFIX }} &'

View File

@@ -1,46 +1,50 @@
# Mapping logic for backup-docker-to-local CLI arguments # Mapping logic for backup-docker-to-local CLI arguments
# #
# - bkp_docker_2_loc_database_routine: All service names where backup.database_routine is set (for --database-containers) # - BKP_DOCKER_2_LOC_DB_ROUTINE: All service names where backup.database_routine is set (for --database-containers)
# - bkp_docker_2_loc_no_stop_required: All images where backup.no_stop_required is set (for --images-no-stop-required) # - BKP_DOCKER_2_LOC_NO_STOP_REQUIRED: All images where backup.no_stop_required is set (for --images-no-stop-required)
# - bkp_docker_2_loc_disabled: All images where backup.disabled is set (for --images-no-backup-required) # - BKP_DOCKER_2_LOC_DISABLED: All images where backup.disabled is set (for --images-no-backup-required)
# CLI-ready variables render these lists as argument strings. # CLI-ready variables render these lists as argument strings.
BKP_DOCKER_2_LOC_SERVICE: "{{ role_name ~ SYS_SERVICE_SUFFIX }}"
BKP_DOCKER_2_LOC_SERVICE_ALL: "{{ role_name }}-everything{{ SYS_SERVICE_SUFFIX }}"
# Verify if DB is enabled # Verify if DB is enabled
bkp_docker_2_loc_db_enabled: "{{ database_type | default('') | bool }}" BKP_DOCKER_2_LOC_DB_ENABLED: "{{ database_type | default('') | bool }}"
# Gather mapped values as lists # Gather mapped values as lists
bkp_docker_2_loc_database_routine: >- BKP_DOCKER_2_LOC_DB_ROUTINE: >-
{{ applications | find_dock_val_by_bkp_entr('database_routine', 'name') | list }} {{ applications | find_dock_val_by_bkp_entr('database_routine', 'name') | list }}
bkp_docker_2_loc_no_stop_required: >- BKP_DOCKER_2_LOC_NO_STOP_REQUIRED: >-
{{ applications | find_dock_val_by_bkp_entr('no_stop_required', 'image') | list }} {{ applications | find_dock_val_by_bkp_entr('no_stop_required', 'image') | list }}
bkp_docker_2_loc_disabled: >- BKP_DOCKER_2_LOC_DISABLED: >-
{{ applications | find_dock_val_by_bkp_entr('disabled', 'image') | list }} {{ applications | find_dock_val_by_bkp_entr('disabled', 'image') | list }}
# CLI argument strings (only set if list not empty) # CLI argument strings (only set if list not empty)
bkp_docker_2_loc_database_routine_cli: >- BKP_DOCKER_2_LOC_DB_ROUTINE_CLI: >-
{% if bkp_docker_2_loc_database_routine | length > 0 -%} {% if BKP_DOCKER_2_LOC_DB_ROUTINE | length > 0 -%}
--database-containers {{ bkp_docker_2_loc_database_routine | join(' ') }} --database-containers {{ BKP_DOCKER_2_LOC_DB_ROUTINE | join(' ') }}
{%- endif %} {%- endif %}
bkp_docker_2_loc_no_stop_required_cli: >- BKP_DOCKER_2_LOC_NO_STOP_REQUIRED_CLI: >-
{% if bkp_docker_2_loc_no_stop_required | length > 0 -%} {% if BKP_DOCKER_2_LOC_NO_STOP_REQUIRED | length > 0 -%}
--images-no-stop-required {{ bkp_docker_2_loc_no_stop_required | join(' ') }} --images-no-stop-required {{ BKP_DOCKER_2_LOC_NO_STOP_REQUIRED | join(' ') }}
{%- endif %} {%- endif %}
bkp_docker_2_loc_disabled_cli: >- BKP_DOCKER_2_LOC_DISABLED_CLI: >-
{% if bkp_docker_2_loc_disabled | length > 0 -%} {% if BKP_DOCKER_2_LOC_DISABLED | length > 0 -%}
--images-no-backup-required {{ bkp_docker_2_loc_disabled | join(' ') }} --images-no-backup-required {{ BKP_DOCKER_2_LOC_DISABLED | join(' ') }}
{%- endif %} {%- endif %}
# List of CLI args for convenience (e.g. for looping or joining) # List of CLI args for convenience (e.g. for looping or joining)
bkp_docker_2_loc_cli_args_list: BKP_DOCKER_2_LOC_CLI_ARGS_LIST:
- "{{ bkp_docker_2_loc_database_routine_cli }}" - "{{ BKP_DOCKER_2_LOC_DB_ROUTINE_CLI }}"
- "{{ bkp_docker_2_loc_no_stop_required_cli }}" - "{{ BKP_DOCKER_2_LOC_NO_STOP_REQUIRED_CLI }}"
- "{{ bkp_docker_2_loc_disabled_cli }}" - "{{ BKP_DOCKER_2_LOC_DISABLED_CLI }}"
bkp_docker_2_loc_exec: >- BKP_DOCKER_2_LOC_EXEC: >-
/usr/bin/python {{ backup_docker_to_local_folder }}backup-docker-to-local.py /usr/bin/python {{ backup_docker_to_local_folder }}backup-docker-to-local.py
--compose-dir {{ PATH_DOCKER_COMPOSE_INSTANCES }} --compose-dir {{ PATH_DOCKER_COMPOSE_INSTANCES }}
{{ bkp_docker_2_loc_cli_args_list | select('string') | join(' ') }} {{ BKP_DOCKER_2_LOC_CLI_ARGS_LIST | select('string') | join(' ') }}

View File

@@ -4,5 +4,5 @@ OnFailure=sys-alm-compose.{{ SOFTWARE_NAME }}@%n.service
[Service] [Service]
Type=oneshot Type=oneshot
ExecStartPre=/bin/sh -c '/usr/bin/python {{ PATH_SYSTEM_LOCK_SCRIPT }} {{ system_maintenance_services | join(' ') }} --ignore {{system_maintenance_cleanup_services| join(' ') }} --timeout "{{system_maintenance_lock_timeout_backup_services}}"' ExecStartPre=/bin/sh -c '/usr/bin/python {{ PATH_SYSTEM_LOCK_SCRIPT }} {{ system_maintenance_services | join(' ') }} --ignore {{system_maintenance_cleanup_services| join(' ') }} --timeout "{{ system_maintenance_lock_timeout_backup_services }}"'
ExecStart=/bin/sh -c '/usr/bin/python {{cleanup_backups_directory}}sys-cln-backups.py --backups-folder-path {{backups_folder_path}} --maximum-backup-size-percent {{size_percent_maximum_backup}}' ExecStart=/bin/sh -c '/usr/bin/python {{cleanup_backups_directory}}sys-cln-backups.py --backups-folder-path {{backups_folder_path}} --maximum-backup-size-percent {{size_percent_maximum_backup}}'

View File

@@ -4,5 +4,5 @@ OnFailure=sys-alm-compose.{{ SOFTWARE_NAME }}@%n.service
[Service] [Service]
Type=oneshot Type=oneshot
ExecStartPre=/bin/sh -c '/usr/bin/python {{ PATH_SYSTEM_LOCK_SCRIPT }} {{ system_maintenance_services | join(' ') }} --ignore {{system_maintenance_cleanup_services| join(' ') }} --timeout "{{system_maintenance_lock_timeout_backup_services}}"' ExecStartPre=/bin/sh -c '/usr/bin/python {{ PATH_SYSTEM_LOCK_SCRIPT }} {{ system_maintenance_services | join(' ') }} --ignore {{system_maintenance_cleanup_services| join(' ') }} --timeout "{{ system_maintenance_lock_timeout_backup_services }}"'
ExecStart=/bin/sh -c '/bin/bash {{cleanup_disc_space_folder}}sys-cln-disc-space.sh {{size_percent_cleanup_disc_space}}' ExecStart=/bin/sh -c '/bin/bash {{cleanup_disc_space_folder}}sys-cln-disc-space.sh {{size_percent_cleanup_disc_space}}'

View File

@@ -19,7 +19,7 @@ CRON_DISABLED=true
# ------------------------------------------------ # ------------------------------------------------
# Initial admin account # Initial admin account
# ------------------------------------------------ # ------------------------------------------------
ESPOCRM_ADMIN_USERNAME={{ applications | get_app_conf(application_id, 'users.administrator.username', True) }} ESPOCRM_ADMIN_USERNAME={{ applications | get_app_conf(application_id, 'users.administrator.username') }}
ESPOCRM_ADMIN_PASSWORD={{ applications | get_app_conf(application_id, 'credentials.administrator_password', True) }} ESPOCRM_ADMIN_PASSWORD={{ applications | get_app_conf(application_id, 'credentials.administrator_password', True) }}
# Public base URL of the EspoCRM instance # Public base URL of the EspoCRM instance

View File

@@ -2,7 +2,7 @@
# https://github.com/LDAPAccountManager/lam/blob/develop/lam-packaging/docker/.env # https://github.com/LDAPAccountManager/lam/blob/develop/lam-packaging/docker/.env
# Basic Configuration # Basic Configuration
LAM_PASSWORD= {{applications | get_app_conf(application_id, 'credentials.administrator_password', True)}} # LAM configuration master password and password for server profile "lam LAM_PASSWORD= {{ applications | get_app_conf(application_id, 'credentials.administrator_password') }} # LAM configuration master password and password for server profile "lam
# Database # Database
LAM_CONFIGURATION_DATABASE= files # configuration database (files or mysql) @todo implement mariadb LAM_CONFIGURATION_DATABASE= files # configuration database (files or mysql) @todo implement mariadb

View File

@@ -1,10 +1,9 @@
actions: actions:
import_realm: True # Import REALM import_realm: True # Import REALM
update_ldap_bind: True # Updates LDAP binds
features: features:
matomo: true matomo: true
css: true css: true
desktop: true desktop: true
ldap: true ldap: true
central_database: true central_database: true
recaptcha: true recaptcha: true

View File

@@ -1,19 +0,0 @@
- name: "load variables from {{ DOCKER_VARS_FILE }}"
include_vars: "{{ DOCKER_VARS_FILE }}"
- name: Set the directory to which keycloak import files will be copied on host
set_fact:
keycloak_host_import_directory: "{{ docker_compose.directories.volumes }}import/"
- name: "create directory {{ keycloak_host_import_directory }}"
file:
path: "{{ keycloak_host_import_directory }}"
state: directory
mode: "0755"
- name: "Copy import files to {{ keycloak_host_import_directory }}"
template:
src: "{{ item }}"
dest: "{{ keycloak_host_import_directory }}/{{ item | basename | regex_replace('\\.j2$', '') }}"
mode: "0770"
loop: "{{ lookup('fileglob', role_path ~ '/templates/import/*.j2', wantlist=True) }}"

View File

@@ -0,0 +1,15 @@
- name: "load variables from {{ DOCKER_VARS_FILE }}"
include_vars: "{{ DOCKER_VARS_FILE }}"
- name: "create directory {{ KEYCLOAK_HOST_IMPORT_DIR }}"
file:
path: "{{ KEYCLOAK_HOST_IMPORT_DIR }}"
state: directory
mode: "0755"
- name: "Copy import files to {{ KEYCLOAK_HOST_IMPORT_DIR }}"
template:
src: "{{ item }}"
dest: "{{ KEYCLOAK_HOST_IMPORT_DIR }}/{{ item | basename | regex_replace('\\.j2$', '') }}"
mode: "0770"
loop: "{{ lookup('fileglob', role_path ~ '/templates/import/*.j2', wantlist=True) }}"

View File

@@ -0,0 +1,107 @@
# Generic updater for Keycloak client/component via kcadm.
# Flow: resolve ID → read current object → merge with desired → preserve immutable fields → update via stdin.
#
# Required vars (pass via include):
# - kc_object_kind: "client" | "component"
# - kc_lookup_value: e.g., KEYCLOAK_CLIENT_ID or KEYCLOAK_LDAP_CMP_NAME
# - kc_desired: dict, e.g., KEYCLOAK_DICTIONARY_CLIENT or KEYCLOAK_DICTIONARY_LDAP
#
# Optional:
# - kc_lookup_field: override lookup field (defaults: clientId for client, name for component)
# - kc_merge_path: if set (e.g. "config"), only that subkey is merged
# - kc_force_attrs: dict to force on the final payload (merged last)
- name: Assert required vars
assert:
that:
- kc_object_kind in ['client','component']
- kc_lookup_value is defined
- kc_desired is defined
fail_msg: "kc_object_kind, kc_lookup_value, kc_desired are required."
- name: Derive API endpoint and lookup field
set_fact:
kc_api: "{{ 'clients' if kc_object_kind == 'client' else 'components' }}"
kc_lookup_field_eff: "{{ 'clientId' if kc_object_kind == 'client' else (kc_lookup_field | default('name')) }}"
- name: Resolve object id
shell: >
{{ KEYCLOAK_EXEC_KCADM }} get {{ kc_api }}
-r {{ KEYCLOAK_REALM }}
--query '{{ kc_lookup_field_eff }}={{ kc_lookup_value }}'
--fields id --format json | jq -r '.[0].id'
register: kc_obj_id
changed_when: false
- name: Fail if object not found
assert:
that:
- (kc_obj_id.stdout | trim) != ''
- (kc_obj_id.stdout | trim) != 'null'
fail_msg: "{{ kc_object_kind | capitalize }} '{{ kc_lookup_value }}' not found."
- name: Read current object
shell: >
{{ KEYCLOAK_EXEC_KCADM }} get {{ kc_api }}/{{ kc_obj_id.stdout }}
-r {{ KEYCLOAK_REALM }} --format json
register: kc_cur
changed_when: false
# ── Build merge payload safely (avoid evaluating kc_desired[kc_merge_path] when undefined) ─────────
- name: Parse current object
set_fact:
cur_obj: "{{ kc_cur.stdout | from_json }}"
- name: Prepare merge payload (subpath)
when: kc_merge_path is defined and (kc_merge_path | length) > 0
set_fact:
merge_payload: "{{ { (kc_merge_path): (kc_desired[kc_merge_path] | default({}, true)) } }}"
- name: Prepare merge payload (full object)
when: kc_merge_path is not defined or (kc_merge_path | length) == 0
set_fact:
merge_payload: "{{ kc_desired }}"
- name: Build desired object (base merge)
set_fact:
desired_obj: "{{ cur_obj | combine(merge_payload, recursive=True) }}"
# Preserve immutable fields
- name: Preserve immutable fields for client
when: kc_object_kind == 'client'
set_fact:
desired_obj: >-
{{
desired_obj
| combine({
'id': cur_obj.id,
'clientId': cur_obj.clientId
}, recursive=True)
}}
- name: Preserve immutable fields for component
when: kc_object_kind == 'component'
set_fact:
desired_obj: >-
{{
desired_obj
| combine({
'id': cur_obj.id,
'providerId': cur_obj.providerId,
'providerType': cur_obj.providerType,
'parentId': cur_obj.parentId
}, recursive=True)
}}
# Optional forced attributes (e.g., frontchannelLogout)
- name: Apply forced attributes (optional)
when: kc_force_attrs is defined
set_fact:
desired_obj: "{{ desired_obj | combine(kc_force_attrs, recursive=True) }}"
- name: Update object via stdin
shell: |
cat <<'JSON' | {{ KEYCLOAK_EXEC_KCADM }} update {{ kc_api }}/{{ kc_obj_id.stdout }} -r {{ KEYCLOAK_REALM }} -f -
{{ desired_obj | to_json }}
JSON

View File

@@ -1,118 +0,0 @@
---
# Update redirectUris/webOrigins per kcadm.sh — no defaults used.
# ── REQUIRED VARS (must be provided by caller) ───────────────────────────────
# - WEB_PROTOCOL e.g. "https"
# - keycloak_realm target realm name
# - keycloak_server_host_url e.g. "http://127.0.0.1:8080"
# - keycloak_server_internal_url e.g. "http://127.0.0.1:8080"
# - keycloak_kcadm_path e.g. "docker exec -i keycloak /opt/keycloak/bin/kcadm.sh"
# - keycloak_master_api_user_name
# - keycloak_master_api_user_password
# - keycloak_client_id clientId to update (e.g. same as realm or an app client)
# - domains your domain map
# - applications your applications map
- name: "Assert required variables are present (no defaults allowed)"
assert:
that:
- WEB_PROTOCOL is defined
- keycloak_realm is defined
- keycloak_server_host_url is defined
- keycloak_server_internal_url is defined
- keycloak_kcadm_path is defined
- keycloak_master_api_user_name is defined
- keycloak_master_api_user_password is defined
- keycloak_client_id is defined
- keycloak_redirect_features is defined
- domains is defined
- applications is defined
fail_msg: "Missing required variable(s). Provide all vars listed at the top of 02_update_client_redirects.yml."
- name: "kcadm login"
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
shell: >
{{ keycloak_kcadm_path }} config credentials
--server {{ keycloak_server_internal_url }}
--realm master
--user {{ keycloak_master_api_user_name }}
--password {{ keycloak_master_api_user_password }}
changed_when: false
# 1) Build desired sets (NO defaults)
- name: "Build desired redirect URIs from config via filter"
set_fact:
kc_redirect_uris: >-
{{ domains | redirect_uris(applications, WEB_PROTOCOL, '/*', keycloak_redirect_features, True) }}
- name: Build desired web origins (scheme://host[:port])
set_fact:
kc_web_origins: >-
{{ kc_redirect_uris
| map('regex_replace','/\\*$','')
| map('regex_search','^(https?://[^/]+)')
| select('string')
| list | unique }}
#- name: "Build post.logout.redirect.uris value ('+' plus explicit URIs without /*)"
# set_fact:
# kc_desired_post_logout_uris: >-
# {{ (['+'] + (kc_redirect_uris | map('regex_replace','/\\*$','') | list)) | join('\n') }}
# 2) Resolve client id (strictly by provided clientId, no fallback)
- name: "Resolve client internal id for {{ keycloak_client_id }}"
shell: >
{{ keycloak_kcadm_path }} get clients
-r {{ keycloak_realm }}
--query 'clientId={{ keycloak_client_id }}'
--fields id --format json | jq -r '.[0].id'
register: kc_client
changed_when: false
- name: "Fail if client not found"
assert:
that: kc_client.stdout is match('^[0-9a-f-]+$')
fail_msg: "Client '{{ keycloak_client_id }}' not found in realm '{{ keycloak_realm }}'."
# 3) Read current config (assume keys exist; we don't use defaults)
- name: "Read current client configuration"
shell: >
{{ keycloak_kcadm_path }} get clients/{{ kc_client.stdout }}
-r {{ keycloak_realm }} --format json
register: kc_client_obj
changed_when: false
- name: "Normalize current vs desired for comparison"
set_fact:
kc_current_redirect_uris: "{{ (kc_client_obj.stdout | from_json).redirectUris | sort }}"
kc_current_web_origins: "{{ (kc_client_obj.stdout | from_json).webOrigins | sort }}"
kc_current_logout_uris: >-
{{
(
(kc_client_obj.stdout | from_json).attributes['post.logout.redirect.uris']
if 'post.logout.redirect.uris' in (kc_client_obj.stdout | from_json).attributes
else ''
)
| regex_replace('\r','')
| split('\n')
| reject('equalto','')
| list | sort
}}
kc_desired_redirect_uris: "{{ kc_redirect_uris | sort }}"
kc_desired_web_origins: "{{ kc_web_origins | sort }}"
kc_desired_post_logout_uris: "+"
kc_desired_post_logout_uris_list: >-
{{ "+" | split('\n') | reject('equalto','') | list | sort }}
# 4) Update only when changed
- name: "Update redirectUris, webOrigins, post.logout.redirect.uris"
shell: >
{{ keycloak_kcadm_path }} update clients/{{ kc_client.stdout }}
-r {{ keycloak_realm }}
-s 'redirectUris={{ kc_redirect_uris | to_json }}'
-s 'webOrigins={{ kc_web_origins | to_json }}'
-s 'attributes."post.logout.redirect.uris"={{ kc_desired_post_logout_uris | to_json }}'
when: kc_current_redirect_uris != kc_desired_redirect_uris
or kc_current_web_origins != kc_desired_web_origins
or kc_current_logout_uris != kc_desired_post_logout_uris_list

View File

@@ -1,118 +0,0 @@
---
# Idempotent update of Keycloak LDAP provider:
# - bindDn
# - bindCredential
# - connectionUrl
#
# STRICT: Uses ONLY values from ldap.* (no computed defaults)
# - ldap.dn.administrator.data
# - ldap.bind_credential
# - ldap.server.uri
- name: "Assert required vars exist (strict: use ldap.* only, no defaults)"
assert:
that:
- keycloak_realm is defined
- keycloak_container is defined
- keycloak_server_internal_url is defined
- keycloak_master_api_user_name is defined
- keycloak_master_api_user_password is defined
- keycloak_ldap_component_name is defined
- ldap is defined
- ldap.dn.administrator is defined
- ldap.dn.administrator.data is defined
- ldap.bind_credential is defined
- ldap.server is defined
- ldap.server.uri is defined
fail_msg: >-
Missing required Keycloak/LDAP variables. Ensure ldap.dn.administrator.data,
ldap.bind_credential, and ldap.server.uri are defined.
# Build a base argv for kcadm to avoid fragile shell quoting
- name: "Build kcadm argv base"
set_fact:
kcadm_argv_base:
- docker
- exec
- -i
- "{{ keycloak_container }}"
- /opt/keycloak/bin/kcadm.sh
- name: "kcadm login (master)"
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
command:
argv: "{{ kcadm_argv_base
+ ['config', 'credentials',
'--server', keycloak_server_internal_url,
'--realm', 'master',
'--user', keycloak_master_api_user_name,
'--password', keycloak_master_api_user_password] }}"
changed_when: false
# Resolve the LDAP component *by name* to avoid picking the wrong one.
- name: "Resolve LDAP component id by name '{{ keycloak_ldap_component_name }}'"
command:
argv: "{{ kcadm_argv_base
+ ['get', 'components',
'-r', keycloak_realm,
'--query', 'name=' ~ keycloak_ldap_component_name,
'--fields', 'id,name,providerId,config',
'--format', 'json'] }}"
register: kc_ldap_list
changed_when: false
- name: "Validate that exactly one LDAP component matched"
vars:
parsed: "{{ kc_ldap_list.stdout | from_json }}"
assert:
that:
- (parsed | length) == 1
fail_msg: >-
Expected exactly one LDAP component named '{{ keycloak_ldap_component_name }}',
found {{ (kc_ldap_list.stdout | from_json) | length }}.
- name: "Extract current LDAP component values"
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
set_fact:
kc_ldap_component_id: "{{ (kc_ldap_list.stdout | from_json)[0].id }}"
kc_ldap_current_bind_dn: "{{ ((kc_ldap_list.stdout | from_json)[0].config['bindDn'] | default(['']))[0] }}"
kc_ldap_current_bind_pw: "{{ ((kc_ldap_list.stdout | from_json)[0].config['bindCredential'] | default(['']))[0] }}"
kc_ldap_current_connection_url: "{{ ((kc_ldap_list.stdout | from_json)[0].config['connectionUrl'] | default(['']))[0] }}"
# Desired values come STRICTLY from ldap.*
- name: "Set desired LDAP values (strict from ldap.*)"
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
set_fact:
kc_desired_bind_dn: "{{ ldap.dn.administrator.data }}"
kc_desired_bind_pw: "{{ ldap.bind_credential }}"
kc_desired_connection_url: "{{ ldap.server.uri }}"
- name: "Determine if update is required"
set_fact:
kc_needs_update: >-
{{
(kc_ldap_current_bind_dn != kc_desired_bind_dn)
or (kc_ldap_current_bind_pw != kc_desired_bind_pw)
or (kc_ldap_current_connection_url != kc_desired_connection_url)
}}
# Pass each -s as a single argv token with valid JSON (arrays), zero shell quoting issues.
- name: "Update LDAP bindDn / bindCredential / connectionUrl (strict, argv)"
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
command:
argv: "{{ kcadm_argv_base
+ ['update', 'components/' ~ kc_ldap_component_id,
'-r', keycloak_realm,
'-s', 'config.bindDn=' ~ ([kc_desired_bind_dn] | to_json),
'-s', 'config.bindCredential=' ~ ([kc_desired_bind_pw] | to_json),
'-s', 'config.connectionUrl=' ~ ([kc_desired_connection_url] | to_json)
] }}"
when: kc_needs_update | bool
register: kc_bind_update
- name: "LDAP provider updated"
debug:
msg: "LDAP bindDn/bindCredential/connectionUrl updated on component {{ keycloak_ldap_component_name }}."
when:
- kc_bind_update is defined
- kc_bind_update.rc == 0

View File

@@ -1,71 +0,0 @@
# Configure Credentials
- name: Ensure Keycloak CLI credentials are configured
shell: |
{{ keycloak_kcadm_path }} config credentials \
--server {{ keycloak_server_internal_url }} \
--realm master \
--user {{ keycloak_master_api_user_name }} \
--password {{ keycloak_master_api_user_password }}
# LDAP Source
- name: Get ID of LDAP storage provider
shell: |
{{ keycloak_kcadm_path }} get components \
-r {{ keycloak_realm }} \
--query 'providerId=ldap' \
--fields id,name \
--format json
register: ldap_components
- name: Extract LDAP component ID
set_fact:
ldap_component_id: "{{ (ldap_components.stdout | from_json)[0].id }}"
- name: Ensure {{ ldap.user.attributes.ssh_public_key }} LDAP Mapper exists
shell: |
docker exec -i keycloak_application bash -c '
/opt/keycloak/bin/kcadm.sh get components -r {{ keycloak_realm }} \
| grep -q "\"name\" : \"{{ ldap.user.attributes.ssh_public_key }}\"" \
|| printf "%s\n" "{
\"name\": \"{{ ldap.user.attributes.ssh_public_key }}\",
\"parentId\": \"{{ ldap_component_id }}\",
\"providerId\": \"user-attribute-ldap-mapper\",
\"providerType\": \"org.keycloak.storage.ldap.mappers.LDAPStorageMapper\",
\"config\": {
\"user.model.attribute\": [\"{{ ldap.user.attributes.ssh_public_key }}\"],
\"ldap.attribute\": [\"{{ ldap.user.attributes.ssh_public_key }}\"],
\"read.only\": [\"false\"],
\"write.only\": [\"true\"],
\"always.read.value.from.ldap\": [\"false\"],
\"multivalued\": [\"true\"]
}
}" | /opt/keycloak/bin/kcadm.sh create components -r {{ keycloak_realm }} -f -'
register: mapper_create
changed_when: mapper_create.rc == 0 and mapper_create.stdout != ""
# GUI
- name: Enable user profile in realm
shell: >
{{ keycloak_kcadm_path }} update realms/{{ keycloak_realm }}
-s 'attributes.userProfileEnabled=true'
- name: Re-authenticate to Keycloak after enabling user profile
shell: |
{{ keycloak_kcadm_path }} config credentials \
--server {{ keycloak_server_internal_url }} \
--realm master \
--user {{ keycloak_master_api_user_name }} \
--password {{ keycloak_master_api_user_password }}
- name: Render user-profile JSON for SSH key
template:
src: import/user-profile.json.j2
dest: "{{ keycloak_host_import_directory }}/user-profile.json"
mode: '0644'
notify: docker compose up
- name: Apply SSH Public Key to user-profile via kcadm
shell: |
docker exec -i {{ keycloak_container }} \
/opt/keycloak/bin/kcadm.sh update realms/{{ keycloak_realm }} -f {{ keycloak_docker_import_directory }}user-profile.json

View File

@@ -1,3 +1,3 @@
# Todos # Todos
- Include 03_update-ldap-bind.yml - Include 03_update-ldap.yml
- Include 04_ssh_public_key.yml - Include 04_ssh_public_key.yml

View File

@@ -1,6 +1,6 @@
--- ---
- name: "create import files for {{ application_id }}" - name: "create import files for {{ application_id }}"
include_tasks: 01_import.yml include_tasks: 01_initialize.yml
- name: "load required 'web-svc-logout' for {{ application_id }}" - name: "load required 'web-svc-logout' for {{ application_id }}"
include_role: include_role:
@@ -10,10 +10,12 @@
- name: "load docker, db and proxy for {{ application_id }}" - name: "load docker, db and proxy for {{ application_id }}"
include_role: include_role:
name: cmp-db-docker-proxy name: cmp-db-docker-proxy
vars:
docker_compose_flush_handlers: true
- name: "Wait until Keycloak is reachable at {{ keycloak_server_host_url }}" - name: "Wait until Keycloak is reachable at {{ KEYCLOAK_SERVER_HOST_URL }}"
uri: uri:
url: "{{ keycloak_server_host_url }}/realms/master" url: "{{ KEYCLOAK_MASTER_REALM_URL }}"
method: GET method: GET
status_code: 200 status_code: 200
validate_certs: false validate_certs: false
@@ -22,13 +24,41 @@
delay: 5 delay: 5
until: kc_up.status == 200 until: kc_up.status == 200
- name: "Apply client redirects without realm import" - name: kcadm login (master)
include_tasks: 02_update_client_redirects.yml no_log: true
shell: >
{{ KEYCLOAK_EXEC_KCADM }} config credentials
--server {{ KEYCLOAK_SERVER_INTERNAL_URL }}
--realm master
--user {{ KEYCLOAK_MASTER_API_USER_NAME }}
--password {{ KEYCLOAK_MASTER_API_USER_PASSWORD }}
changed_when: false
- name: "Update LDAP bind credentials from ldap.*" - name: "Update REALM settings"
when: keycloak_update_ldap_bind | bool include_tasks: 02_update.yml
include_tasks: 03_update-ldap-bind.yml vars:
kc_object_kind: "component"
kc_lookup_value: "{{ KEYCLOAK_LDAP_CMP_NAME }}"
kc_desired: >-
{{
KEYCLOAK_DICTIONARY_REALM.components['org.keycloak.storage.UserStorageProvider']
| selectattr('providerId','equalto','ldap')
| list | first }}
kc_merge_path: "config"
when: KEYCLOAK_LDAP_ENABLED | bool
# Deactivated temporary. Import now via realm.yml - name: "Update Client settings"
#- name: Implement SSH Public Key Attribut vars:
# include_tasks: 03_ssh_public_key.yml kc_object_kind: "client"
kc_lookup_value: "{{ KEYCLOAK_CLIENT_ID }}"
kc_desired: >-
{{
KEYCLOAK_DICTIONARY_REALM.clients
| selectattr('clientId','equalto', KEYCLOAK_CLIENT_ID)
| list | first
}}
kc_force_attrs:
frontchannelLogout: true
attributes: "{{ (KEYCLOAK_DICTIONARY_CLIENT.attributes | default({}))
| combine({'frontchannel.logout.url': KEYCLOAK_FRONTCHANNEL_LOGOUT_URL}, recursive=True) }}"
include_tasks: 02_update.yml

View File

@@ -1,14 +1,14 @@
{% include 'roles/docker-compose/templates/base.yml.j2' %} {% include 'roles/docker-compose/templates/base.yml.j2' %}
application: application:
image: "{{ keycloak_image }}:{{ keycloak_version }}" image: "{{ KEYCLOAK_IMAGE }}:{{ KEYCLOAK_VERSION }}"
container_name: {{ keycloak_container }} container_name: {{ KEYCLOAK_CONTAINER }}
command: start{% if keycloak_import_realm %} --import-realm{% endif %}{% if keycloak_debug_enabled %} --verbose{% endif %} command: start{% if KEYCLOAK_IMPORT_REALM_ENABLED %} --import-realm{% endif %}{% if KEYCLOAK_DEBUG_ENABLED %} --verbose{% endif %}
{% include 'roles/docker-container/templates/base.yml.j2' %} {% include 'roles/docker-container/templates/base.yml.j2' %}
ports: ports:
- "{{ keycloak_server_host }}:8080" - "{{ KEYCLOAK_SERVER_HOST }}:8080"
volumes: volumes:
- "{{ keycloak_host_import_directory }}:{{keycloak_docker_import_directory}}" - "{{ KEYCLOAK_HOST_IMPORT_DIR }}:{{KEYCLOAK_DOCKER_IMPORT_DIR}}"
{% include 'roles/docker-container/templates/depends_on/dmbs_excl.yml.j2' %} {% include 'roles/docker-container/templates/depends_on/dmbs_excl.yml.j2' %}
{% include 'roles/docker-container/templates/networks.yml.j2' %} {% include 'roles/docker-container/templates/networks.yml.j2' %}
{% set container_port = 9000 %} {% set container_port = 9000 %}

View File

@@ -2,7 +2,7 @@
# Documentation can be found here: # Documentation can be found here:
# @see https://www.keycloak.org/server/containers # @see https://www.keycloak.org/server/containers
KC_HOSTNAME= https://{{ domains | get_domain(application_id) }} KC_HOSTNAME= {{ KEYCLOAK_URL }}
KC_HTTP_ENABLED= true KC_HTTP_ENABLED= true
# Health Checks # Health Checks
@@ -11,18 +11,18 @@ KC_HEALTH_ENABLED= true
KC_METRICS_ENABLED= true KC_METRICS_ENABLED= true
# Administrator # Administrator
KEYCLOAK_ADMIN= "{{applications | get_app_conf(application_id, 'users.administrator.username', True)}}" KEYCLOAK_ADMIN= "{{ KEYCLOAK_ADMIN }}"
KEYCLOAK_ADMIN_PASSWORD= "{{applications | get_app_conf(application_id, 'credentials.administrator_password', True)}}" KEYCLOAK_ADMIN_PASSWORD= "{{ KEYCLOAK_ADMIN_PASSWORD }}"
# Database # Database
KC_DB= postgres KC_DB= {{ database_type }}
KC_DB_URL= {{database_url_jdbc}} KC_DB_URL= {{ database_url_jdbc }}
KC_DB_USERNAME= {{ database_username }} KC_DB_USERNAME= {{ database_username }}
KC_DB_PASSWORD= {{ database_password }} KC_DB_PASSWORD= {{ database_password }}
# If the initial administrator already exists and the environment variables are still present at startup, an error message stating the failed creation of the initial administrator is shown in the logs. Keycloak ignores the values and starts up correctly. # If the initial administrator already exists and the environment variables are still present at startup, an error message stating the failed creation of the initial administrator is shown in the logs. Keycloak ignores the values and starts up correctly.
KC_BOOTSTRAP_ADMIN_USERNAME= "{{applications | get_app_conf(application_id, 'users.administrator.username', True)}}" KC_BOOTSTRAP_ADMIN_USERNAME= "{{ KEYCLOAK_ADMIN }}"
KC_BOOTSTRAP_ADMIN_PASSWORD= "{{applications | get_app_conf(application_id, 'credentials.administrator_password', True)}}" KC_BOOTSTRAP_ADMIN_PASSWORD= "{{ KEYCLOAK_ADMIN_PASSWORD }}"
# Enable detailed logs # Enable detailed logs
{% if MODE_DEBUG | bool %} {% if MODE_DEBUG | bool %}

View File

@@ -0,0 +1,59 @@
{
"clientId": "{{ KEYCLOAK_CLIENT_ID }}",
"name": "",
"description": "",
"rootUrl": "{{ KEYCLOAK_REALM_URL }}",
"adminUrl": "{{ KEYCLOAK_REALM_URL }}",
"baseUrl": "{{ KEYCLOAK_REALM_URL }}",
"surrogateAuthRequired": false,
"enabled": true,
"alwaysDisplayInConsole": false,
"clientAuthenticatorType": "client-secret",
"secret": "{{ OIDC.CLIENT.SECRET }}",
"redirectUris": {{ KEYCLOAK_REDIRECT_URIS | to_json }},
"webOrigins": {{ KEYCLOAK_WEB_ORIGINS | to_json }},
"notBefore": 0,
"bearerOnly": false,
"consentRequired": false,
"standardFlowEnabled": true,
"implicitFlowEnabled": true,
"directAccessGrantsEnabled": true,
"serviceAccountsEnabled": true,
"publicClient": false,
"frontchannelLogout": true,
"protocol": "openid-connect",
"attributes": {
"frontchannel.logout.url": {{ KEYCLOAK_FRONTCHANNEL_LOGOUT_URL | to_json }},
"realm_client": "false",
"oidc.ciba.grant.enabled": "false",
"client.secret.creation.time": "0",
"backchannel.logout.session.required": "true",
"post.logout.redirect.uris": {{ KEYCLOAK_POST_LOGOUT_URIS | to_json }},
"frontchannel.logout.session.required": "true",
"oauth2.device.authorization.grant.enabled": "false",
"display.on.consent.screen": "false",
"use.jwks.url": "false",
"backchannel.logout.revoke.offline.tokens": "false"
},
"authenticationFlowBindingOverrides": {},
"fullScopeAllowed": true,
"nodeReRegistrationTimeout": -1,
"defaultClientScopes": [
"web-origins",
"service_account",
"acr",
"roles",
"profile",
"basic",
"email"
],
"optionalClientScopes": [
"address",
"phone",
"organization",
"offline_access",
"microprofile-jwt",
"{{ applications | get_app_conf(application_id, 'scopes.rbac_roles', True) }}",
"{{ applications | get_app_conf(application_id, 'scopes.nextcloud', True) }}"
]
}

View File

@@ -0,0 +1,227 @@
{
"name": "{{ KEYCLOAK_LDAP_CMP_NAME }}",
"providerId": "ldap",
"subComponents": {
"org.keycloak.storage.ldap.mappers.LDAPStorageMapper": [
{# ---------------------- First Name ---------------------- #}
{
"name": "first name",
"providerId": "user-attribute-ldap-mapper",
"subComponents": {},
"config": {
"ldap.attribute": [ "{{ ldap.user.attributes.firstname }}" ],
"attribute.force.default": [ "true" ],
"is.mandatory.in.ldap": [ "true" ],
"is.binary.attribute": [ "false" ],
"always.read.value.from.ldap": [ "true" ],
"read.only": [ "false" ],
"user.model.attribute": [ "firstName" ]
}
},
{# ---------------------- Last Name ----------------------- #}
{
"name": "last name",
"providerId": "user-attribute-ldap-mapper",
"subComponents": {},
"config": {
"ldap.attribute": [ "{{ ldap.user.attributes.surname }}" ],
"is.mandatory.in.ldap": [ "true" ],
"always.read.value.from.ldap": [ "true" ],
"read.only": [ "false" ],
"user.model.attribute": [ "lastName" ]
}
},
{# ---------------------- Full Name (cn) ------------------ #}
{
"name": "full name",
"providerId": "full-name-ldap-mapper",
"subComponents": {},
"config": {
"read.only": [ "false" ],
"write.only": [ "true" ],
"ldap.full.name.attribute": [ "{{ ldap.user.attributes.fullname }}" ]
}
},
{# ---------------------- Username ------------------------ #}
{
"name": "username",
"providerId": "user-attribute-ldap-mapper",
"subComponents": {},
"config": {
"ldap.attribute": [ "{{ ldap.user.attributes.id }}" ],
"is.mandatory.in.ldap": [ "true" ],
"attribute.force.default": [ "false" ],
"is.binary.attribute": [ "false" ],
"always.read.value.from.ldap": [ "false" ],
"read.only": [ "false" ],
"user.model.attribute": [ "username" ]
}
},
{# ---------------------- Email --------------------------- #}
{
"name": "email",
"providerId": "user-attribute-ldap-mapper",
"subComponents": {},
"config": {
"ldap.attribute": [ "{{ ldap.user.attributes.mail }}" ],
"is.mandatory.in.ldap": [ "false" ],
"read.only": [ "false" ],
"always.read.value.from.ldap": [ "false" ],
"user.model.attribute": [ "email" ]
}
},
{# ---------------------- SSH Public Key ------------------ #}
{
"name": "SSH Public Key",
"providerId": "user-attribute-ldap-mapper",
"subComponents": {},
"config": {
"ldap.attribute": [ "{{ ldap.user.attributes.ssh_public_key }}" ],
"is.mandatory.in.ldap": [ "false" ],
"attribute.force.default": [ "false" ],
"is.binary.attribute": [ "false" ],
"read.only": [ "false" ],
"always.read.value.from.ldap": [ "true" ],
"user.model.attribute": [ "{{ ldap.user.attributes.ssh_public_key }}" ]
}
},
{# ---------------------- Nextcloud Quota ----------------- #}
{
"name": "{{ ldap.user.attributes.nextcloud_quota }}",
"providerId": "user-attribute-ldap-mapper",
"subComponents": {},
"config": {
"ldap.attribute": [ "{{ ldap.user.attributes.nextcloud_quota }}" ],
"is.mandatory.in.ldap": [ "false" ],
"attribute.force.default": [ "false" ],
"is.binary.attribute": [ "false" ],
"always.read.value.from.ldap": [ "false" ],
"read.only": [ "false" ],
"user.model.attribute": [ "{{ ldap.user.attributes.nextcloud_quota }}" ]
}
},
{# ---------------------- Creation Date ------------------- #}
{
"name": "creation date",
"providerId": "user-attribute-ldap-mapper",
"subComponents": {},
"config": {
"ldap.attribute": [ "createTimestamp" ],
"is.mandatory.in.ldap": [ "false" ],
"always.read.value.from.ldap": [ "true" ],
"read.only": [ "true" ],
"user.model.attribute": [ "createTimestamp" ]
}
},
{# ---------------------- Modify Date --------------------- #}
{
"name": "modify date",
"providerId": "user-attribute-ldap-mapper",
"subComponents": {},
"config": {
"ldap.attribute": [ "modifyTimestamp" ],
"is.mandatory.in.ldap": [ "false" ],
"always.read.value.from.ldap": [ "true" ],
"read.only": [ "true" ],
"user.model.attribute": [ "modifyTimestamp" ]
}
},
{# ---------------------- LDAP Groups -> KC Groups -------- #}
{
"name": "ldap-roles",
"providerId": "group-ldap-mapper",
"subComponents": {},
"config": {
"membership.attribute.type": [ "DN" ],
"group.name.ldap.attribute": [ "cn" ],
"membership.user.ldap.attribute": [ "{{ ldap.user.attributes.id }}" ],
"preserve.group.inheritance": [ "true" ],
"groups.dn": [ "{{ ldap.dn.ou.roles }}" ],
"mode": [ "LDAP_ONLY" ],
"user.roles.retrieve.strategy": [ "LOAD_GROUPS_BY_MEMBER_ATTRIBUTE" ],
"groups.ldap.filter": [
"{% set flavors = ldap.rbac.flavors | default([]) %}\
{% if 'groupOfNames' in flavors and 'organizationalUnit' in flavors %}(|(objectClass=groupOfNames)(objectClass=organizationalUnit))\
{% elif 'groupOfNames' in flavors %}(objectClass=groupOfNames)\
{% elif 'organizationalUnit' in flavors %}(objectClass=organizationalUnit)\
{% else %}(objectClass=groupOfNames){% endif %}"
],
"membership.ldap.attribute": [ "member" ],
"ignore.missing.groups": [ "true" ],
"group.object.classes": [ "groupOfNames" ],
"memberof.ldap.attribute": [ "memberOf" ],
"drop.non.existing.groups.during.sync": [ "false" ],
"groups.path": [ "{{ applications | get_app_conf(application_id, 'rbac_groups', True) }}" ]
}
}{% if keycloak_map_ldap_realm_roles | default(false) %},
{# ---------------------- LDAP -> Realm Roles (optional) -- #}
{
"name": "ldap-realm-roles",
"providerId": "role-ldap-mapper",
"subComponents": {},
"config": {
"mode": [ "LDAP_ONLY" ],
"membership.attribute.type": [ "DN" ],
"user.roles.retrieve.strategy": [ "LOAD_ROLES_BY_MEMBER_ATTRIBUTE" ],
"roles.dn": [ "{{ ldap.dn.ou.roles }}" ],
"membership.ldap.attribute": [ "member" ],
"membership.user.ldap.attribute": [ "{{ ldap.user.attributes.id }}" ],
"memberof.ldap.attribute": [ "memberOf" ],
"role.name.ldap.attribute": [ "cn" ],
"use.realm.roles.mapping": [ "true" ],
"role.object.classes": [ "groupOfNames" ]
}
}{% endif %}
]
},
"config": {
"fullSyncPeriod": [ "-1" ],
"pagination": [ "true" ],
"connectionTrace": [ "false" ],
"startTls": [ "false" ],
"usersDn": [ "{{ ldap.dn.ou.users }}" ],
"connectionPooling": [ "true" ],
"cachePolicy": [ "DEFAULT" ],
"useKerberosForPasswordAuthentication": [ "false" ],
"importEnabled": [ "true" ],
"enabled": [ "true" ],
"bindCredential": [ "{{ KEYCLOAK_LDAP_BIND_PW }}" ],
"changedSyncPeriod": [ "-1" ],
"usernameLDAPAttribute": [ "{{ ldap.user.attributes.id }}" ],
"bindDn": [ "{{ KEYCLOAK_LDAP_BIND_DN }}" ],
"vendor": [ "other" ],
"uuidLDAPAttribute": [ "{{ ldap.user.attributes.id }}" ],
"allowKerberosAuthentication": [ "false" ],
"connectionUrl": [ "{{ KEYCLOAK_LDAP_URL }}" ],
"syncRegistrations": [ "true" ],
"authType": [ "simple" ],
"krbPrincipalAttribute": [ "krb5PrincipalName" ],
"searchScope": [ "1" ],
"useTruststoreSpi": [ "always" ],
"usePasswordModifyExtendedOp": [ "true" ],
"trustEmail": [ "false" ],
{# Build objectClasses from structural + auxiliary definitions #}
"userObjectClasses": [
"{{ (ldap.user.objects.structural + (ldap.user.objects.auxiliary | dict2items | map(attribute='value') | list)) | join(', ') }}"
],
"rdnLDAPAttribute": [ "{{ ldap.user.attributes.id }}" ],
"editMode": [ "WRITABLE" ],
"validatePasswordPolicy": [ "false" ],
{# Recommended: prune Keycloak shadow users not in LDAP anymore #}
"removeInvalidUsersEnabled": [ "true" ]
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -3,24 +3,63 @@ application_id: "web-app-keycloak"
database_type: "postgres" # Database which will be used database_type: "postgres" # Database which will be used
# Keycloak # Keycloak
keycloak_container: "{{ applications | get_app_conf(application_id, 'docker.services.keycloak.name') }}" # Name of the keycloak docker container
keycloak_docker_import_directory: "/opt/keycloak/data/import/" # Directory in which keycloak import files are placed in the running docker container
keycloak_realm: "{{ OIDC.CLIENT.REALM }}" # This is the name of the default realm which is used by the applications
keycloak_master_api_user: "{{ applications | get_app_conf(application_id, 'users.administrator') }}" # Master Administrator
keycloak_master_api_user_name: "{{ keycloak_master_api_user.username }}" # Master Administrator Username
keycloak_master_api_user_password: "{{ keycloak_master_api_user.password }}" # Master Administrator Password
keycloak_kcadm_path: "docker exec -i {{ keycloak_container }} /opt/keycloak/bin/kcadm.sh" # Init script for keycloak
keycloak_server_internal_url: "http://127.0.0.1:8080"
keycloak_server_host: "127.0.0.1:{{ ports.localhost.http[application_id] }}"
keycloak_server_host_url: "http://{{ keycloak_server_host }}"
keycloak_image: "{{ applications | get_app_conf(application_id, 'docker.services.keycloak.image') }}" # Keycloak docker image
keycloak_version: "{{ applications | get_app_conf(application_id, 'docker.services.keycloak.version') }}" # Keycloak docker version
keycloak_debug_enabled: "{{ MODE_DEBUG }}"
keycloak_redirect_features: ["features.oauth2","features.oidc"]
keycloak_client_id: "{{ OIDC.CLIENT.ID }}"
keycloak_ldap_component_name: "{{ ldap.server.domain }}" # Name of the LDAP User Federation component in Keycloak (as shown in UI)
keycloak_import_realm: "{{ applications | get_app_conf(application_id, 'actions.import_realm') }}" # Activate realm import
keycloak_update_ldap_bind: "{{ applications | get_app_conf(application_id, 'actions.update_ldap_bind') }}" # Toggle the LDAP bind update step
# Docker ## General
docker_compose_flush_handlers: true # Remember to copy realm import before flush when set to true KEYCLOAK_URL: "{{ domains | get_url(application_id, WEB_PROTOCOL) }}"
KEYCLOAK_REALM: "{{ OIDC.CLIENT.REALM }}" # This is the name of the default realm which is used by the applications
KEYCLOAK_REALM_URL: "{{ WEB_PROTOCOL }}://{{ KEYCLOAK_REALM }}"
KEYCLOAK_DEBUG_ENABLED: "{{ MODE_DEBUG }}"
KEYCLOAK_CLIENT_ID: "{{ OIDC.CLIENT.ID }}"
KEYCLOAK_MASTER_REALM_URL: "{{ KEYCLOAK_SERVER_HOST_URL }}/realms/master"
KEYCLOAK_HOST_IMPORT_DIR: "{{ docker_compose.directories.volumes }}import/"
KEYCLOAK_SERVER_INTERNAL_URL: "http://127.0.0.1:8080"
# Credentials
KEYCLOAK_ADMIN: "{{ applications | get_app_conf(application_id, 'users.administrator.username') }}"
KEYCLOAK_ADMIN_PASSWORD: "{{ applications | get_app_conf(application_id, 'credentials.administrator_password') }}"
## Docker
KEYCLOAK_CONTAINER: "{{ applications | get_app_conf(application_id, 'docker.services.keycloak.name') }}" # Name of the keycloak docker container
KEYCLOAK_DOCKER_IMPORT_DIR: "/opt/keycloak/data/import/" # Directory in which keycloak import files are placed in the running docker container
KEYCLOAK_EXEC_KCADM: "docker exec -i {{ KEYCLOAK_CONTAINER }} /opt/keycloak/bin/kcadm.sh" # Init script for keycloak
KEYCLOAK_IMAGE: "{{ applications | get_app_conf(application_id, 'docker.services.keycloak.image') }}" # Keycloak docker image
KEYCLOAK_VERSION: "{{ applications | get_app_conf(application_id, 'docker.services.keycloak.version') }}" # Keycloak docker version
## Server
KEYCLOAK_SERVER_HOST: "127.0.0.1:{{ ports.localhost.http[application_id] }}"
KEYCLOAK_SERVER_HOST_URL: "http://{{ KEYCLOAK_SERVER_HOST }}"
## Update
KEYCLOAK_REDIRECT_FEATURES: ["features.oauth2","features.oidc"]
KEYCLOAK_IMPORT_REALM_ENABLED: "{{ applications | get_app_conf(application_id, 'actions.import_realm') }}" # Activate realm import
KEYCLOAK_FRONTCHANNEL_LOGOUT_URL: "{{ domains | get_url('web-svc-logout', WEB_PROTOCOL) }}/"
KEYCLOAK_REDIRECT_URIS: "{{ domains | redirect_uris(applications, WEB_PROTOCOL, '/*', KEYCLOAK_REDIRECT_FEATURES) }}"
KEYCLOAK_WEB_ORIGINS: >-
{{ KEYCLOAK_REDIRECT_URIS
| map('regex_replace','/\\*$','')
| map('regex_search','^(https?://[^/]+)')
| select('string')
| list | unique }}
KEYCLOAK_POST_LOGOUT_URIS: "+"
## LDAP
KEYCLOAK_LDAP_ENABLED: "{{ applications | get_app_conf(application_id, 'features.ldap', False) }}"
KEYCLOAK_LDAP_CMP_NAME: "{{ ldap.server.domain }}" # Name of the LDAP User Federation component in Keycloak (as shown in UI)
KEYCLOAK_LDAP_BIND_DN: "{{ ldap.dn.administrator.data }}"
KEYCLOAK_LDAP_BIND_PW: "{{ ldap.bind_credential }}"
KEYCLOAK_LDAP_URL: "{{ ldap.server.uri }}"
## API
KEYCLOAK_MASTER_API_USER: "{{ applications | get_app_conf(application_id, 'users.administrator') }}" # Master Administrator
KEYCLOAK_MASTER_API_USER_NAME: "{{ KEYCLOAK_MASTER_API_USER.username }}" # Master Administrator Username
KEYCLOAK_MASTER_API_USER_PASSWORD: "{{ KEYCLOAK_MASTER_API_USER.password }}" # Master Administrator Password
# Dictionaries
KEYCLOAK_DICTIONARY_REALM_RAW: "{{ lookup('template', 'import/realm.json.j2') }}"
KEYCLOAK_DICTIONARY_REALM: >-
{{
KEYCLOAK_DICTIONARY_REALM_RAW
if (KEYCLOAK_DICTIONARY_REALM_RAW is mapping)
else (KEYCLOAK_DICTIONARY_REALM_RAW | from_json)
}}

View File

@@ -2,7 +2,7 @@
# https://github.com/LDAPAccountManager/lam/blob/develop/lam-packaging/docker/.env # https://github.com/LDAPAccountManager/lam/blob/develop/lam-packaging/docker/.env
# Basic Configuration # Basic Configuration
LAM_PASSWORD= {{applications | get_app_conf(application_id, 'credentials.administrator_password', True)}} # LAM configuration master password and password for server profile "lam LAM_PASSWORD= {{ applications | get_app_conf(application_id, 'credentials.administrator_password') }} # LAM configuration master password and password for server profile "lam
# Database # Database
LAM_CONFIGURATION_DATABASE= files # configuration database (files or mysql) @todo implement mariadb LAM_CONFIGURATION_DATABASE= files # configuration database (files or mysql) @todo implement mariadb

View File

@@ -2,5 +2,5 @@ TZ={{ HOST_TIMEZONE }}
# Administrator setup # Administrator setup
LISTMONK_ADMIN_USER={{ applications | get_app_conf(application_id, 'users.administrator.username', True) }} LISTMONK_ADMIN_USER={{ applications | get_app_conf(application_id, 'users.administrator.username') }}
LISTMONK_ADMIN_PASSWORD={{ applications | get_app_conf(application_id, 'credentials.administrator_password', True) }} LISTMONK_ADMIN_PASSWORD={{ applications | get_app_conf(application_id, 'credentials.administrator_password', True) }}

View File

@@ -103,7 +103,7 @@
- name: create admin account - name: create admin account
command: command:
cmd: docker compose exec -it synapse register_new_matrix_user -u {{ MATRIX_ADMINISTRATOR_USERNAME }} -p {{applications | get_app_conf(application_id, 'credentials.administrator_password', True)}} -a -c {{ MATRIX_SYNAPSE_CONFIG_PATH_CONTAINER }} http://localhost:8008 cmd: docker compose exec -it synapse register_new_matrix_user -u {{ MATRIX_ADMINISTRATOR_USERNAME }} -p {{ applications | get_app_conf(application_id, 'credentials.administrator_password') }} -a -c {{ MATRIX_SYNAPSE_CONFIG_PATH_CONTAINER }} http://localhost:8008
chdir: "{{ docker_compose.directories.instance }}" chdir: "{{ docker_compose.directories.instance }}"
ignore_errors: true ignore_errors: true
when: applications | get_app_conf(application_id, 'setup', True) | bool when: applications | get_app_conf(application_id, 'setup', True) | bool

View File

@@ -4,7 +4,7 @@ MOODLE_SITE_NAME="{{applications | get_app_conf(application_id, 'site_titel', Tr
MOODLE_HOST="{{ domains | get_domain(application_id) }}" MOODLE_HOST="{{ domains | get_domain(application_id) }}"
MOODLE_SSLPROXY=yes MOODLE_SSLPROXY=yes
MOODLE_REVERSE_PROXY=yes MOODLE_REVERSE_PROXY=yes
MOODLE_USERNAME={{applications | get_app_conf(application_id, 'users.administrator.username', True)}} MOODLE_USERNAME={{ applications | get_app_conf(application_id, 'users.administrator.username') }}
MOODLE_PASSWORD={{applications | get_app_conf(application_id, 'credentials.user_password', True)}} MOODLE_PASSWORD={{applications | get_app_conf(application_id, 'credentials.user_password', True)}}
MOODLE_EMAIL={{applications | get_app_conf(application_id, 'users.administrator.email', True)}} MOODLE_EMAIL={{applications | get_app_conf(application_id, 'users.administrator.email', True)}}
BITNAMI_DEBUG={% if MODE_DEBUG | bool %}true{% else %}false{% endif %} BITNAMI_DEBUG={% if MODE_DEBUG | bool %}true{% else %}false{% endif %}

View File

@@ -12,7 +12,7 @@ database_password: "{{ applications | get_app_conf(
database_type: "mariadb" # Database flavor database_type: "mariadb" # Database flavor
nextcloud_plugins_enabled: "{{ applications | get_app_conf(application_id, 'plugins_enabled', True) }}" nextcloud_plugins_enabled: "{{ applications | get_app_conf(application_id, 'plugins_enabled', True) }}"
nextcloud_administrator_username: "{{ applications | get_app_conf(application_id, 'users.administrator.username', True) }}" nextcloud_administrator_username: "{{ applications | get_app_conf(application_id, 'users.administrator.username') }}"
# Control Node # Control Node
nextcloud_control_node_plugin_vars_directory: "{{role_path}}/vars/plugins/" # Folder in which the files for the plugin configuration are stored nextcloud_control_node_plugin_vars_directory: "{{role_path}}/vars/plugins/" # Folder in which the files for the plugin configuration are stored

View File

@@ -5,7 +5,7 @@
wp core install wp core install
--url="{{ domains | get_url(application_id, WEB_PROTOCOL) }}" --url="{{ domains | get_url(application_id, WEB_PROTOCOL) }}"
--title="{{ applications | get_app_conf(application_id, 'title', True) }}" --title="{{ applications | get_app_conf(application_id, 'title', True) }}"
--admin_user="{{ applications | get_app_conf(application_id, 'users.administrator.username', True) }}" --admin_user="{{ applications | get_app_conf(application_id, 'users.administrator.username') }}"
--admin_password="{{ applications | get_app_conf(application_id, 'credentials.administrator_password', True) }}" --admin_password="{{ applications | get_app_conf(application_id, 'credentials.administrator_password', True) }}"
--admin_email="{{ applications | get_app_conf(application_id, 'users.administrator.email', True) }}" --admin_email="{{ applications | get_app_conf(application_id, 'users.administrator.email', True) }}"
--path="{{ wordpress_docker_html_path }}" --path="{{ wordpress_docker_html_path }}"

View File

@@ -3,7 +3,7 @@ application_id: "web-app-yourls"
database_type: "mariadb" database_type: "mariadb"
# Yourls Specific # Yourls Specific
yourls_user: "{{ applications | get_app_conf(application_id, 'users.administrator.username', True) }}" yourls_user: "{{ applications | get_app_conf(application_id, 'users.administrator.username') }}"
yourls_password: "{{ applications | get_app_conf(application_id, 'credentials.administrator_password', True) }}" yourls_password: "{{ applications | get_app_conf(application_id, 'credentials.administrator_password', True) }}"
yourls_version: "{{ applications | get_app_conf(application_id, 'docker.services.yourls.version', True) }}" yourls_version: "{{ applications | get_app_conf(application_id, 'docker.services.yourls.version', True) }}"
yourls_image: "{{ applications | get_app_conf(application_id, 'docker.services.yourls.image', True) }}" yourls_image: "{{ applications | get_app_conf(application_id, 'docker.services.yourls.image', True) }}"