mirror of
				https://github.com/kevinveenbirkenbach/computer-playbook.git
				synced 2025-10-31 10:19:09 +00: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:
		| @@ -1,10 +1,9 @@ | ||||
| actions: | ||||
|   import_realm:       True     # Import REALM | ||||
|   update_ldap_bind:   True     # Updates LDAP binds | ||||
| features: | ||||
|   matomo:             true | ||||
|   css:                true | ||||
|   desktop:    true | ||||
|   desktop:            true | ||||
|   ldap:               true | ||||
|   central_database:   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 | ||||
| - Include 03_update-ldap-bind.yml | ||||
| - Include 03_update-ldap.yml | ||||
| - Include 04_ssh_public_key.yml | ||||
| @@ -1,6 +1,6 @@ | ||||
| --- | ||||
| - 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 }}" | ||||
|   include_role:  | ||||
| @@ -15,7 +15,7 @@ | ||||
|  | ||||
| - name: "Wait until Keycloak is reachable at {{ KEYCLOAK_SERVER_HOST_URL }}" | ||||
|   uri: | ||||
|     url: "{{ KEYCLOAK_SERVER_HOST_URL }}/realms/master" | ||||
|     url: "{{ KEYCLOAK_MASTER_REALM_URL }}" | ||||
|     method: GET | ||||
|     status_code: 200 | ||||
|     validate_certs: false | ||||
| @@ -24,13 +24,41 @@ | ||||
|   delay: 5 | ||||
|   until: kc_up.status == 200 | ||||
|  | ||||
| - name: "Apply client redirects without realm import" | ||||
|   include_tasks: 02_update_client_redirects.yml | ||||
| - name: kcadm login (master) | ||||
|   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.*" | ||||
|   when: KEYCLOAK_UPDATE_LDAP_BIND | bool | ||||
|   include_tasks: 03_update-ldap-bind.yml | ||||
| - name: "Update REALM settings" | ||||
|   include_tasks: 02_update.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: Implement SSH Public Key Attribut | ||||
| #  include_tasks: 03_ssh_public_key.yml | ||||
| - name: "Update Client settings" | ||||
|   vars: | ||||
|     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: | ||||
|       - "{{ KEYCLOAK_SERVER_HOST }}:8080" | ||||
|     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/networks.yml.j2' %} | ||||
| {% set container_port = 9000 %} | ||||
|   | ||||
| @@ -10,7 +10,6 @@ | ||||
|   "alwaysDisplayInConsole": false, | ||||
|   "clientAuthenticatorType": "client-secret", | ||||
|   "secret": "{{ OIDC.CLIENT.SECRET }}", | ||||
|   {# The following line should be covered by 02_update_client_redirects.yml #} | ||||
|   "redirectUris": {{ KEYCLOAK_REDIRECT_URIS | to_json }}, | ||||
|   "webOrigins": {{ KEYCLOAK_WEB_ORIGINS | to_json }}, | ||||
|   "notBefore": 0, | ||||
|   | ||||
| @@ -196,14 +196,14 @@ | ||||
|     "useKerberosForPasswordAuthentication": [ "false" ], | ||||
|     "importEnabled": [ "true" ], | ||||
|     "enabled": [ "true" ], | ||||
|     "bindCredential": [ "{{ ldap.bind_credential }}" ], | ||||
|     "bindCredential": [ "{{ KEYCLOAK_LDAP_BIND_PW }}" ], | ||||
|     "changedSyncPeriod": [ "-1" ], | ||||
|     "usernameLDAPAttribute": [ "{{ ldap.user.attributes.id }}" ], | ||||
|     "bindDn": [ "{{ ldap.dn.administrator.data }}" ], | ||||
|     "bindDn": [ "{{ KEYCLOAK_LDAP_BIND_DN }}" ], | ||||
|     "vendor": [ "other" ], | ||||
|     "uuidLDAPAttribute": [ "{{ ldap.user.attributes.id }}" ], | ||||
|     "allowKerberosAuthentication": [ "false" ], | ||||
|     "connectionUrl": [ "{{ ldap.server.uri }}" ], | ||||
|     "connectionUrl": [ "{{ KEYCLOAK_LDAP_URL }}" ], | ||||
|     "syncRegistrations": [ "true" ], | ||||
|     "authType": [ "simple" ], | ||||
|     "krbPrincipalAttribute": [ "krb5PrincipalName" ], | ||||
|   | ||||
| @@ -1654,7 +1654,7 @@ | ||||
|       } | ||||
|     ], | ||||
|     "org.keycloak.storage.UserStorageProvider": [ | ||||
|       {% include "client.json.j2" %} | ||||
|       {% include "ldap.json.j2" %} | ||||
|     ], | ||||
|     "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_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') }}" | ||||
| @@ -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 | ||||
|  | ||||
| ## 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_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_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_REDIRECT_URIS:             "{{ domains | redirect_uris(applications, WEB_PROTOCOL, '/*', KEYCLOAK_REDIRECT_FEATURES) }}" | ||||
| KEYCLOAK_WEB_ORIGINS: >- | ||||
| @@ -42,6 +43,7 @@ KEYCLOAK_WEB_ORIGINS: >- | ||||
| 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 }}" | ||||
| @@ -50,4 +52,14 @@ 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 | ||||
| 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) | ||||
|   }} | ||||
|   | ||||
		Reference in New Issue
	
	Block a user