mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 10:59:59 +00:00
tasks/00_core.yml built webserver_extra_configuration by concatenating the helper iframes, the admin allowlist and the logout panel template lookups with `~`. With the shipped empty `services.keycloak.admin.allowed_networks` the allowlist template rendered no text, Ansible's template lookup returned None for it, and `~` wrote the word "None" into the auth vhost. OpenResty then refused `unknown directive "Nonelocation"` in front of the logout panel location and every deploy with Keycloak went red. The role now resolves the declared networks once in KEYCLOAK_ADMIN_NETWORKS and the task appends the allowlist template only when that list is not empty, the same guard the logout panel uses. The template renders the restricted /admin location unconditionally from KEYCLOAK_ADMIN_NETWORKS. With a list that did not contain the deploy host, the deploy still went red: Keycloak answers `/` with a redirect to `/admin/`, so the CSP health check and the front proxy probe, which request `/` from the host, got 403 and hlth-csp failed. While the allowlist is active, `location = /` now redirects to the account console of the platform realm instead, and the README no longer claims the deploy needs no entry for any reason. test_admin_allowlist.py rendered the template with plain Jinja, which yields '' for an empty template, so it could not see the None. It now evaluates the task's own webserver_extra_configuration expression through Ansible's Templar and real template lookup, which reproduces "HELPER_IFRAMES\nNoneLOGOUT_PANEL" on the old task, and covers the root redirect. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
145 lines
8.0 KiB
YAML
145 lines
8.0 KiB
YAML
---
|
|
application_id: "web-app-keycloak"
|
|
entity_name: "{{ application_id | get_entity_name }}"
|
|
domain: "{{ lookup('domain',application_id) }}"
|
|
|
|
KEYCLOAK_CONTAINER_HOSTNAME: "{{ entity_name }}"
|
|
KEYCLOAK_DOMAIN: "{{ domain }}"
|
|
KEYCLOAK_REALM: "{{ OIDC.CLIENT.REALM }}"
|
|
KEYCLOAK_URL: "{{ lookup('tls', application_id, 'url.base') | regex_replace('/+$', '') }}"
|
|
KEYCLOAK_REALM_URL: "{{ KEYCLOAK_URL }}/realms/{{ KEYCLOAK_REALM }}"
|
|
KEYCLOAK_DEBUG_ENABLED: "{{ MODE_DEBUG }}"
|
|
KEYCLOAK_CLIENT_ID: "{{ OIDC.CLIENT.ID }}"
|
|
KEYCLOAK_SERVER_INTERNAL_URL: "http://127.0.0.1:8080"
|
|
|
|
KEYCLOAK_RBAC_GROUP_CLAIM: "{{ RBAC.GROUP.CLAIM }}"
|
|
KEYCLOAK_RBAC_GROUP_NAME: "{{ RBAC.GROUP.NAME }}"
|
|
KEYCLOAK_RBAC_ALL_APP_IDS: "{{ lookup('deployment').groups | rbac_app_ids }}"
|
|
KEYCLOAK_RBAC_RUN_APP_IDS: "{{ lookup('deployment').running | rbac_app_ids }}"
|
|
|
|
KEYCLOAK_PER_APP_LDAP_ROLES_MAPPER: >-
|
|
{{
|
|
((KEYCLOAK_DICTIONARY_REALM.components['org.keycloak.storage.UserStorageProvider']
|
|
| selectattr('name','equalto', KEYCLOAK_LDAP_CMP_NAME)
|
|
| list | first
|
|
).subComponents['org.keycloak.storage.ldap.mappers.LDAPStorageMapper']
|
|
| selectattr('name','equalto','ldap-roles')
|
|
| list | first
|
|
| dict2items
|
|
| rejectattr('key','in',['subComponents','id'])
|
|
| list | items2dict)
|
|
| combine({
|
|
'name': kc_app_id | kc_per_app_mapper_name,
|
|
'parentId': ldap_cmp_id | trim,
|
|
'providerId': 'group-ldap-mapper',
|
|
'providerType': 'org.keycloak.storage.ldap.mappers.LDAPStorageMapper',
|
|
'config': {
|
|
'preserve.group.inheritance': ['false'],
|
|
'group.name.ldap.attribute': ['description'],
|
|
'groups.ldap.filter': [kc_app_id | kc_per_app_ldap_filter],
|
|
'groups.path': ['/' ~ (KEYCLOAK_RBAC_GROUP_NAME | trim('/')) ~ '/' ~ kc_app_id],
|
|
'groups.path.group': [kc_app_group_id],
|
|
'drop.non.existing.groups.during.sync': ['true'],
|
|
}
|
|
}, recursive=True)
|
|
}}
|
|
|
|
KEYCLOAK_HEALTH_ENABLED: true
|
|
|
|
KEYCLOAK_REALM_IMPORT_ENABLED: "{{ lookup('config', application_id, 'services.keycloak.realm.import_enabled') }}"
|
|
KEYCLOAK_REALM_IMPORT_DIR_HOST: "{{ [lookup('container', application_id, 'directories.volumes'),'import'] | path_join }}"
|
|
KEYCLOAK_REALM_IMPORT_DIR_DOCKER: "{{ lookup('volume', 'web-app-keycloak', 'realm_import').target }}"
|
|
KEYCLOAK_REALM_IMPORT_FILE_SRC: "import/realm.json.j2"
|
|
KEYCLOAK_REALM_IMPORT_FILENAME: "realm.json"
|
|
KEYCLOAK_REALM_IMPORT_FILE_DST: "{{ [KEYCLOAK_REALM_IMPORT_DIR_HOST,KEYCLOAK_REALM_IMPORT_FILENAME] | path_join }}"
|
|
KEYCLOAK_REALM_IMPORT_FILE_DOCKER: "{{ [KEYCLOAK_REALM_IMPORT_DIR_DOCKER,KEYCLOAK_REALM_IMPORT_FILENAME] | path_join }}"
|
|
KEYCLOAK_REALM_TOTP_ENABLED: "{{ lookup('config', application_id, 'services.keycloak.realm.totp_enabled') | bool }}"
|
|
|
|
KEYCLOAK_BOOTSTRAP_ADMIN_USERNAME: "{{ lookup('config', application_id, 'services.keycloak.accounts.bootstrap.username') }}"
|
|
KEYCLOAK_BOOTSTRAP_ADMIN_PASSWORD: "{{ lookup('config', application_id, 'secrets.credentials.administrator_password') }}"
|
|
|
|
KEYCLOAK_PERMANENT_ADMIN_USERNAME: "{{ lookup('config', application_id, 'services.keycloak.accounts.system.username') }}"
|
|
KEYCLOAK_PERMANENT_ADMIN_PASSWORD: "{{ lookup('config', application_id, 'secrets.credentials.administrator_password') }}"
|
|
|
|
KEYCLOAK_CONTAINER: "{{ lookup('config', application_id, 'services.keycloak.name') }}"
|
|
KEYCLOAK_CONTAINER_ADDRESS: "{{ lookup('container_address', application_id, entity_name) }}"
|
|
# Exception: KEYCLOAK_LOCAL_CID is set by utils/login.yml after resolving the swarm-hosting
|
|
# node; fall back to the compose-mode container_address when the fact is not yet set
|
|
# so vars resolve cleanly before the manager-side ops kick in.
|
|
KEYCLOAK_TARGET_CONTAINER: "{{ KEYCLOAK_LOCAL_CID | default(KEYCLOAK_CONTAINER_ADDRESS) }}" # nocheck: container-var-name-lookup references the consumer's container, not this role's service
|
|
KEYCLOAK_EXEC_CONTAINER: "container exec -i {{ KEYCLOAK_TARGET_CONTAINER }}" # nocheck: container-exec-resolver # nocheck: container-var-name-lookup references the consumer's container, not this role's service
|
|
KEYCLOAK_KCADM: "/opt/keycloak/bin/kcadm.sh"
|
|
KEYCLOAK_EXEC_KCADM: "{{ KEYCLOAK_EXEC_CONTAINER }} {{ KEYCLOAK_KCADM }}"
|
|
KEYCLOAK_CID_RESOLVE_CMD: >-
|
|
container ps
|
|
--filter "label=com.docker.swarm.service.name={{ lookup('container_service', application_id, entity_name) }}"
|
|
--filter status=running
|
|
--format '{{ "{{.ID}}" }}' | head -1
|
|
KEYCLOAK_IMAGE: "{{ lookup('config', application_id, 'services.keycloak.image') }}"
|
|
KEYCLOAK_VERSION: "{{ lookup('config', application_id, 'services.keycloak.version') }}"
|
|
KEYCLOAK_REPLICAS: "{{ groups[application_id] | length }}"
|
|
KEYCLOAK_REPLICAS_WANT: "{{ KEYCLOAK_REPLICAS }}/{{ KEYCLOAK_REPLICAS }}"
|
|
KEYCLOAK_DB_NETWORK_ARG: "{% if lookup('database', application_id, 'shared') | bool %}--network {{ lookup('database', application_id, 'network') | quote }}{% endif
|
|
%}"
|
|
|
|
KEYCLOAK_FRONTCHANNEL_LOGOUT_URL: "{{ lookup('tls', 'web-svc-logout', 'url.base') }}"
|
|
KEYCLOAK_LOGOUT_PANEL_FILE: "{{ lookup('nginx', 'directories.configuration.lua') }}{{ application_id }}-logout-panel.js"
|
|
KEYCLOAK_LOGOUT_PANEL_ENABLED: "{{ lookup('config', application_id, 'services.javascript.enabled') | bool }}"
|
|
KEYCLOAK_ADMIN_NETWORKS: "{{ lookup('config', application_id, 'services.keycloak.admin.allowed_networks') | kc_admin_networks }}"
|
|
KEYCLOAK_REDIRECT_URIS: "{{ lookup('redirect_uris') }}"
|
|
KEYCLOAK_SAML_APPS: >-
|
|
{{ lookup('applications') | dict2items
|
|
| selectattr('value.services', 'defined')
|
|
| selectattr('value.services.sso', 'defined')
|
|
| selectattr('value.services.sso.flavor', 'defined')
|
|
| selectattr('value.services.sso.flavor', 'equalto', 'saml')
|
|
| map(attribute='key')
|
|
| select('in', group_names)
|
|
| list }}
|
|
KEYCLOAK_WEB_ORIGINS: >-
|
|
{{ KEYCLOAK_REDIRECT_URIS
|
|
| map('regex_replace','/\\*$','')
|
|
| map('regex_search','^(https?://[^/]+)')
|
|
| select('string')
|
|
| list | unique }}
|
|
KEYCLOAK_POST_LOGOUT_URIS: "+"
|
|
|
|
KEYCLOAK_LDAP_ENABLED: "{{ lookup('config', application_id, 'services.ldap.enabled') }}"
|
|
KEYCLOAK_LDAP_SHARED: "{{ lookup('config', application_id, 'services.ldap.shared') }}"
|
|
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 }}"
|
|
KEYCLOAK_LDAP_CONNECTION_POOLING: "{{ lookup('config', application_id, 'services.ldap.connection_pooling') | bool }}"
|
|
KEYCLOAK_LDAP_SYNC_CHANGES_PERIOD: "{{ lookup('config', application_id, 'services.ldap.sync.period.changes') }}"
|
|
KEYCLOAK_LDAP_SYNC_FULL_PERIOD: "{{ lookup('config', application_id, 'services.ldap.sync.period.full') }}"
|
|
|
|
KEYCLOAK_LDAP_USER_OBJECT_CLASSES: >
|
|
{{
|
|
(
|
|
(LDAP.USER.OBJECTS.STRUCTURAL | reject('equalto','posixAccount') | list)
|
|
+ (LDAP.USER.OBJECTS.AUXILIARY | dict2items | map(attribute='value') | list)
|
|
) | join(', ')
|
|
}}
|
|
|
|
KEYCLOAK_USER_EMAILS_VERIFIED: "{{ lookup('config', application_id, 'services.keycloak.users.emails_verified') }}"
|
|
|
|
KEYCLOAK_MOODLE_ENABLED: "{{ 'web-app-moodle' in group_names }}"
|
|
|
|
KEYCLOAK_DECLARED_CLIENT_APPS: "{{ lookup('applications') | kc_declared_client_apps(lookup('deployment').groups) }}"
|
|
KEYCLOAK_DECLARED_CLIENT_IDS: "{{ lookup('applications') | kc_declared_client_ids(lookup('deployment').groups) }}"
|
|
|
|
KEYCLOAK_RECAPTCHA_ENABLED: "{{ lookup('config', application_id, 'services.recaptcha.enabled') }}"
|
|
KEYCLOAK_RECAPTCHA_KEY: "{{ lookup('config', application_id, 'secrets.credentials.recaptcha.key') }}"
|
|
KEYCLOAK_RECAPTCHA_SECRET: "{{ lookup('config', application_id, 'secrets.credentials.recaptcha.secret') }}"
|
|
|
|
KEYCLOAK_COMMAND_PERMANENT_ADMIN_LOGIN: |-
|
|
{{ KEYCLOAK_EXEC_CONTAINER }} sh -lc '
|
|
set -e
|
|
{{ KEYCLOAK_KCADM }} config credentials \
|
|
--server {{ KEYCLOAK_SERVER_INTERNAL_URL }} \
|
|
--realm master \
|
|
--user "$KEYCLOAK_PERMANENT_ADMIN_USERNAME" \
|
|
--password "$KEYCLOAK_PERMANENT_ADMIN_PASSWORD"
|
|
'
|