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>
199 lines
9.4 KiB
YAML
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
|