Kevin Veen-Birkenbach 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

108 lines
3.6 KiB
YAML

# 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