Set-OPS-Public/roles/serveur_keycloak/tasks/groupes-ldap.yml
Daniel Allaire 8b0eaaec0a keycloak : le claim de groupes exige les clients — troisieme defaut d'ordre
Meme motif que les roles de realm, une etape plus loin. groupes-ldap.yml
posait aussi un oidc-group-membership-mapper sur les CLIENTS, alors que
clients-oidc.yml s'execute apres. Invisible tant que les clients existaient
d'un passage precedent.

Extrait dans claim-groupes.yml, place APRES clients-oidc. J'avais d'abord
insere l'appel AVANT — le defaut meme que je corrigeais ; rattrape avant tout
deploiement.

La lecon vaut au-dela du role : un fichier de taches nomme d'apres un SUJET
(« les groupes ») rassemble des etapes aux dependances differentes, et l'ordre
qui en resulte n'est correct que par accident. Ce qui doit gouverner le
decoupage, c'est ce dont chaque etape a BESOIN, pas ce dont elle parle.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 18:06:16 -04:00

199 lines
9.4 KiB
YAML

---
# 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: 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