--- # PROJECTION DES GROUPES LDAP (D-65). LDAP porte les habilitations ; Keycloak les # traduit en groupes de realm, puis en roles que les services lisent dans le jeton. # # Sans ce mapper, la chaine s'arrete avant le premier service : les groupes LDAP # n'atteignent jamais Grafana, Forgejo ni Nextcloud, et l'assignation se fait # utilisateur par utilisateur — ce que D-66 interdit. Constate le 2026-08-07. - name: Projeter les groupes LDAP dans le realm (group-ldap-mapper) ansible.builtin.shell: executable: /bin/bash cmd: | set -euo pipefail KC={{ serveur_keycloak_home }}/bin/kcadm.sh "$KC" config credentials --server http://localhost:8080 --realm master \ --user {{ serveur_keycloak_admin_user }} --password "$KC_ADMIN_PW" >/dev/null CHANGED=0 # Le provider de federation, retrouve par son NOM : son identifiant est # genere a la creation, on ne peut pas le supposer. FID=$("$KC" get components -r {{ serveur_keycloak_realm }} \ -q type=org.keycloak.storage.UserStorageProvider \ --fields id,name --format csv --noquotes 2>/dev/null \ | awk -F, '$2 == "{{ serveur_keycloak_ldap_nom }}" {print $1}' | head -1) if [ -z "$FID" ]; then echo "SETOPS_ERREUR: provider LDAP '{{ serveur_keycloak_ldap_nom }}' introuvable" >&2 exit 3 fi EXISTE=$("$KC" get components -r {{ serveur_keycloak_realm }} \ -q parent="$FID" -q type=org.keycloak.storage.ldap.mappers.LDAPStorageMapper \ --fields name --format csv --noquotes 2>/dev/null \ | grep -c '^{{ serveur_keycloak_groupes_mapper_nom }}$' || true) if [ "$EXISTE" = 0 ]; then "$KC" create components -r {{ serveur_keycloak_realm }} \ -s name={{ serveur_keycloak_groupes_mapper_nom }} \ -s providerId=group-ldap-mapper \ -s providerType=org.keycloak.storage.ldap.mappers.LDAPStorageMapper \ -s parentId="$FID" \ -s 'config."groups.dn"=["{{ serveur_keycloak_groupes_dn }}"]' \ -s 'config."group.name.ldap.attribute"=["cn"]' \ -s 'config."group.object.classes"=["{{ serveur_keycloak_groupes_object_class }}"]' \ -s 'config."membership.ldap.attribute"=["{{ serveur_keycloak_groupes_membership_attr }}"]' \ -s 'config."membership.attribute.type"=["DN"]' \ -s 'config."membership.user.ldap.attribute"=["{{ serveur_keycloak_ldap_username_attr }}"]' \ -s 'config."mode"=["READ_ONLY"]' \ -s 'config."user.roles.retrieve.strategy"=["LOAD_GROUPS_BY_MEMBER_ATTRIBUTE"]' \ -s 'config."preserve.group.inheritance"=["false"]' \ -s 'config."drop.non.existing.groups.during.sync"=["false"]' >/dev/null CHANGED=1 fi # La synchronisation IMPORTE les groupes : sans elle, le mapper existe mais # le realm reste vide jusqu'a la premiere connexion d'un membre. MID=$("$KC" get components -r {{ serveur_keycloak_realm }} -q parent="$FID" \ --fields id,name --format csv --noquotes 2>/dev/null \ | awk -F, '$2 == "{{ serveur_keycloak_groupes_mapper_nom }}" {print $1}' | head -1) # PAS de `|| true` ici : un echec de synchronisation doit se voir. Masque, il # m'a coute la moitie du diagnostic — le mapper existait, le realm restait # vide, et rien ne disait pourquoi (2026-08-07). if [ -n "$MID" ]; then if ! "$KC" create "user-storage/$FID/mappers/$MID/sync?direction=fedToKeycloak" \ -r {{ serveur_keycloak_realm }} 2>/tmp/setops-sync.err >/dev/null; then echo "SETOPS_ERREUR: synchronisation des groupes echouee : $(cat /tmp/setops-sync.err)" >&2 rm -f /tmp/setops-sync.err exit 5 fi rm -f /tmp/setops-sync.err fi [ "$CHANGED" = 1 ] && echo SETOPS_CHANGED || echo SETOPS_OK environment: KC_ADMIN_PW: "{{ serveur_keycloak_admin_password }}" register: serveur_keycloak_grp changed_when: "'SETOPS_CHANGED' in serveur_keycloak_grp.stdout" no_log: true retries: 5 delay: 6 until: serveur_keycloak_grp.rc == 0 when: not ansible_check_mode # LE GROUPE PORTE LE ROLE, et c'est ce qui remplace l'assignation nominative. # Un membre du groupe LDAP herite du role de realm sans que personne ne le nomme : # ajouter quelqu'un a `cn=sysadmin` dans LDAP suffit (D-66). - name: Attacher les rôles de realm aux groupes projetés ansible.builtin.shell: executable: /bin/bash cmd: | set -euo pipefail KC={{ serveur_keycloak_home }}/bin/kcadm.sh "$KC" config credentials --server http://localhost:8080 --realm master \ --user {{ serveur_keycloak_admin_user }} --password "$KC_ADMIN_PW" >/dev/null CHANGED=0 {% for lien in serveur_keycloak_groupes_roles %} GID=$("$KC" get groups -r {{ serveur_keycloak_realm }} \ --fields id,name --format csv --noquotes 2>/dev/null \ | awk -F, '$2 == "{{ lien.groupe }}" {print $1}' | head -1) if [ -z "$GID" ]; then echo "SETOPS_ERREUR: groupe '{{ lien.groupe }}' absent du realm (synchronise ?)" >&2 exit 4 fi {% for role in lien.roles | default([]) %} if ! "$KC" get "groups/$GID/role-mappings/realm" -r {{ serveur_keycloak_realm }} \ --fields name --format csv --noquotes 2>/dev/null | grep -q '^{{ role }}$'; then "$KC" add-roles -r {{ serveur_keycloak_realm }} --gid "$GID" --rolename {{ role }} >/dev/null CHANGED=1 fi {% endfor %} {# Roles de CLIENT — `realm-admin` du client `realm-management` en est un. Ils vivent dans un espace de noms distinct des roles de realm : `add-roles` les distingue par `--cclientid`. Sans ca, administrer le realm resterait le privilege du seul compte local de secours. #} {% for cl in lien.roles_client | default([]) %} {% for role in cl.roles %} # L'API attend l'UUID du client, PAS son clientId : interroger par le nom rend # une erreur, la verification echoue toujours, et la tache se declare `changed` # a chaque passage alors que le role est deja la. Constate le 2026-08-07. CCID=$("$KC" get clients -r {{ serveur_keycloak_realm }} -q clientId={{ cl.client }} \ --fields id --format csv --noquotes 2>/dev/null | head -1 || true) if [ -z "$CCID" ]; then echo "SETOPS_ERREUR: client '{{ cl.client }}' introuvable dans le realm" >&2; exit 6 fi if ! "$KC" get "groups/$GID/role-mappings/clients/$CCID" \ -r {{ serveur_keycloak_realm }} --fields name --format csv --noquotes 2>/dev/null \ | grep -q '^{{ role }}$'; then "$KC" add-roles -r {{ serveur_keycloak_realm }} --gid "$GID" \ --cclientid {{ cl.client }} --rolename {{ role }} >/dev/null CHANGED=1 fi {% endfor %} {% endfor %} {% endfor %} [ "$CHANGED" = 1 ] && echo SETOPS_CHANGED || echo SETOPS_OK environment: KC_ADMIN_PW: "{{ serveur_keycloak_admin_password }}" register: serveur_keycloak_grp_roles changed_when: "'SETOPS_CHANGED' in serveur_keycloak_grp_roles.stdout" no_log: true when: - serveur_keycloak_groupes_roles | length > 0 - not ansible_check_mode # Le mapper de PROTOCOLE : sans lui, les groupes existent dans le realm mais # n'apparaissent JAMAIS dans le jeton — Forgejo et Nextcloud lisent un claim vide # et n'accordent rien. C'est le dernier maillon de la chaine. - name: Émettre le claim de groupes dans le jeton des clients ansible.builtin.shell: executable: /bin/bash cmd: | set -euo pipefail KC={{ serveur_keycloak_home }}/bin/kcadm.sh "$KC" config credentials --server http://localhost:8080 --realm master \ --user {{ serveur_keycloak_admin_user }} --password "$KC_ADMIN_PW" >/dev/null CID=$("$KC" get clients -r {{ serveur_keycloak_realm }} -q clientId={{ item }} 2>/dev/null \ | grep -oP '"id"\s*:\s*"\K[^"]+' | head -1) if [ -z "$CID" ]; then echo "SETOPS_OK client absent"; exit 0; fi if "$KC" get clients/$CID/protocol-mappers/models -r {{ serveur_keycloak_realm }} 2>/dev/null \ | grep -q '"groupes-membres"'; then echo SETOPS_OK else # `full.path=false` : le claim porte `sysadmin`, pas `/sysadmin`. Les # services comparent un nom de groupe, pas un chemin. "$KC" create clients/$CID/protocol-mappers/models -r {{ serveur_keycloak_realm }} \ -s name=groupes-membres -s protocol=openid-connect \ -s protocolMapper=oidc-group-membership-mapper \ -s 'config."claim.name"={{ serveur_keycloak_groupes_claim }}' \ -s 'config."full.path"=false' \ -s 'config."id.token.claim"=true' \ -s 'config."access.token.claim"=true' \ -s 'config."userinfo.token.claim"=true' >/dev/null echo SETOPS_CHANGED fi environment: KC_ADMIN_PW: "{{ serveur_keycloak_admin_password }}" loop: "{{ serveur_keycloak_groupes_mapper_clients }}" register: serveur_keycloak_grp_mapper changed_when: "'SETOPS_CHANGED' in serveur_keycloak_grp_mapper.stdout" no_log: true when: - serveur_keycloak_groupes_mapper_clients | length > 0 - not ansible_check_mode # --- Rendre le changement de mot de passe EXPLICITE --------------------------- # OpenLDAP marque `pwdReset` et refuse les operations au niveau du PROTOCOLE ; # Keycloak a son propre systeme d'actions requises et NE LIT PAS `pwdReset`. Les # deux mecanismes ne se parlent pas : l'utilisateur entre avec son jeton, atterrit # dans la console de compte, et rien ne lui dit pourquoi ni quoi faire. Constate le # 2026-08-07 par l'operateur : « ce n'est pas du tout explicite ». # # On aligne donc Keycloak sur l'annuaire : tant que `pwdReset` est vrai, l'action # `UPDATE_PASSWORD` est posee et Keycloak affiche un ecran qui l'explique. Elle # disparait d'elle-meme quand le mot de passe change — l'annuaire retire `pwdReset`, # et la condition cesse d'etre vraie. - name: Lire `pwdReset` sur le compte d'amorçage (délégué à l'annuaire) ansible.builtin.command: argv: - ldapsearch - "-x" - "-H" - "ldapi:///" - "-D" - "{{ resoudre_annuaire_bind_dn }}" - "-w" - "{{ resoudre_annuaire_bind_password }}" - "-b" - "uid={{ serveur_keycloak_amorcage_uid }},{{ resoudre_annuaire_users_dn }}" - "+" delegate_to: "{{ groups['serveur_openldap'][0] }}" register: serveur_keycloak_pwdreset changed_when: false failed_when: false no_log: true when: - serveur_keycloak_amorcage_uid | length > 0 - (groups['serveur_openldap'] | default([])) | length > 0 - not ansible_check_mode - name: Exiger le changement de mot de passe dans Keycloak aussi ansible.builtin.shell: executable: /bin/bash cmd: | set -euo pipefail KC={{ serveur_keycloak_home }}/bin/kcadm.sh "$KC" config credentials --server http://localhost:8080 --realm master \ --user {{ serveur_keycloak_admin_user }} --password "$KC_ADMIN_PW" >/dev/null UID_KC=$("$KC" get users -r {{ serveur_keycloak_realm }} \ -q username={{ serveur_keycloak_amorcage_uid }} \ --fields id --format csv --noquotes 2>/dev/null | head -1 || true) if [ -z "$UID_KC" ]; then echo "SETOPS_OK (pas encore importe)"; exit 0; fi if "$KC" get users/"$UID_KC" -r {{ serveur_keycloak_realm }} 2>/dev/null \ | grep -q 'UPDATE_PASSWORD'; then echo SETOPS_OK else "$KC" update users/"$UID_KC" -r {{ serveur_keycloak_realm }} \ -s 'requiredActions=["UPDATE_PASSWORD"]' >/dev/null echo SETOPS_CHANGED fi environment: KC_ADMIN_PW: "{{ serveur_keycloak_admin_password }}" register: serveur_keycloak_req_action changed_when: "'SETOPS_CHANGED' in serveur_keycloak_req_action.stdout" no_log: true when: - serveur_keycloak_amorcage_uid | length > 0 - "'pwdReset: TRUE' in (serveur_keycloak_pwdreset.stdout | default(''))" - not ansible_check_mode