keycloak : group-ldap-mapper — le role s'attache au GROUPE
La chaine est complete : LDAP cn=sysadmin -> groupe de realm -> role grafana-admin -> Admin dans Grafana. Ajouter quelqu'un au groupe dans l'annuaire lui ouvre Grafana sans deploiement et sans que personne ne soit nomme (D-66). Rejoue : changed=0. Defaut de fond trouve en chemin : la federation LDAP n'etait JAMAIS reconciliee. `federation-ldap.yml` creait le provider s'il manquait puis ne le corrigeait plus — il pointait encore `ldaps://id-ldap-01...`, le nom errone corrige le matin meme dans `resoudre_annuaire`. La synchronisation echouait sur `UnknownHost`. C'est le revers de D-67 au mauvais endroit : l'INFRASTRUCTURE se reconcilie, seules les appartenances ne le sont pas. URL, usersDn et bindDn sont desormais corriges a chaque passage. Et j'avais masque l'echec avec `|| true` : le mapper existait, le realm restait vide, rien ne disait pourquoi. Retire — l'erreur remonte avec son message. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
6f2acafdb6
commit
74a2456f09
6 changed files with 216 additions and 15 deletions
33
CHANGELOG.md
33
CHANGELOG.md
|
|
@ -2,6 +2,39 @@
|
|||
|
||||
## 2026-08-06 — le chemin nord-sud devient dérivable
|
||||
|
||||
### La chaîne d'habilitation est complète
|
||||
|
||||
`group-ldap-mapper` construit, et le rôle est **attaché au groupe** — pas à une personne.
|
||||
|
||||
```
|
||||
LDAP cn=sysadmin,ou=groups member: uid=sysadmin
|
||||
Keycloak groupe `sysadmin` projeté, membre `sysadmin`
|
||||
rôle de realm `grafana-admin` attaché AU GROUPE
|
||||
Grafana claim roles → oidc_role_path → Admin
|
||||
```
|
||||
|
||||
Ajouter quelqu'un à `cn=sysadmin` dans l'annuaire lui ouvre Grafana en Admin : **sans toucher
|
||||
au dépôt, sans déploiement, sans nommer personne**. C'est D-66 réalisé, et c'est ce qui rend
|
||||
la reprise par le sysadmin effective. Rejoué : `changed=0`.
|
||||
|
||||
`serveur_keycloak_role_assignments` reste vide, et son commentaire dit maintenant que c'est
|
||||
définitif.
|
||||
|
||||
**Un défaut de fond découvert en chemin : la fédération n'était jamais réconciliée.**
|
||||
`federation-ldap.yml` créait le provider s'il manquait, puis ne le corrigeait plus jamais. Le
|
||||
provider pointait donc encore `ldaps://id-ldap-01.chezlepro.internal` — le nom d'hôte erroné
|
||||
corrigé le matin même dans `resoudre_annuaire`. La fédération était muette, et la
|
||||
synchronisation des groupes échouait sur un laconique `UnknownHost`.
|
||||
|
||||
C'est le revers de D-67 appliqué au mauvais endroit : **l'infrastructure se réconcilie**,
|
||||
seules les appartenances ne le sont pas. L'URL, le DN des utilisateurs et le DN de liaison
|
||||
sont désormais corrigés à chaque passage.
|
||||
|
||||
**Et j'avais masqué l'échec.** La synchronisation portait `|| true` : le mapper existait, le
|
||||
realm restait vide, et rien ne disait pourquoi. Ça m'a coûté la moitié du diagnostic. Le `||
|
||||
true` est retiré, l'erreur remonte avec son message.
|
||||
|
||||
|
||||
### Les cinq `meta/acces.yml` — et ce qu'ils rendent visible
|
||||
|
||||
Chaque rôle web déclare désormais le **groupe** qu'il reconnaît et ce qu'il lui accorde. Un
|
||||
|
|
|
|||
|
|
@ -68,3 +68,24 @@ serveur_keycloak_role_assignments: []
|
|||
# chiffre + verifie le cert serveur contre le root_ca step-ca (l'hote doit etre dans le SAN).
|
||||
serveur_keycloak_db_sslmode: ""
|
||||
serveur_keycloak_db_sslrootcert: "/etc/step/certs/root_ca.crt"
|
||||
|
||||
# --- 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 et l'assignation se fait utilisateur par utilisateur —
|
||||
# ce que D-66 interdit.
|
||||
serveur_keycloak_groupes_ldap: true
|
||||
serveur_keycloak_groupes_mapper_nom: "groupes-ldap"
|
||||
|
||||
# DERIVE de l'annuaire (resoudre_annuaire), jamais ecrit.
|
||||
serveur_keycloak_groupes_dn: "ou=groups,{{ resoudre_annuaire_base_dn | default('') }}"
|
||||
|
||||
# Le schema REEL de l'annuaire Set-OPS : `serveur_openldap` cree `ou=groups`, et
|
||||
# `amorcage_acces` y pose des `groupOfNames` dont les membres sont des DN.
|
||||
serveur_keycloak_groupes_object_class: "groupOfNames"
|
||||
serveur_keycloak_groupes_membership_attr: "member"
|
||||
|
||||
# Quel groupe LDAP porte quel role de realm. C'est ce qui REMPLACE l'assignation
|
||||
# nominative : ajouter quelqu'un au groupe suffit, aucun deploiement n'est requis.
|
||||
# Les roles doivent exister (`serveur_keycloak_realm_roles`).
|
||||
serveur_keycloak_groupes_roles: []
|
||||
|
|
|
|||
|
|
@ -5,26 +5,25 @@
|
|||
#
|
||||
# `projette` decrit la traduction attendue ; `porte_par` dit ce qui la realise.
|
||||
acces_projection:
|
||||
# /!\ LA PROJECTION N'EXISTE PAS. `serveur_keycloak_role_assignments` assigne
|
||||
# un role a un UTILISATEUR, nommement — ce que D-66 interdit. Le commentaire du
|
||||
# role l'admettait deja : « En prod, preferer l'assignation via groupe
|
||||
# d'annuaire ; ici, explicite pour la preuve. »
|
||||
#
|
||||
# Sans mapper `group-ldap-mapper` + politique role<-groupe, la chaine s'arrete
|
||||
# ici : les groupes LDAP n'atteignent jamais les services. Constate le
|
||||
# 2026-08-07, au moment d'ecrire les meta/acces.yml des cinq roles web.
|
||||
# `group-ldap-mapper` importe les groupes de l'annuaire dans le realm, et le
|
||||
# ROLE EST ATTACHE AU GROUPE — pas a une personne (D-66). Ajouter quelqu'un a
|
||||
# `cn=sysadmin,ou=groups` lui ouvre Grafana en Admin, sans deploiement.
|
||||
# `serveur_keycloak_groupes_roles` declare la correspondance.
|
||||
- groupe: sysadmin
|
||||
projette: [grafana-admin]
|
||||
porte_par: aucun
|
||||
porte_par: group-ldap-mapper
|
||||
raison: >-
|
||||
Le groupe d'administration doit devenir le role de realm que Grafana lit
|
||||
dans le claim `roles`.
|
||||
Le groupe d'administration devient le role de realm que Grafana lit dans le
|
||||
claim `roles`.
|
||||
|
||||
# Ce que Keycloak accorde SUR LUI-MEME (sa propre console d'administration).
|
||||
acces:
|
||||
- groupe: sysadmin
|
||||
accorde: realm-admin
|
||||
# /!\ Non cable : la console d'administration de Keycloak reste accessible au
|
||||
# seul compte local `admin` (voute). C'est l'acces de secours de D-40 — mais il
|
||||
# devrait AUSSI etre atteignable par le groupe, sans quoi administrer le realm
|
||||
# oblige a passer par le compte de secours.
|
||||
porte_par: aucun
|
||||
raison: >-
|
||||
Administration du realm : clients, mappers, politiques. Aujourd'hui seul le
|
||||
compte local `admin` (voute) l'obtient.
|
||||
Administration du realm : clients, mappers, politiques.
|
||||
|
|
|
|||
|
|
@ -23,8 +23,31 @@
|
|||
fi
|
||||
RID=$("$KC" get realms/{{ serveur_keycloak_realm }} --fields id 2>/dev/null \
|
||||
| grep -oiE '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}' | head -1)
|
||||
if ! "$KC" get components -r {{ serveur_keycloak_realm }} -q name={{ serveur_keycloak_ldap_nom }} 2>/dev/null \
|
||||
| grep -q '"{{ serveur_keycloak_ldap_nom }}"'; then
|
||||
# Le provider est cree S'IL MANQUE, et son URL de connexion est RECONCILIEE
|
||||
# s'il existe. Creer sans jamais corriger laissait une valeur perimee vivre
|
||||
# indefiniment : le 2026-08-07, le provider pointait encore
|
||||
# `ldaps://id-ldap-01...` — le nom d'hote errone corrige le matin meme dans
|
||||
# `resoudre_annuaire`. La federation etait muette, et la synchronisation des
|
||||
# groupes echouait sur `UnknownHost`.
|
||||
#
|
||||
# C'est de l'INFRASTRUCTURE : elle se reconcilie. Seules les APPARTENANCES
|
||||
# ne le sont pas (D-67), et elles ne vivent pas ici.
|
||||
FEDID=$("$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 [ -n "$FEDID" ]; then
|
||||
URL_ACTUELLE=$("$KC" get components/"$FEDID" -r {{ serveur_keycloak_realm }} 2>/dev/null \
|
||||
| grep -o '"connectionUrl" : \[ "[^"]*"' | grep -o 'ldaps\?://[^"]*' | head -1)
|
||||
if [ "$URL_ACTUELLE" != "{{ serveur_keycloak_ldap_url }}" ]; then
|
||||
"$KC" update components/"$FEDID" -r {{ serveur_keycloak_realm }} \
|
||||
-s 'config.connectionUrl=["{{ serveur_keycloak_ldap_url }}"]' \
|
||||
-s 'config.usersDn=["{{ serveur_keycloak_ldap_users_dn }}"]' \
|
||||
-s 'config.bindDn=["{{ serveur_keycloak_ldap_bind_dn }}"]' >/dev/null
|
||||
CHANGED=1
|
||||
fi
|
||||
fi
|
||||
if [ -z "$FEDID" ]; then
|
||||
"$KC" create components -r {{ serveur_keycloak_realm }} \
|
||||
-s name={{ serveur_keycloak_ldap_nom }} -s providerId=ldap \
|
||||
-s providerType=org.keycloak.storage.UserStorageProvider -s parentId="$RID" \
|
||||
|
|
|
|||
119
roles/serveur_keycloak/tasks/groupes-ldap.yml
Normal file
119
roles/serveur_keycloak/tasks/groupes-ldap.yml
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
---
|
||||
# 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 %}
|
||||
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 %}
|
||||
{% 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
|
||||
|
|
@ -163,6 +163,12 @@
|
|||
ansible.builtin.include_tasks: federation-ldap.yml
|
||||
when: serveur_keycloak_ldap_federation | bool
|
||||
|
||||
- name: Projeter les groupes LDAP (mapper + roles de groupe)
|
||||
ansible.builtin.include_tasks: groupes-ldap.yml
|
||||
when:
|
||||
- serveur_keycloak_ldap_federation | bool
|
||||
- serveur_keycloak_groupes_ldap | bool
|
||||
|
||||
- name: Enregistrer les clients OIDC applicatifs
|
||||
ansible.builtin.include_tasks: clients-oidc.yml
|
||||
when: serveur_keycloak_clients | length > 0
|
||||
|
|
|
|||
Loading…
Reference in a new issue