mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2025-08-20 02:35:04 +02:00
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.
This commit is contained in:
parent
20c8d46f54
commit
7d0502ebc5
@ -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
|
||||||
|
@ -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) }}"
|
|
15
roles/web-app-keycloak/tasks/01_initialize.yml
Normal file
15
roles/web-app-keycloak/tasks/01_initialize.yml
Normal 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) }}"
|
107
roles/web-app-keycloak/tasks/02_update.yml
Normal file
107
roles/web-app-keycloak/tasks/02_update.yml
Normal 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
|
@ -1,108 +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_EXEC_KCADM 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_EXEC_KCADM 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_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: "Resolve client internal id for {{ KEYCLOAK_CLIENT_ID }}"
|
|
||||||
shell: >
|
|
||||||
{{ KEYCLOAK_EXEC_KCADM }} 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 }}'."
|
|
||||||
|
|
||||||
- name: "Read current client configuration"
|
|
||||||
shell: >
|
|
||||||
{{ KEYCLOAK_EXEC_KCADM }} 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: "{{ KEYCLOAK_REDIRECT_URIS | sort }}"
|
|
||||||
kc_desired_web_origins: "{{ KEYCLOAK_WEB_ORIGINS | sort }}"
|
|
||||||
KEYCLOAK_POST_LOGOUT_URIS_list: >-
|
|
||||||
{{ "+" | split('\n') | reject('equalto','') | list | sort }}
|
|
||||||
|
|
||||||
- name: "Extract current frontchannel logout url"
|
|
||||||
set_fact:
|
|
||||||
kc_current_frontchannel_logout_url: >-
|
|
||||||
{{
|
|
||||||
(
|
|
||||||
(kc_client_obj.stdout | from_json).attributes['frontchannel.logout.url']
|
|
||||||
if 'frontchannel.logout.url' in (kc_client_obj.stdout | from_json).attributes
|
|
||||||
else ''
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
|
|
||||||
- name: "Update client with redirectUris, webOrigins, frontchannelLogout"
|
|
||||||
shell: >
|
|
||||||
{{ KEYCLOAK_EXEC_KCADM }} update clients/{{ kc_client.stdout }}
|
|
||||||
-r {{ KEYCLOAK_REALM }}
|
|
||||||
-s 'redirectUris={{ KEYCLOAK_REDIRECT_URIS | to_json }}'
|
|
||||||
-s 'webOrigins={{ KEYCLOAK_WEB_ORIGINS | to_json }}'
|
|
||||||
-s 'frontchannelLogout=true'
|
|
||||||
-s 'attributes."frontchannel.logout.url"={{ KEYCLOAK_FRONTCHANNEL_LOGOUT_URL | to_json }}'
|
|
||||||
when: kc_current_redirect_uris != kc_desired_redirect_uris
|
|
||||||
or kc_current_web_origins != kc_desired_web_origins
|
|
||||||
or kc_current_frontchannel_logout_url != KEYCLOAK_FRONTCHANNEL_LOGOUT_URL
|
|
||||||
async: "{{ ASYNC_TIME if ASYNC_ENABLED | bool else omit }}"
|
|
||||||
poll: "{{ ASYNC_POLL if ASYNC_ENABLED | bool else omit }}"
|
|
||||||
|
|
@ -1,105 +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_CMP_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_CMP_NAME }}'"
|
|
||||||
command:
|
|
||||||
argv: "{{ kcadm_argv_base
|
|
||||||
+ ['get', 'components',
|
|
||||||
'-r', KEYCLOAK_REALM,
|
|
||||||
'--query', 'name=' ~ KEYCLOAK_LDAP_CMP_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_CMP_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] }}"
|
|
||||||
|
|
||||||
- name: "Determine if update is required"
|
|
||||||
set_fact:
|
|
||||||
kc_needs_update: >-
|
|
||||||
{{
|
|
||||||
(kc_ldap_current_bind_dn != KEYCLOAK_LDAP_BIND_DN)
|
|
||||||
or (kc_ldap_current_bind_pw != KEYCLOAK_LDAP_BIND_PW)
|
|
||||||
or (kc_ldap_current_connection_url != KEYCLOAK_LDAP_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=' ~ ([KEYCLOAK_LDAP_BIND_DN] | to_json),
|
|
||||||
'-s', 'config.bindCredential=' ~ ([KEYCLOAK_LDAP_BIND_PW] | to_json),
|
|
||||||
'-s', 'config.connectionUrl=' ~ ([KEYCLOAK_LDAP_URL] | to_json)
|
|
||||||
] }}"
|
|
||||||
when: kc_needs_update | bool
|
|
||||||
register: kc_bind_update
|
|
||||||
async: "{{ ASYNC_TIME if ASYNC_ENABLED | bool else omit }}"
|
|
||||||
poll: "{{ ASYNC_POLL if ASYNC_ENABLED | bool else omit }}"
|
|
@ -1,73 +0,0 @@
|
|||||||
# Configure Credentials
|
|
||||||
- name: Ensure Keycloak CLI credentials are configured
|
|
||||||
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 }}
|
|
||||||
|
|
||||||
# LDAP Source
|
|
||||||
- name: Get ID of LDAP storage provider
|
|
||||||
shell: |
|
|
||||||
{{ KEYCLOAK_EXEC_KCADM }} 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_EXEC_KCADM }} update realms/{{ KEYCLOAK_REALM }}
|
|
||||||
-s 'attributes.userProfileEnabled=true'
|
|
||||||
|
|
||||||
- name: Re-authenticate to Keycloak after enabling user profile
|
|
||||||
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 }}
|
|
||||||
|
|
||||||
- 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_DIR }}user-profile.json
|
|
||||||
async: "{{ ASYNC_TIME if ASYNC_ENABLED | bool else omit }}"
|
|
||||||
poll: "{{ ASYNC_POLL if ASYNC_ENABLED | bool else omit }}"
|
|
@ -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
|
@ -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:
|
||||||
@ -15,7 +15,7 @@
|
|||||||
|
|
||||||
- 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
|
||||||
@ -24,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
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
ports:
|
ports:
|
||||||
- "{{ KEYCLOAK_SERVER_HOST }}:8080"
|
- "{{ KEYCLOAK_SERVER_HOST }}:8080"
|
||||||
volumes:
|
volumes:
|
||||||
- "{{ keycloak_host_import_directory }}:{{KEYCLOAK_DOCKER_IMPORT_DIR}}"
|
- "{{ 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 %}
|
||||||
|
@ -10,7 +10,6 @@
|
|||||||
"alwaysDisplayInConsole": false,
|
"alwaysDisplayInConsole": false,
|
||||||
"clientAuthenticatorType": "client-secret",
|
"clientAuthenticatorType": "client-secret",
|
||||||
"secret": "{{ OIDC.CLIENT.SECRET }}",
|
"secret": "{{ OIDC.CLIENT.SECRET }}",
|
||||||
{# The following line should be covered by 02_update_client_redirects.yml #}
|
|
||||||
"redirectUris": {{ KEYCLOAK_REDIRECT_URIS | to_json }},
|
"redirectUris": {{ KEYCLOAK_REDIRECT_URIS | to_json }},
|
||||||
"webOrigins": {{ KEYCLOAK_WEB_ORIGINS | to_json }},
|
"webOrigins": {{ KEYCLOAK_WEB_ORIGINS | to_json }},
|
||||||
"notBefore": 0,
|
"notBefore": 0,
|
||||||
|
@ -196,14 +196,14 @@
|
|||||||
"useKerberosForPasswordAuthentication": [ "false" ],
|
"useKerberosForPasswordAuthentication": [ "false" ],
|
||||||
"importEnabled": [ "true" ],
|
"importEnabled": [ "true" ],
|
||||||
"enabled": [ "true" ],
|
"enabled": [ "true" ],
|
||||||
"bindCredential": [ "{{ ldap.bind_credential }}" ],
|
"bindCredential": [ "{{ KEYCLOAK_LDAP_BIND_PW }}" ],
|
||||||
"changedSyncPeriod": [ "-1" ],
|
"changedSyncPeriod": [ "-1" ],
|
||||||
"usernameLDAPAttribute": [ "{{ ldap.user.attributes.id }}" ],
|
"usernameLDAPAttribute": [ "{{ ldap.user.attributes.id }}" ],
|
||||||
"bindDn": [ "{{ ldap.dn.administrator.data }}" ],
|
"bindDn": [ "{{ KEYCLOAK_LDAP_BIND_DN }}" ],
|
||||||
"vendor": [ "other" ],
|
"vendor": [ "other" ],
|
||||||
"uuidLDAPAttribute": [ "{{ ldap.user.attributes.id }}" ],
|
"uuidLDAPAttribute": [ "{{ ldap.user.attributes.id }}" ],
|
||||||
"allowKerberosAuthentication": [ "false" ],
|
"allowKerberosAuthentication": [ "false" ],
|
||||||
"connectionUrl": [ "{{ ldap.server.uri }}" ],
|
"connectionUrl": [ "{{ KEYCLOAK_LDAP_URL }}" ],
|
||||||
"syncRegistrations": [ "true" ],
|
"syncRegistrations": [ "true" ],
|
||||||
"authType": [ "simple" ],
|
"authType": [ "simple" ],
|
||||||
"krbPrincipalAttribute": [ "krb5PrincipalName" ],
|
"krbPrincipalAttribute": [ "krb5PrincipalName" ],
|
||||||
|
@ -1654,7 +1654,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"org.keycloak.storage.UserStorageProvider": [
|
"org.keycloak.storage.UserStorageProvider": [
|
||||||
{% include "client.json.j2" %}
|
{% include "ldap.json.j2" %}
|
||||||
],
|
],
|
||||||
"org.keycloak.keys.KeyProvider": [
|
"org.keycloak.keys.KeyProvider": [
|
||||||
{
|
{
|
||||||
|
@ -10,6 +10,9 @@ KEYCLOAK_REALM: "{{ OIDC.CLIENT.REALM }}" # This is the name
|
|||||||
KEYCLOAK_REALM_URL: "{{ WEB_PROTOCOL }}://{{ KEYCLOAK_REALM }}"
|
KEYCLOAK_REALM_URL: "{{ WEB_PROTOCOL }}://{{ KEYCLOAK_REALM }}"
|
||||||
KEYCLOAK_DEBUG_ENABLED: "{{ MODE_DEBUG }}"
|
KEYCLOAK_DEBUG_ENABLED: "{{ MODE_DEBUG }}"
|
||||||
KEYCLOAK_CLIENT_ID: "{{ OIDC.CLIENT.ID }}"
|
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
|
# Credentials
|
||||||
KEYCLOAK_ADMIN: "{{ applications | get_app_conf(application_id, 'users.administrator.username') }}"
|
KEYCLOAK_ADMIN: "{{ applications | get_app_conf(application_id, 'users.administrator.username') }}"
|
||||||
@ -23,14 +26,12 @@ KEYCLOAK_IMAGE: "{{ applications | get_app_conf(application_
|
|||||||
KEYCLOAK_VERSION: "{{ applications | get_app_conf(application_id, 'docker.services.keycloak.version') }}" # Keycloak docker version
|
KEYCLOAK_VERSION: "{{ applications | get_app_conf(application_id, 'docker.services.keycloak.version') }}" # Keycloak docker version
|
||||||
|
|
||||||
## Server
|
## Server
|
||||||
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: "127.0.0.1:{{ ports.localhost.http[application_id] }}"
|
||||||
KEYCLOAK_SERVER_HOST_URL: "http://{{ KEYCLOAK_SERVER_HOST }}"
|
KEYCLOAK_SERVER_HOST_URL: "http://{{ KEYCLOAK_SERVER_HOST }}"
|
||||||
|
|
||||||
## Update
|
## Update
|
||||||
KEYCLOAK_REDIRECT_FEATURES: ["features.oauth2","features.oidc"]
|
KEYCLOAK_REDIRECT_FEATURES: ["features.oauth2","features.oidc"]
|
||||||
KEYCLOAK_IMPORT_REALM_ENABLED: "{{ applications | get_app_conf(application_id, 'actions.import_realm') }}" # Activate realm import
|
KEYCLOAK_IMPORT_REALM_ENABLED: "{{ 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
|
|
||||||
KEYCLOAK_FRONTCHANNEL_LOGOUT_URL: "{{ domains | get_url('web-svc-logout', WEB_PROTOCOL) }}/"
|
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_REDIRECT_URIS: "{{ domains | redirect_uris(applications, WEB_PROTOCOL, '/*', KEYCLOAK_REDIRECT_FEATURES) }}"
|
||||||
KEYCLOAK_WEB_ORIGINS: >-
|
KEYCLOAK_WEB_ORIGINS: >-
|
||||||
@ -42,6 +43,7 @@ KEYCLOAK_WEB_ORIGINS: >-
|
|||||||
KEYCLOAK_POST_LOGOUT_URIS: "+"
|
KEYCLOAK_POST_LOGOUT_URIS: "+"
|
||||||
|
|
||||||
## LDAP
|
## 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_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_DN: "{{ ldap.dn.administrator.data }}"
|
||||||
KEYCLOAK_LDAP_BIND_PW: "{{ ldap.bind_credential }}"
|
KEYCLOAK_LDAP_BIND_PW: "{{ ldap.bind_credential }}"
|
||||||
@ -50,4 +52,14 @@ KEYCLOAK_LDAP_URL: "{{ ldap.server.uri }}"
|
|||||||
## API
|
## API
|
||||||
KEYCLOAK_MASTER_API_USER: "{{ applications | get_app_conf(application_id, 'users.administrator') }}" # Master Administrator
|
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_NAME: "{{ KEYCLOAK_MASTER_API_USER.username }}" # Master Administrator Username
|
||||||
KEYCLOAK_MASTER_API_USER_PASSWORD: "{{ KEYCLOAK_MASTER_API_USER.password }}" # Master Administrator Password
|
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)
|
||||||
|
}}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user