2026-08-07 14:32:47 -04:00
|
|
|
---
|
|
|
|
|
# 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
|
2026-08-07 19:47:10 -04:00
|
|
|
{% for role in lien.roles | default([]) %}
|
2026-08-07 14:32:47 -04:00
|
|
|
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 %}
|
2026-08-07 19:47:10 -04:00
|
|
|
{# 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 %}
|
2026-08-07 14:32:47 -04:00
|
|
|
{% 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
|
2026-08-07 14:49:28 -04:00
|
|
|
|
|
|
|
|
# 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
|
2026-08-07 19:47:10 -04:00
|
|
|
|
|
|
|
|
# --- 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
|