mirror of
				https://github.com/kevinveenbirkenbach/computer-playbook.git
				synced 2025-10-31 02:10:05 +00:00 
			
		
		
		
	- Allow kc_object_kind='realm' - Map endpoint to 'realms' and default lookup_field to 'id' - Use realm-specific kcadm GET/UPDATE (no -r flag) - Preserve immutables: id, realm - Guard query-based ID resolution to non-realm objects Context: fixing failure in 'Update REALM mail settings' task. See: https://chatgpt.com/share/68affdb8-3d28-800f-8480-aa6a74000bf8
		
			
				
	
	
		
			186 lines
		
	
	
		
			6.4 KiB
		
	
	
	
		
			YAML
		
	
	
	
	
	
			
		
		
	
	
			186 lines
		
	
	
		
			6.4 KiB
		
	
	
	
		
			YAML
		
	
	
	
	
	
| # Generic updater for Keycloak client/component/realm 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" | "client-scope" | "realm"
 | |
| # - kc_lookup_value:  e.g., KEYCLOAK_CLIENT_ID or KEYCLOAK_LDAP_CMP_NAME or KEYCLOAK_REALM
 | |
| # - 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, id for realm)
 | |
| # - 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','client-scope','realm']
 | |
|       - kc_lookup_value is defined
 | |
|       - kc_desired is defined
 | |
|     fail_msg: "kc_object_kind, kc_lookup_value, kc_desired are required."
 | |
|   when: MODE_ASSERT | bool
 | |
| 
 | |
| - name: Derive API endpoint and lookup field
 | |
|   set_fact:
 | |
|     kc_api: >-
 | |
|       {{ 'clients' if kc_object_kind == 'client'
 | |
|          else 'components' if kc_object_kind == 'component'
 | |
|          else 'client-scopes' if kc_object_kind == 'client-scope'
 | |
|          else 'realms' if kc_object_kind == 'realm'
 | |
|          else '' }}
 | |
|     kc_lookup_field_eff: >-
 | |
|       {{ 'clientId' if kc_object_kind == 'client'
 | |
|          else (kc_lookup_field | default('name')) if kc_object_kind == 'component'
 | |
|          else 'name' if kc_object_kind == 'client-scope'
 | |
|          else 'id' if kc_object_kind == 'realm'
 | |
|          else '' }}
 | |
| 
 | |
| - name: Resolve object id (direct when lookup_field is id)
 | |
|   when: kc_lookup_field_eff == 'id'
 | |
|   set_fact:
 | |
|     kc_obj_id: "{{ kc_lookup_value | string }}"
 | |
| 
 | |
| - name: Resolve object id via query
 | |
|   when: kc_lookup_field_eff != 'id' and kc_object_kind != 'realm'
 | |
|   shell: >
 | |
|     {% if kc_object_kind == 'client-scope' -%}
 | |
|     {{ KEYCLOAK_EXEC_KCADM }} get client-scopes -r {{ KEYCLOAK_REALM }} --format json
 | |
|     | jq -r '.[] | select(.{{ kc_lookup_field_eff }}=="{{ kc_lookup_value }}") | .id' | head -n1
 | |
|     {%- else -%}
 | |
|     {{ 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'
 | |
|     {%- endif %}
 | |
|   register: kc_obj_id_cmd
 | |
|   changed_when: false
 | |
| 
 | |
| - name: Normalize resolved id to a plain string
 | |
|   set_fact:
 | |
|     kc_obj_id: >-
 | |
|       {{
 | |
|         kc_obj_id
 | |
|         if kc_lookup_field_eff == 'id'
 | |
|         else (kc_obj_id_cmd.stdout | default('') | trim)
 | |
|       }}
 | |
| 
 | |
| - name: Fail if object not found
 | |
|   assert:
 | |
|     that:
 | |
|       - (kc_obj_id | trim) != ''
 | |
|       - (kc_obj_id | trim) != 'null'
 | |
|     fail_msg: "{{ kc_object_kind | capitalize }} '{{ kc_lookup_value }}' not found."
 | |
|   when: MODE_ASSERT | bool
 | |
| 
 | |
| - name: Read current object
 | |
|   shell: >
 | |
|     {% if kc_object_kind == 'realm' -%}
 | |
|     {{ KEYCLOAK_EXEC_KCADM }} get {{ kc_api }}/{{ kc_obj_id }} --format json
 | |
|     {%- else -%}
 | |
|     {{ KEYCLOAK_EXEC_KCADM }} get {{ kc_api }}/{{ kc_obj_id }} -r {{ KEYCLOAK_REALM }} --format json
 | |
|     {%- endif %}
 | |
|   register: kc_cur
 | |
|   changed_when: false
 | |
|   no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
 | |
| 
 | |
| - name: Parse current object
 | |
|   set_fact:
 | |
|     cur_obj: "{{ kc_cur.stdout | from_json }}"
 | |
|   no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
 | |
| 
 | |
| - name: "Safety check: providerId must match when updating a component"
 | |
|   when:
 | |
|     - kc_object_kind == 'component'
 | |
|     - (kc_desired.providerId is defined)
 | |
|     - MODE_ASSERT | bool
 | |
|   assert:
 | |
|     that:
 | |
|       - cur_obj.providerId == kc_desired.providerId
 | |
|     fail_msg: >
 | |
|       Refusing to update component '{{ cur_obj.name }}' (providerId={{ cur_obj.providerId }})
 | |
|       because desired providerId={{ kc_desired.providerId }}. Check your lookup/ID.
 | |
| 
 | |
| - 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)) } }}"
 | |
|   no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
 | |
| 
 | |
| - 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 }}"
 | |
|   no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
 | |
| 
 | |
| - name: Build desired object (base merge)
 | |
|   set_fact:
 | |
|     desired_obj: "{{ cur_obj | combine(merge_payload, recursive=True) }}"
 | |
|   no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
 | |
| 
 | |
| # 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)
 | |
|       }}
 | |
|   no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
 | |
| 
 | |
| - 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)
 | |
|       }}
 | |
|   no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
 | |
| 
 | |
| - name: Preserve immutable fields for client-scope
 | |
|   when: kc_object_kind == 'client-scope'
 | |
|   set_fact:
 | |
|     desired_obj: "{{ desired_obj | combine({'id': cur_obj.id, 'name': cur_obj.name}, recursive=True) }}"
 | |
| 
 | |
| - name: Preserve immutable fields for realm
 | |
|   when: kc_object_kind == 'realm'
 | |
|   set_fact:
 | |
|     desired_obj: >-
 | |
|       {{
 | |
|         desired_obj
 | |
|         | combine({
 | |
|             'id': cur_obj.id,
 | |
|             'realm': cur_obj.realm
 | |
|           }, recursive=True)
 | |
|       }}
 | |
|   no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
 | |
| 
 | |
| # 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: |
 | |
|     {% if kc_object_kind == 'realm' -%}
 | |
|     cat <<'JSON' | {{ KEYCLOAK_EXEC_KCADM }} update {{ kc_api }}/{{ kc_obj_id }} -f -
 | |
|     {{ desired_obj | to_json }}
 | |
|     JSON
 | |
|     {%- else -%}
 | |
|     cat <<'JSON' | {{ KEYCLOAK_EXEC_KCADM }} update {{ kc_api }}/{{ kc_obj_id }} -r {{ KEYCLOAK_REALM }} -f -
 | |
|     {{ desired_obj | to_json }}
 | |
|     JSON
 | |
|     {%- endif %}
 | |
|   async: "{{ ASYNC_TIME if ASYNC_ENABLED | bool else omit }}"
 | |
|   poll:  "{{ ASYNC_POLL if ASYNC_ENABLED | bool else omit }}"
 |