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

228 lines
8.5 KiB
Django/Jinja

{
"name": "{{ KEYCLOAK_LDAP_CMP_NAME }}",
"providerId": "ldap",
"subComponents": {
"org.keycloak.storage.ldap.mappers.LDAPStorageMapper": [
{# ---------------------- First Name ---------------------- #}
{
"name": "first name",
"providerId": "user-attribute-ldap-mapper",
"subComponents": {},
"config": {
"ldap.attribute": [ "{{ ldap.user.attributes.firstname }}" ],
"attribute.force.default": [ "true" ],
"is.mandatory.in.ldap": [ "true" ],
"is.binary.attribute": [ "false" ],
"always.read.value.from.ldap": [ "true" ],
"read.only": [ "false" ],
"user.model.attribute": [ "firstName" ]
}
},
{# ---------------------- Last Name ----------------------- #}
{
"name": "last name",
"providerId": "user-attribute-ldap-mapper",
"subComponents": {},
"config": {
"ldap.attribute": [ "{{ ldap.user.attributes.surname }}" ],
"is.mandatory.in.ldap": [ "true" ],
"always.read.value.from.ldap": [ "true" ],
"read.only": [ "false" ],
"user.model.attribute": [ "lastName" ]
}
},
{# ---------------------- Full Name (cn) ------------------ #}
{
"name": "full name",
"providerId": "full-name-ldap-mapper",
"subComponents": {},
"config": {
"read.only": [ "false" ],
"write.only": [ "true" ],
"ldap.full.name.attribute": [ "{{ ldap.user.attributes.fullname }}" ]
}
},
{# ---------------------- Username ------------------------ #}
{
"name": "username",
"providerId": "user-attribute-ldap-mapper",
"subComponents": {},
"config": {
"ldap.attribute": [ "{{ ldap.user.attributes.id }}" ],
"is.mandatory.in.ldap": [ "true" ],
"attribute.force.default": [ "false" ],
"is.binary.attribute": [ "false" ],
"always.read.value.from.ldap": [ "false" ],
"read.only": [ "false" ],
"user.model.attribute": [ "username" ]
}
},
{# ---------------------- Email --------------------------- #}
{
"name": "email",
"providerId": "user-attribute-ldap-mapper",
"subComponents": {},
"config": {
"ldap.attribute": [ "{{ ldap.user.attributes.mail }}" ],
"is.mandatory.in.ldap": [ "false" ],
"read.only": [ "false" ],
"always.read.value.from.ldap": [ "false" ],
"user.model.attribute": [ "email" ]
}
},
{# ---------------------- SSH Public Key ------------------ #}
{
"name": "SSH Public Key",
"providerId": "user-attribute-ldap-mapper",
"subComponents": {},
"config": {
"ldap.attribute": [ "{{ ldap.user.attributes.ssh_public_key }}" ],
"is.mandatory.in.ldap": [ "false" ],
"attribute.force.default": [ "false" ],
"is.binary.attribute": [ "false" ],
"read.only": [ "false" ],
"always.read.value.from.ldap": [ "true" ],
"user.model.attribute": [ "{{ ldap.user.attributes.ssh_public_key }}" ]
}
},
{# ---------------------- Nextcloud Quota ----------------- #}
{
"name": "{{ ldap.user.attributes.nextcloud_quota }}",
"providerId": "user-attribute-ldap-mapper",
"subComponents": {},
"config": {
"ldap.attribute": [ "{{ ldap.user.attributes.nextcloud_quota }}" ],
"is.mandatory.in.ldap": [ "false" ],
"attribute.force.default": [ "false" ],
"is.binary.attribute": [ "false" ],
"always.read.value.from.ldap": [ "false" ],
"read.only": [ "false" ],
"user.model.attribute": [ "{{ ldap.user.attributes.nextcloud_quota }}" ]
}
},
{# ---------------------- Creation Date ------------------- #}
{
"name": "creation date",
"providerId": "user-attribute-ldap-mapper",
"subComponents": {},
"config": {
"ldap.attribute": [ "createTimestamp" ],
"is.mandatory.in.ldap": [ "false" ],
"always.read.value.from.ldap": [ "true" ],
"read.only": [ "true" ],
"user.model.attribute": [ "createTimestamp" ]
}
},
{# ---------------------- Modify Date --------------------- #}
{
"name": "modify date",
"providerId": "user-attribute-ldap-mapper",
"subComponents": {},
"config": {
"ldap.attribute": [ "modifyTimestamp" ],
"is.mandatory.in.ldap": [ "false" ],
"always.read.value.from.ldap": [ "true" ],
"read.only": [ "true" ],
"user.model.attribute": [ "modifyTimestamp" ]
}
},
{# ---------------------- LDAP Groups -> KC Groups -------- #}
{
"name": "ldap-roles",
"providerId": "group-ldap-mapper",
"subComponents": {},
"config": {
"membership.attribute.type": [ "DN" ],
"group.name.ldap.attribute": [ "cn" ],
"membership.user.ldap.attribute": [ "{{ ldap.user.attributes.id }}" ],
"preserve.group.inheritance": [ "true" ],
"groups.dn": [ "{{ ldap.dn.ou.roles }}" ],
"mode": [ "LDAP_ONLY" ],
"user.roles.retrieve.strategy": [ "LOAD_GROUPS_BY_MEMBER_ATTRIBUTE" ],
"groups.ldap.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" ],
"ignore.missing.groups": [ "true" ],
"group.object.classes": [ "groupOfNames" ],
"memberof.ldap.attribute": [ "memberOf" ],
"drop.non.existing.groups.during.sync": [ "false" ],
"groups.path": [ "{{ applications | get_app_conf(application_id, 'rbac_groups', True) }}" ]
}
}{% if keycloak_map_ldap_realm_roles | default(false) %},
{# ---------------------- LDAP -> Realm Roles (optional) -- #}
{
"name": "ldap-realm-roles",
"providerId": "role-ldap-mapper",
"subComponents": {},
"config": {
"mode": [ "LDAP_ONLY" ],
"membership.attribute.type": [ "DN" ],
"user.roles.retrieve.strategy": [ "LOAD_ROLES_BY_MEMBER_ATTRIBUTE" ],
"roles.dn": [ "{{ ldap.dn.ou.roles }}" ],
"membership.ldap.attribute": [ "member" ],
"membership.user.ldap.attribute": [ "{{ ldap.user.attributes.id }}" ],
"memberof.ldap.attribute": [ "memberOf" ],
"role.name.ldap.attribute": [ "cn" ],
"use.realm.roles.mapping": [ "true" ],
"role.object.classes": [ "groupOfNames" ]
}
}{% endif %}
]
},
"config": {
"fullSyncPeriod": [ "-1" ],
"pagination": [ "true" ],
"connectionTrace": [ "false" ],
"startTls": [ "false" ],
"usersDn": [ "{{ ldap.dn.ou.users }}" ],
"connectionPooling": [ "true" ],
"cachePolicy": [ "DEFAULT" ],
"useKerberosForPasswordAuthentication": [ "false" ],
"importEnabled": [ "true" ],
"enabled": [ "true" ],
"bindCredential": [ "{{ KEYCLOAK_LDAP_BIND_PW }}" ],
"changedSyncPeriod": [ "-1" ],
"usernameLDAPAttribute": [ "{{ ldap.user.attributes.id }}" ],
"bindDn": [ "{{ KEYCLOAK_LDAP_BIND_DN }}" ],
"vendor": [ "other" ],
"uuidLDAPAttribute": [ "{{ ldap.user.attributes.id }}" ],
"allowKerberosAuthentication": [ "false" ],
"connectionUrl": [ "{{ KEYCLOAK_LDAP_URL }}" ],
"syncRegistrations": [ "true" ],
"authType": [ "simple" ],
"krbPrincipalAttribute": [ "krb5PrincipalName" ],
"searchScope": [ "1" ],
"useTruststoreSpi": [ "always" ],
"usePasswordModifyExtendedOp": [ "true" ],
"trustEmail": [ "false" ],
{# Build objectClasses from structural + auxiliary definitions #}
"userObjectClasses": [
"{{ (ldap.user.objects.structural + (ldap.user.objects.auxiliary | dict2items | map(attribute='value') | list)) | join(', ') }}"
],
"rdnLDAPAttribute": [ "{{ ldap.user.attributes.id }}" ],
"editMode": [ "WRITABLE" ],
"validatePasswordPolicy": [ "false" ],
{# Recommended: prune Keycloak shadow users not in LDAP anymore #}
"removeInvalidUsersEnabled": [ "true" ]
}
}