Added parameter to skipp dependency loading to speed up debugging

This commit is contained in:
Kevin Veen-Birkenbach 2025-08-17 21:00:45 +02:00
parent 7d0502ebc5
commit 5642793f4a
No known key found for this signature in database
GPG Key ID: 44D8F11FD62F878E
19 changed files with 582 additions and 190 deletions

View File

@ -1,3 +1,4 @@
load_dependencies: True # When set to false the dependencies aren't loaded. Helpful for developing
actions: actions:
import_realm: True # Import REALM import_realm: True # Import REALM
features: features:
@ -29,7 +30,6 @@ server:
canonical: canonical:
- "auth.{{ PRIMARY_DOMAIN }}" - "auth.{{ PRIMARY_DOMAIN }}"
scopes: scopes:
rbac_roles: rbac_roles
nextcloud: nextcloud nextcloud: nextcloud
rbac_groups: "/rbac" rbac_groups: "/rbac"

View File

@ -0,0 +1,47 @@
from typing import Iterable
class FilterModule(object):
"""Custom Jinja2 filters for LDAP related rendering."""
def filters(self):
return {
"ldap_groups_filter": self.ldap_groups_filter,
}
def ldap_groups_filter(self, flavors, default="groupOfNames") -> str:
"""
Build an LDAP objectClass filter for groups based on available flavors.
Args:
flavors: list/tuple/set of enabled flavors (e.g. ["groupOfNames","organizationalUnit"])
default: fallback objectClass if nothing matches
Returns:
A *single-line* LDAP filter string suitable for JSON, e.g.:
(|(objectClass=groupOfNames)(objectClass=organizationalUnit))
Rules:
- If both groupOfNames and organizationalUnit are present -> OR them.
- If one of them is present -> use that one.
- Otherwise -> use `default`.
"""
if flavors is None:
flavors = []
if isinstance(flavors, str):
# be forgiving if someone passes a comma-separated string
flavors = [f.strip() for f in flavors.split(",") if f.strip()]
if not isinstance(flavors, Iterable):
raise ValueError("ldap_groups_filter: 'flavors' must be an iterable or comma-separated string")
have_gon = "groupOfNames" in flavors
have_ou = "organizationalUnit" in flavors
if have_gon and have_ou:
classes = ["groupOfNames", "organizationalUnit"]
return f"(|{''.join(f'(objectClass={c})' for c in classes)})"
if have_gon:
return "(objectClass=groupOfNames)"
if have_ou:
return "(objectClass=organizationalUnit)"
# fallback
return f"(objectClass={default})"

View File

@ -21,5 +21,3 @@ galaxy_info:
class: "fa-solid fa-lock" class: "fa-solid fa-lock"
run_after: run_after:
- web-app-matomo - web-app-matomo
dependencies:
- web-svc-logout

View File

@ -0,0 +1,4 @@
- name: "remove directory {{ KEYCLOAK_REALM_IMPORT_DIR_HOST }}"
ansible.builtin.file:
path: "{{ KEYCLOAK_REALM_IMPORT_DIR_HOST }}"
state: absent

View File

@ -1,15 +0,0 @@
- 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) }}"

View File

@ -0,0 +1,15 @@
- name: "load variables from {{ DOCKER_VARS_FILE }}"
include_vars: "{{ DOCKER_VARS_FILE }}"
- name: "create directory {{ KEYCLOAK_REALM_IMPORT_DIR_HOST }}"
file:
path: "{{ KEYCLOAK_REALM_IMPORT_DIR_HOST }}"
state: directory
mode: "0755"
- name: "Copy REALM import file '{{ KEYCLOAK_REALM_IMPORT_FILE_SRC }}' to '{{ KEYCLOAK_REALM_IMPORT_FILE_DST }}'"
template:
src: "{{ KEYCLOAK_REALM_IMPORT_FILE_SRC }}"
dest: "{{ KEYCLOAK_REALM_IMPORT_FILE_DST }}"
mode: "0770"
when: KEYCLOAK_REALM_IMPORT_ENABLED | bool

View File

@ -14,58 +14,100 @@
- name: Assert required vars - name: Assert required vars
assert: assert:
that: that:
- kc_object_kind in ['client','component'] - kc_object_kind in ['client','component','client-scope']
- kc_lookup_value is defined - kc_lookup_value is defined
- kc_desired is defined - kc_desired is defined
fail_msg: "kc_object_kind, kc_lookup_value, kc_desired are required." fail_msg: "kc_object_kind, kc_lookup_value, kc_desired are required."
- name: Derive API endpoint and lookup field - name: Derive API endpoint and lookup field
set_fact: set_fact:
kc_api: "{{ 'clients' if kc_object_kind == 'client' else 'components' }}" kc_api: >-
kc_lookup_field_eff: "{{ 'clientId' if kc_object_kind == 'client' else (kc_lookup_field | default('name')) }}" {{ 'clients' if kc_object_kind == 'client'
else 'components' if kc_object_kind == 'component'
else 'client-scopes' if kc_object_kind == 'client-scope'
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 '' }}
- name: Resolve object id - 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'
shell: > 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 }} {{ KEYCLOAK_EXEC_KCADM }} get {{ kc_api }}
-r {{ KEYCLOAK_REALM }} -r {{ KEYCLOAK_REALM }}
--query '{{ kc_lookup_field_eff }}={{ kc_lookup_value }}' --query '{{ kc_lookup_field_eff }}={{ kc_lookup_value }}'
--fields id --format json | jq -r '.[0].id' --fields id --format json | jq -r '.[0].id'
register: kc_obj_id {%- endif %}
register: kc_obj_id_cmd
changed_when: false 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 - name: Fail if object not found
assert: assert:
that: that:
- (kc_obj_id.stdout | trim) != '' - (kc_obj_id | trim) != ''
- (kc_obj_id.stdout | trim) != 'null' - (kc_obj_id | trim) != 'null'
fail_msg: "{{ kc_object_kind | capitalize }} '{{ kc_lookup_value }}' not found." fail_msg: "{{ kc_object_kind | capitalize }} '{{ kc_lookup_value }}' not found."
- name: Read current object - name: Read current object
shell: > shell: >
{{ KEYCLOAK_EXEC_KCADM }} get {{ kc_api }}/{{ kc_obj_id.stdout }} {{ KEYCLOAK_EXEC_KCADM }} get {{ kc_api }}/{{ kc_obj_id }}
-r {{ KEYCLOAK_REALM }} --format json -r {{ KEYCLOAK_REALM }} --format json
register: kc_cur register: kc_cur
changed_when: false changed_when: false
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
# ── Build merge payload safely (avoid evaluating kc_desired[kc_merge_path] when undefined) ─────────
- name: Parse current object - name: Parse current object
set_fact: set_fact:
cur_obj: "{{ kc_cur.stdout | from_json }}" 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)
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) - name: Prepare merge payload (subpath)
when: kc_merge_path is defined and (kc_merge_path | length) > 0 when: kc_merge_path is defined and (kc_merge_path | length) > 0
set_fact: set_fact:
merge_payload: "{{ { (kc_merge_path): (kc_desired[kc_merge_path] | default({}, true)) } }}" 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) - name: Prepare merge payload (full object)
when: kc_merge_path is not defined or (kc_merge_path | length) == 0 when: kc_merge_path is not defined or (kc_merge_path | length) == 0
set_fact: set_fact:
merge_payload: "{{ kc_desired }}" merge_payload: "{{ kc_desired }}"
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
- name: Build desired object (base merge) - name: Build desired object (base merge)
set_fact: set_fact:
desired_obj: "{{ cur_obj | combine(merge_payload, recursive=True) }}" desired_obj: "{{ cur_obj | combine(merge_payload, recursive=True) }}"
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
# Preserve immutable fields # Preserve immutable fields
- name: Preserve immutable fields for client - name: Preserve immutable fields for client
@ -79,6 +121,7 @@
'clientId': cur_obj.clientId 'clientId': cur_obj.clientId
}, recursive=True) }, recursive=True)
}} }}
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
- name: Preserve immutable fields for component - name: Preserve immutable fields for component
when: kc_object_kind == 'component' when: kc_object_kind == 'component'
@ -93,6 +136,14 @@
'parentId': cur_obj.parentId 'parentId': cur_obj.parentId
}, recursive=True) }, recursive=True)
}} }}
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
# Preserve immutables for client-scope
- 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) }}"
# Optional forced attributes (e.g., frontchannelLogout) # Optional forced attributes (e.g., frontchannelLogout)
- name: Apply forced attributes (optional) - name: Apply forced attributes (optional)
@ -102,6 +153,8 @@
- name: Update object via stdin - name: Update object via stdin
shell: | shell: |
cat <<'JSON' | {{ KEYCLOAK_EXEC_KCADM }} update {{ kc_api }}/{{ kc_obj_id.stdout }} -r {{ KEYCLOAK_REALM }} -f - cat <<'JSON' | {{ KEYCLOAK_EXEC_KCADM }} update {{ kc_api }}/{{ kc_obj_id }} -r {{ KEYCLOAK_REALM }} -f -
{{ desired_obj | to_json }} {{ desired_obj | to_json }}
JSON JSON
async: "{{ ASYNC_TIME if ASYNC_ENABLED | bool else omit }}"
poll: "{{ ASYNC_POLL if ASYNC_ENABLED | bool else omit }}"

View File

@ -0,0 +1,72 @@
# --- Ensure RBAC client scope exists (idempotent) ---
- name: Ensure RBAC client scope exists
shell: |
cat <<'JSON' | {{ KEYCLOAK_EXEC_KCADM }} create client-scopes -r {{ KEYCLOAK_REALM }} -f -
{{
(
KEYCLOAK_DICTIONARY_REALM.clientScopes
| selectattr('name','equalto', KEYCLOAK_OIDC_RBAC_SCOPE_NAME)
| list | first
) | to_json
}}
JSON
register: create_rbac_scope
changed_when: create_rbac_scope.rc == 0
failed_when: create_rbac_scope.rc != 0 and
('already exists' not in (create_rbac_scope.stderr | lower))
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
# --- Get the scope id we will attach to the client ---
- name: Get all client scopes
shell: "{{ KEYCLOAK_EXEC_KCADM }} get client-scopes -r {{ KEYCLOAK_REALM }} --format json"
register: all_scopes
changed_when: false
- name: Extract RBAC scope id
set_fact:
scope_id_rbac: >-
{{ (
all_scopes.stdout | from_json
| selectattr('name','equalto', KEYCLOAK_OIDC_RBAC_SCOPE_NAME)
| list | first | default({})
).id | default('') }}
- name: Resolve application 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: app_client_id_cmd
changed_when: false
- name: Sanity check IDs
assert:
that:
- scope_id_rbac | length > 0
- (app_client_id_cmd.stdout | trim) is match('^[0-9a-f-]+$')
fail_msg: "Could not determine client or scope ID."
- name: Get current optional client scopes
shell: >
{{ KEYCLOAK_EXEC_KCADM }} get
clients/{{ app_client_id_cmd.stdout | trim }}/optional-client-scopes
-r {{ KEYCLOAK_REALM }} --format json
register: opt_scopes
changed_when: false
- name: Decide if RBAC scope already assigned
set_fact:
has_rbac_optional: >-
{{ (opt_scopes.stdout | from_json
| selectattr('id','equalto', scope_id_rbac) | list | length) > 0 }}
- name: Ensure RBAC scope assigned as optional (only if missing)
when: not has_rbac_optional
shell: >
{{ KEYCLOAK_EXEC_KCADM }} update
clients/{{ app_client_id_cmd.stdout | trim }}/optional-client-scopes/{{ scope_id_rbac }}
-r {{ KEYCLOAK_REALM }}
register: add_opt
changed_when: true
failed_when: add_opt.rc != 0

View File

@ -0,0 +1,134 @@
# roles/web-app-keycloak/tasks/05_ldap.yml
---
- name: "Update REALM settings (merge LDAP component .config)"
include_tasks: 03_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"
# --- Read desired mapper definition from KEYCLOAK_DICTIONARY_REALM ---
- name: Get LDAP component (from KEYCLOAK_DICTIONARY_REALM)
set_fact:
ldap_component: >-
{{
KEYCLOAK_DICTIONARY_REALM.components['org.keycloak.storage.UserStorageProvider']
| selectattr('providerId','equalto','ldap')
| list | first | default({})
}}
- name: Sanity check LDAP component
assert:
that:
- ldap_component | length > 0
- (ldap_component.subComponents | default({})) | length > 0
fail_msg: "LDAP component not found in KEYCLOAK_DICTIONARY_REALM."
- name: Extract desired group-ldap-mapper definition (raw)
set_fact:
desired_group_mapper_raw: >-
{{
(
ldap_component.subComponents['org.keycloak.storage.ldap.mappers.LDAPStorageMapper']
| default([])
)
| selectattr('providerId','equalto','group-ldap-mapper')
| list | first | default({})
}}
- name: Ensure we found the mapper in the dictionary
assert:
that:
- desired_group_mapper_raw | length > 0
fail_msg: "group-ldap-mapper not found below LDAP component in KEYCLOAK_DICTIONARY_REALM."
- name: Compute desired mapper name
set_fact:
desired_group_mapper_name: "{{ desired_group_mapper_raw.name | default('ldap-roles') }}"
- name: Build clean mapper payload (strip unsupported keys)
set_fact:
desired_group_mapper: >-
{{
desired_group_mapper_raw
| dict2items
| rejectattr('key','equalto','subComponents')
| rejectattr('key','equalto','id')
| list | items2dict
}}
# --- Work against Keycloak ---
- name: Resolve LDAP component id
shell: >
{{ KEYCLOAK_EXEC_KCADM }} get components
-r {{ KEYCLOAK_REALM }}
--query 'name={{ KEYCLOAK_LDAP_CMP_NAME }}'
--fields id --format json | jq -r '.[0].id'
register: ldap_cmp_id
changed_when: false
- name: Assert LDAP component id resolved
assert:
that:
- (ldap_cmp_id.stdout | trim) not in ["", "null"]
fail_msg: "LDAP component '{{ KEYCLOAK_LDAP_CMP_NAME }}' not found in Keycloak."
- name: Check for group-ldap-mapper existence (by name under LDAP component)
shell: >
{{ KEYCLOAK_EXEC_KCADM }} get components
-r {{ KEYCLOAK_REALM }}
--query "parent={{ ldap_cmp_id.stdout | trim }}&type=org.keycloak.storage.ldap.mappers.LDAPStorageMapper&name={{ desired_group_mapper_name }}"
--format json
| jq -r '.[] | select(.parentId=="{{ ldap_cmp_id.stdout | trim }}"
and .providerType=="org.keycloak.storage.ldap.mappers.LDAPStorageMapper"
and .providerId=="group-ldap-mapper"
and .name=="{{ desired_group_mapper_name }}") | .id' | head -n1
register: grp_mapper_id
changed_when: false
- name: Ensure group-ldap-mapper exists (create if missing)
when: (grp_mapper_id.stdout | trim) in ["", "null"]
shell: |
cat <<'JSON' | {{ KEYCLOAK_EXEC_KCADM }} create components -r {{ KEYCLOAK_REALM }} -f -
{{
desired_group_mapper
| combine({
'name': desired_group_mapper_name,
'parentId': ldap_cmp_id.stdout | trim,
'providerType':'org.keycloak.storage.ldap.mappers.LDAPStorageMapper',
'providerId': 'group-ldap-mapper'
}, recursive=True)
| to_json
}}
JSON
register: create_mapper
changed_when: create_mapper.rc == 0
failed_when: create_mapper.rc != 0 and ('already exists' not in (create_mapper.stderr | lower))
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
- name: Update existing group-ldap-mapper config (merge only .config)
when: (grp_mapper_id.stdout | trim) not in ["", "null"]
vars:
kc_object_kind: "component"
kc_lookup_field: "id"
kc_lookup_value: "{{ grp_mapper_id.stdout | trim }}"
kc_desired: >-
{{
desired_group_mapper
| combine({
'name': desired_group_mapper_name,
'parentId': ldap_cmp_id.stdout | trim,
'providerType': 'org.keycloak.storage.ldap.mappers.LDAPStorageMapper',
'providerId': 'group-ldap-mapper'
}, recursive=True)
}}
kc_merge_path: "config"
include_tasks: 03_update.yml

View File

@ -1,31 +1,28 @@
--- ---
- name: "create import files for {{ application_id }}" - name: "Load cleanup routine for '{{ application_id }}'"
include_tasks: 01_initialize.yml include_tasks: 01_cleanup.yml
- name: "load required 'web-svc-logout' for {{ application_id }}" - name: "Load init routine for '{{ application_id }}'"
include_role: include_tasks: 02_initialize.yml
name: web-svc-logout
when: run_once_web_svc_logout is not defined
- name: "load docker, db and proxy for {{ application_id }}" - name: "Load the depdendencies required by '{{ application_id }}'"
include_role: include_tasks: 03_load_dependencies.yml
name: cmp-db-docker-proxy
vars:
docker_compose_flush_handlers: true
- name: "Wait until Keycloak is reachable at {{ KEYCLOAK_SERVER_HOST_URL }}" - name: "Wait until '{{ KEYCLOAK_CONTAINER }}' container is healthy"
uri: community.docker.docker_container_info:
url: "{{ KEYCLOAK_MASTER_REALM_URL }}" name: "{{ KEYCLOAK_CONTAINER }}"
method: GET register: kc_info
status_code: 200 retries: 60
validate_certs: false
register: kc_up
retries: 30
delay: 5 delay: 5
until: kc_up.status == 200 until: >
kc_info is succeeded and
(kc_info.container | default({})) != {} and
(kc_info.container.State | default({})) != {} and
(kc_info.container.State.Health | default({})) != {} and
(kc_info.container.State.Health.Status | default('')) == 'healthy'
- name: kcadm login (master) - name: kcadm login (master)
no_log: true no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
shell: > shell: >
{{ KEYCLOAK_EXEC_KCADM }} config credentials {{ KEYCLOAK_EXEC_KCADM }} config credentials
--server {{ KEYCLOAK_SERVER_INTERNAL_URL }} --server {{ KEYCLOAK_SERVER_INTERNAL_URL }}
@ -34,19 +31,6 @@
--password {{ KEYCLOAK_MASTER_API_USER_PASSWORD }} --password {{ KEYCLOAK_MASTER_API_USER_PASSWORD }}
changed_when: false changed_when: false
- 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
- name: "Update Client settings" - name: "Update Client settings"
vars: vars:
kc_object_kind: "client" kc_object_kind: "client"
@ -59,6 +43,16 @@
}} }}
kc_force_attrs: kc_force_attrs:
frontchannelLogout: true frontchannelLogout: true
attributes: "{{ (KEYCLOAK_DICTIONARY_CLIENT.attributes | default({})) attributes: >-
| combine({'frontchannel.logout.url': KEYCLOAK_FRONTCHANNEL_LOGOUT_URL}, recursive=True) }}" {{
include_tasks: 02_update.yml ( (KEYCLOAK_DICTIONARY_REALM.clients
| selectattr('clientId','equalto', KEYCLOAK_CLIENT_ID)
| list | first | default({}) ).attributes | default({}) )
| combine({'frontchannel.logout.url': KEYCLOAK_FRONTCHANNEL_LOGOUT_URL}, recursive=True)
}}
include_tasks: 03_update.yml
- include_tasks: 04_rbac_client_scope.yml
- include_tasks: 05_ldap.yml
when: KEYCLOAK_LDAP_ENABLED | bool

View File

@ -3,12 +3,12 @@
application: application:
image: "{{ KEYCLOAK_IMAGE }}:{{ KEYCLOAK_VERSION }}" image: "{{ KEYCLOAK_IMAGE }}:{{ KEYCLOAK_VERSION }}"
container_name: {{ KEYCLOAK_CONTAINER }} container_name: {{ KEYCLOAK_CONTAINER }}
command: start{% if KEYCLOAK_IMPORT_REALM_ENABLED %} --import-realm{% endif %}{% if KEYCLOAK_DEBUG_ENABLED %} --verbose{% endif %} command: start{% if KEYCLOAK_REALM_IMPORT_ENABLED %} --import-realm{% endif %}{% if KEYCLOAK_DEBUG_ENABLED %} --verbose{% endif %}
{% include 'roles/docker-container/templates/base.yml.j2' %} {% include 'roles/docker-container/templates/base.yml.j2' %}
ports: ports:
- "{{ KEYCLOAK_SERVER_HOST }}:8080" - "{{ KEYCLOAK_SERVER_HOST }}:8080"
volumes: volumes:
- "{{ KEYCLOAK_HOST_IMPORT_DIR }}:{{KEYCLOAK_DOCKER_IMPORT_DIR}}" - "{{ KEYCLOAK_REALM_IMPORT_DIR_HOST }}:{{ KEYCLOAK_REALM_IMPORT_DIR_DOCKER }}"
{% 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 %}

View File

@ -7,7 +7,7 @@ KC_HTTP_ENABLED= true
# Health Checks # Health Checks
# @see https://quarkus.io/guides/smallrye-health # @see https://quarkus.io/guides/smallrye-health
KC_HEALTH_ENABLED= true KC_HEALTH_ENABLED= {{ KEYCLOAK_HEALTH_ENABLED | lower }}
KC_METRICS_ENABLED= true KC_METRICS_ENABLED= true
# Administrator # Administrator

View File

@ -28,6 +28,7 @@
"oidc.ciba.grant.enabled": "false", "oidc.ciba.grant.enabled": "false",
"client.secret.creation.time": "0", "client.secret.creation.time": "0",
"backchannel.logout.session.required": "true", "backchannel.logout.session.required": "true",
"standard.token.exchange.enabled": "false",
"post.logout.redirect.uris": {{ KEYCLOAK_POST_LOGOUT_URIS | to_json }}, "post.logout.redirect.uris": {{ KEYCLOAK_POST_LOGOUT_URIS | to_json }},
"frontchannel.logout.session.required": "true", "frontchannel.logout.session.required": "true",
"oauth2.device.authorization.grant.enabled": "false", "oauth2.device.authorization.grant.enabled": "false",
@ -53,7 +54,7 @@
"organization", "organization",
"offline_access", "offline_access",
"microprofile-jwt", "microprofile-jwt",
"{{ applications | get_app_conf(application_id, 'scopes.rbac_roles', True) }}", "{{ KEYCLOAK_OIDC_RBAC_SCOPE_NAME }}",
"{{ applications | get_app_conf(application_id, 'scopes.nextcloud', True) }}" "{{ applications | get_app_conf(application_id, 'scopes.nextcloud', True) }}"
] ]
} }

View File

@ -1,4 +1,5 @@
{ "org.keycloak.storage.UserStorageProvider": [
{
"name": "{{ KEYCLOAK_LDAP_CMP_NAME }}", "name": "{{ KEYCLOAK_LDAP_CMP_NAME }}",
"providerId": "ldap", "providerId": "ldap",
"subComponents": { "subComponents": {
@ -149,13 +150,7 @@
"groups.dn": [ "{{ ldap.dn.ou.roles }}" ], "groups.dn": [ "{{ ldap.dn.ou.roles }}" ],
"mode": [ "LDAP_ONLY" ], "mode": [ "LDAP_ONLY" ],
"user.roles.retrieve.strategy": [ "LOAD_GROUPS_BY_MEMBER_ATTRIBUTE" ], "user.roles.retrieve.strategy": [ "LOAD_GROUPS_BY_MEMBER_ATTRIBUTE" ],
"groups.ldap.filter": [ "groups.ldap.filter": ["{{ ldap.rbac.flavors | ldap_groups_filter }}"],
"{% set flavors = ldap.rbac.flavors | default([]) %}\
{% if 'groupOfNames' in flavors and 'organizationalUnit' in flavors %}(|(objectClass=groupOfNames)(objectClass=organizationalUnit))\
{% elif 'groupOfNames' in flavors %}(objectClass=groupOfNames)\
{% elif 'organizationalUnit' in flavors %}(objectClass=organizationalUnit)\
{% else %}(objectClass=groupOfNames){% endif %}"
],
"membership.ldap.attribute": [ "member" ], "membership.ldap.attribute": [ "member" ],
"ignore.missing.groups": [ "true" ], "ignore.missing.groups": [ "true" ],
"group.object.classes": [ "groupOfNames" ], "group.object.classes": [ "groupOfNames" ],
@ -163,7 +158,44 @@
"drop.non.existing.groups.during.sync": [ "false" ], "drop.non.existing.groups.during.sync": [ "false" ],
"groups.path": [ "{{ applications | get_app_conf(application_id, 'rbac_groups', True) }}" ] "groups.path": [ "{{ applications | get_app_conf(application_id, 'rbac_groups', True) }}" ]
} }
}{% if keycloak_map_ldap_realm_roles | default(false) %}, },
{
"name": "phone number",
"providerId": "user-attribute-ldap-mapper",
"subComponents": {},
"config": {
"ldap.attribute": [ "telephoneNumber" ],
"is.mandatory.in.ldap": [ "false" ],
"always.read.value.from.ldap": [ "true" ],
"read.only": [ "false" ],
"user.model.attribute": [ "phoneNumber" ]
}
},
{
"name": "locale",
"providerId": "user-attribute-ldap-mapper",
"subComponents": {},
"config": {
"ldap.attribute": [ "preferredLanguage" ],
"is.mandatory.in.ldap": [ "false" ],
"always.read.value.from.ldap": [ "true" ],
"read.only": [ "false" ],
"user.model.attribute": [ "locale" ]
}
},
{
"name": "uidNumber",
"providerId": "user-attribute-ldap-mapper",
"subComponents": {},
"config": {
"ldap.attribute": [ "uidNumber" ],
"is.mandatory.in.ldap": [ "false" ],
"always.read.value.from.ldap": [ "true" ],
"read.only": [ "false" ],
"user.model.attribute": [ "uidNumber" ]
}
}
{% if keycloak_map_ldap_realm_roles | default(false) %},
{# ---------------------- LDAP -> Realm Roles (optional) -- #} {# ---------------------- LDAP -> Realm Roles (optional) -- #}
{ {
"name": "ldap-realm-roles", "name": "ldap-realm-roles",
@ -182,7 +214,6 @@
"role.object.classes": [ "groupOfNames" ] "role.object.classes": [ "groupOfNames" ]
} }
}{% endif %} }{% endif %}
] ]
}, },
"config": { "config": {
@ -225,3 +256,4 @@
"removeInvalidUsersEnabled": [ "true" ] "removeInvalidUsersEnabled": [ "true" ]
} }
} }
]

View File

@ -0,0 +1,61 @@
{% set user_profile = {
"attributes": [
{
"name": "username",
"displayName": "${username}",
"validations": {"length": {"min": 3, "max": 255}, "pattern": {"pattern": "^[a-z0-9]+$", "error-message": ""}},
"annotations": {},
"permissions": {"view": ["admin","user"], "edit": ["admin","user"]},
"multivalued": false
},
{
"name": "email",
"displayName": "${email}",
"validations": {"email": {}, "length": {"max": 255}},
"required": {"roles": ["user"]},
"permissions": {"view": ["admin","user"], "edit": ["admin","user"]},
"multivalued": false
},
{
"name": "firstName",
"displayName": "${firstName}",
"validations": {"length": {"max": 255}, "person-name-prohibited-characters": {}},
"required": {"roles": ["user"]},
"permissions": {"view": ["admin","user"], "edit": ["admin","user"]},
"multivalued": false
},
{
"name": "lastName",
"displayName": "${lastName}",
"validations": {"length": {"max": 255}, "person-name-prohibited-characters": {}},
"required": {"roles": ["user"]},
"permissions": {"view": ["admin","user"], "edit": ["admin","user"]},
"multivalued": false
},
{
"name": ldap.user.attributes.ssh_public_key,
"displayName": "SSH Public Key",
"validations": {},
"annotations": {},
"permissions": {"view": ["admin","user"], "edit": ["admin","user"]},
"group": "user-metadata",
"multivalued": true
}
],
"groups": [
{
"name": "user-metadata",
"displayHeader": "User metadata",
"displayDescription": "Attributes, which refer to user metadata"
}
]
} %}
"org.keycloak.userprofile.UserProfileProvider": [
{
"providerId": "declarative-user-profile",
"subComponents": {},
"config": {
"kc.user.profile.config": [{{ (user_profile | tojson) | tojson }}]
}
}
]

View File

@ -507,7 +507,7 @@
"fullScopeAllowed": false, "fullScopeAllowed": false,
"nodeReRegistrationTimeout": 0, "nodeReRegistrationTimeout": 0,
"defaultClientScopes": [ "defaultClientScopes": [
"web-app-origins", "web-origins",
"acr", "acr",
"roles", "roles",
"profile", "profile",
@ -572,7 +572,7 @@
} }
], ],
"defaultClientScopes": [ "defaultClientScopes": [
"web-app-origins", "web-origins",
"acr", "acr",
"roles", "roles",
"profile", "profile",
@ -614,7 +614,7 @@
"fullScopeAllowed": true, "fullScopeAllowed": true,
"nodeReRegistrationTimeout": 0, "nodeReRegistrationTimeout": 0,
"defaultClientScopes": [ "defaultClientScopes": [
"web-app-origins", "web-origins",
"acr", "acr",
"roles", "roles",
"profile", "profile",
@ -655,7 +655,7 @@
"fullScopeAllowed": false, "fullScopeAllowed": false,
"nodeReRegistrationTimeout": 0, "nodeReRegistrationTimeout": 0,
"defaultClientScopes": [ "defaultClientScopes": [
"web-app-origins", "web-origins",
"acr", "acr",
"roles", "roles",
"profile", "profile",
@ -696,7 +696,7 @@
"fullScopeAllowed": false, "fullScopeAllowed": false,
"nodeReRegistrationTimeout": 0, "nodeReRegistrationTimeout": 0,
"defaultClientScopes": [ "defaultClientScopes": [
"web-app-origins", "web-origins",
"acr", "acr",
"roles", "roles",
"profile", "profile",
@ -763,7 +763,7 @@
} }
], ],
"defaultClientScopes": [ "defaultClientScopes": [
"web-app-origins", "web-origins",
"acr", "acr",
"roles", "roles",
"profile", "profile",
@ -778,7 +778,7 @@
"microprofile-jwt" "microprofile-jwt"
] ]
}, },
{% include "client.json.j2" %} {% include "clients/default.json.j2" %}
], ],
"clientScopes": [ "clientScopes": [
{ {
@ -1057,86 +1057,10 @@
} }
] ]
}, },
{% include "scopes/rbac.json.j2" %},
{% include "scopes/nextcloud.json.j2" %},
{ {
"name": "{{ applications | get_app_conf(application_id, 'scopes.nextcloud', True) }}", "name": "web-origins",
"description": "Optimized mappers for nextcloud oidc_login with ldap.",
"protocol": "openid-connect",
"attributes": {
"include.in.token.scope": "false",
"display.on.consent.screen": "true",
"gui.order": "",
"consent.screen.text": ""
},
"protocolMappers": [
{
"name": "{{ ldap.user.attributes.nextcloud_quota }}",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-attribute-mapper",
"consentRequired": false,
"config": {
"aggregate.attrs": "false",
"introspection.token.claim": "true",
"multivalued": "false",
"userinfo.token.claim": "true",
"user.attribute": "{{ ldap.user.attributes.nextcloud_quota }}",
"id.token.claim": "true",
"lightweight.claim": "false",
"access.token.claim": "true",
"claim.name": "{{ ldap.user.attributes.nextcloud_quota }}",
"jsonType.label": "int"
}
},
{
"name": "UID Mapper",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-attribute-mapper",
"consentRequired": false,
"config": {
"aggregate.attrs": "false",
"introspection.token.claim": "true",
"multivalued": "false",
"userinfo.token.claim": "true",
"user.attribute": "username",
"id.token.claim": "true",
"lightweight.claim": "false",
"access.token.claim": "true",
"claim.name": "{{ldap.user.attributes.id}}",
"jsonType.label": "String"
}
}
]
},
{
"name": "{{ applications | get_app_conf(application_id, 'scopes.rbac_roles', True) }}",
"description": "RBAC Groups",
"protocol": "openid-connect",
"attributes": {
"include.in.token.scope": "false",
"display.on.consent.screen": "true",
"gui.order": "",
"consent.screen.text": ""
},
"protocolMappers": [
{
"name": "groups",
"protocol": "openid-connect",
"protocolMapper": "oidc-group-membership-mapper",
"consentRequired": false,
"config": {
"full.path": "true",
"introspection.token.claim": "true",
"userinfo.token.claim": "true",
"multivalued": "true",
"id.token.claim": "true",
"lightweight.claim": "false",
"access.token.claim": "true",
"claim.name": "{{ OIDC.CLAIMS.GROUPS }}"
}
}
]
},
{
"name": "web-app-origins",
"description": "OpenID Connect scope for add allowed web origins to the access token", "description": "OpenID Connect scope for add allowed web origins to the access token",
"protocol": "openid-connect", "protocol": "openid-connect",
"attributes": { "attributes": {
@ -1496,7 +1420,7 @@
"profile", "profile",
"email", "email",
"roles", "roles",
"web-app-origins", "web-origins",
"acr", "acr",
"basic" "basic"
], ],
@ -1506,7 +1430,7 @@
"phone", "phone",
"microprofile-jwt", "microprofile-jwt",
"organization", "organization",
"{{ applications | get_app_conf(application_id, 'scopes.rbac_roles', True) }}", "{{ KEYCLOAK_OIDC_RBAC_SCOPE_NAME }}",
"{{ applications | get_app_conf(application_id, 'scopes.nextcloud', True) }}" "{{ applications | get_app_conf(application_id, 'scopes.nextcloud', True) }}"
], ],
"browserSecurityHeaders": { "browserSecurityHeaders": {
@ -1642,20 +1566,8 @@
"config": {} "config": {}
} }
], ],
"org.keycloak.userprofile.UserProfileProvider": [ {%- include "components/org.keycloak.userprofile.UserProfileProvider.json.j2" -%},
{ {%- include "components/org.keycloak.storage.UserStorageProvider.json.j2" -%},
"providerId": "declarative-user-profile",
"subComponents": {},
"config": {
"kc.user.profile.config": [
"{\"attributes\":[{\"name\":\"username\",\"displayName\":\"${username}\",\"validations\":{\"length\":{\"min\":3,\"max\":255},\"pattern\":{\"pattern\":\"^[a-z0-9]+$\",\"error-message\":\"\"}},\"annotations\":{},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false},{\"name\":\"email\",\"displayName\":\"${email}\",\"validations\":{\"email\":{},\"length\":{\"max\":255}},\"required\":{\"roles\":[\"user\"]},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false},{\"name\":\"firstName\",\"displayName\":\"${firstName}\",\"validations\":{\"length\":{\"max\":255},\"person-name-prohibited-characters\":{}},\"required\":{\"roles\":[\"user\"]},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false},{\"name\":\"lastName\",\"displayName\":\"${lastName}\",\"validations\":{\"length\":{\"max\":255},\"person-name-prohibited-characters\":{}},\"required\":{\"roles\":[\"user\"]},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false},{\"name\":\"{{ ldap.user.attributes.ssh_public_key }}\",\"displayName\":\"SSH Public Key\",\"validations\":{},\"annotations\":{},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"group\":\"user-metadata\",\"multivalued\":true}],\"groups\":[{\"name\":\"user-metadata\",\"displayHeader\":\"User metadata\",\"displayDescription\":\"Attributes, which refer to user metadata\"}]}"
]
}
}
],
"org.keycloak.storage.UserStorageProvider": [
{% include "ldap.json.j2" %}
],
"org.keycloak.keys.KeyProvider": [ "org.keycloak.keys.KeyProvider": [
{ {
"name": "rsa-enc-generated", "name": "rsa-enc-generated",

View File

@ -0,0 +1,49 @@
{
"name": "{{ applications | get_app_conf(application_id, 'scopes.nextcloud') }}",
"description": "Optimized mappers for nextcloud oidc_login with ldap.",
"protocol": "openid-connect",
"attributes": {
"include.in.token.scope": "false",
"display.on.consent.screen": "true",
"gui.order": "",
"consent.screen.text": ""
},
"protocolMappers": [
{
"name": "{{ ldap.user.attributes.nextcloud_quota }}",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-attribute-mapper",
"consentRequired": false,
"config": {
"aggregate.attrs": "false",
"introspection.token.claim": "true",
"multivalued": "false",
"userinfo.token.claim": "true",
"user.attribute": "{{ ldap.user.attributes.nextcloud_quota }}",
"id.token.claim": "true",
"lightweight.claim": "false",
"access.token.claim": "true",
"claim.name": "{{ ldap.user.attributes.nextcloud_quota }}",
"jsonType.label": "int"
}
},
{
"name": "UID Mapper",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-attribute-mapper",
"consentRequired": false,
"config": {
"aggregate.attrs": "false",
"introspection.token.claim": "true",
"multivalued": "false",
"userinfo.token.claim": "true",
"user.attribute": "username",
"id.token.claim": "true",
"lightweight.claim": "false",
"access.token.claim": "true",
"claim.name": "{{ldap.user.attributes.id}}",
"jsonType.label": "String"
}
}
]
}

View File

@ -0,0 +1,29 @@
{
"name": "{{ KEYCLOAK_OIDC_RBAC_SCOPE_NAME }}",
"description": "RBAC Groups",
"protocol": "openid-connect",
"attributes": {
"include.in.token.scope": "false",
"display.on.consent.screen": "true",
"gui.order": "",
"consent.screen.text": ""
},
"protocolMappers": [
{
"name": "groups",
"protocol": "openid-connect",
"protocolMapper": "oidc-group-membership-mapper",
"consentRequired": false,
"config": {
"full.path": "true",
"introspection.token.claim": "true",
"userinfo.token.claim": "true",
"multivalued": "true",
"id.token.claim": "true",
"lightweight.claim": "false",
"access.token.claim": "true",
"claim.name": "{{ OIDC.CLAIMS.GROUPS }}"
}
}
]
}

View File

@ -10,28 +10,35 @@ 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" KEYCLOAK_SERVER_INTERNAL_URL: "http://127.0.0.1:8080"
KEYCLOAK_OIDC_RBAC_SCOPE_NAME: "{{ OIDC.CLAIMS.GROUPS }}"
KEYCLOAK_LOAD_DEPENDENCIES: "{{ applications | get_app_conf(application_id, 'load_dependencies') }}"
# Credentials ## Health
KEYCLOAK_HEALTH_ENABLED: true
## Import
KEYCLOAK_REALM_IMPORT_ENABLED: "{{ applications | get_app_conf(application_id, 'actions.import_realm') }}"
KEYCLOAK_REALM_IMPORT_DIR_HOST: "{{ docker_compose.directories.volumes }}import/"
KEYCLOAK_REALM_IMPORT_DIR_DOCKER: "/opt/keycloak/data/import/"
KEYCLOAK_REALM_IMPORT_FILE_SRC: "import/realm.json.j2"
KEYCLOAK_REALM_IMPORT_FILE_DST: "{{ KEYCLOAK_REALM_IMPORT_DIR_HOST }}/realm.json"
## Credentials
KEYCLOAK_ADMIN: "{{ applications | get_app_conf(application_id, 'users.administrator.username') }}" KEYCLOAK_ADMIN: "{{ applications | get_app_conf(application_id, 'users.administrator.username') }}"
KEYCLOAK_ADMIN_PASSWORD: "{{ applications | get_app_conf(application_id, 'credentials.administrator_password') }}" KEYCLOAK_ADMIN_PASSWORD: "{{ applications | get_app_conf(application_id, 'credentials.administrator_password') }}"
## Docker ## Docker
KEYCLOAK_CONTAINER: "{{ applications | get_app_conf(application_id, 'docker.services.keycloak.name') }}" # Name of the keycloak docker container KEYCLOAK_CONTAINER: "{{ applications | get_app_conf(application_id, 'docker.services.keycloak.name') }}" # Name of the keycloak docker container
KEYCLOAK_DOCKER_IMPORT_DIR: "/opt/keycloak/data/import/" # Directory in which keycloak import files are placed in the running docker container
KEYCLOAK_EXEC_KCADM: "docker exec -i {{ KEYCLOAK_CONTAINER }} /opt/keycloak/bin/kcadm.sh" # Init script for keycloak KEYCLOAK_EXEC_KCADM: "docker exec -i {{ KEYCLOAK_CONTAINER }} /opt/keycloak/bin/kcadm.sh" # Init script for keycloak
KEYCLOAK_IMAGE: "{{ applications | get_app_conf(application_id, 'docker.services.keycloak.image') }}" # Keycloak docker image KEYCLOAK_IMAGE: "{{ applications | get_app_conf(application_id, 'docker.services.keycloak.image') }}" # Keycloak docker image
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_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 }}"
## 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_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: >-
@ -54,7 +61,6 @@ KEYCLOAK_MASTER_API_USER: "{{ applications | get_app_conf(application_
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 # Dictionaries
KEYCLOAK_DICTIONARY_REALM_RAW: "{{ lookup('template', 'import/realm.json.j2') }}" KEYCLOAK_DICTIONARY_REALM_RAW: "{{ lookup('template', 'import/realm.json.j2') }}"
KEYCLOAK_DICTIONARY_REALM: >- KEYCLOAK_DICTIONARY_REALM: >-