keycloak : federation LDAP en WRITABLE, et reconciliee

Le changement de mot de passe imposé echouait sur « Federated storage is not
writable » : `editMode=READ_ONLY` etait code en dur. Keycloak lisait l'annuaire
sans pouvoir y ecrire — `pwdReset` devenait un cul-de-sac, LDAP exigeant un
changement que Keycloak ne pouvait pas faire.

Trois modes, un seul tient avec la doctrine. `UNSYNCED` ferait ecrire Keycloak
dans SA base : Dovecot et Postfix, qui se lient directement a LDAP (D-39),
valideraient encore l'ancien mot de passe. Ca aurait « marche » a l'ecran en
cassant le courriel en silence. `WRITABLE` ecrit A TRAVERS : l'annuaire reste la
source unique, Keycloak n'en est qu'un client.

Le mode devient une variable et il est RECONCILIE : un provider cree en
READ_ONLY le serait reste a vie.

Deux fautes de ma part au passage : extraction ancree sur `$` alors que la ligne
finit par un guillemet, et pas de `|| true` — sous `pipefail`, un grep vide tue
le script. Masque par `no_log`, pour la troisieme fois aujourd'hui.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Daniel Allaire 2026-08-07 18:16:34 -04:00
parent c21ed62237
commit 71ed179b0c
3 changed files with 54 additions and 3 deletions

View file

@ -2,6 +2,33 @@
## 2026-08-06 — le chemin nord-sud devient dérivable ## 2026-08-06 — le chemin nord-sud devient dérivable
### « Federated storage is not writable » — la fédération était en lecture seule
Le changement de mot de passe imposé échouait : `editMode=READ_ONLY` était **codé en dur**
dans `federation-ldap.yml`. Keycloak lisait l'annuaire sans jamais pouvoir y écrire — donc
`pwdReset` était un cul-de-sac : LDAP *exige* le changement, et Keycloak *ne peut pas* le
faire.
**Trois modes, un seul tient avec la doctrine :**
| Mode | Effet | Verdict |
|---|---|---|
| `READ_ONLY` | Keycloak n'écrit jamais | le changement de mot de passe est impossible |
| `UNSYNCED` | Keycloak écrit dans **sa** base | Dovecot et Postfix, qui se lient directement à LDAP (D-39), valideraient encore l'ancien — une identité, deux mots de passe |
| `WRITABLE` | Keycloak écrit **à travers** vers LDAP | l'annuaire reste la source unique ; Keycloak n'en est qu'un client |
`UNSYNCED` aurait « marché » à l'écran tout en cassant le courriel en silence. C'est le piège
qu'il fallait éviter.
Le mode devient une variable, et il est **réconcilié** — comme l'URL depuis hier. Un provider
créé en `READ_ONLY` le serait resté à vie.
**Deux fautes de ma part dans le même correctif.** Mon extraction du mode actuel s'ancrait sur
`$` alors que la ligne finit par un guillemet : elle ne correspondait jamais. Et sans `|| true`,
un `grep` sans correspondance tue le script entier sous `pipefail`. La tâche échouait — masquée
par `no_log`, pour la troisième fois aujourd'hui.
### « invalid username or password » — c'était la porte, pas le mot de passe ### « invalid username or password » — c'était la porte, pas le mot de passe
Première tentative de connexion du sysadmin : refusée. Ni le jeton ni le compte n'étaient en Première tentative de connexion du sysadmin : refusée. Ni le jeton ni le compte n'étaient en

View file

@ -95,3 +95,20 @@ serveur_keycloak_groupes_roles: []
# jamais les services : Forgejo et Nextcloud liraient un claim vide. # jamais les services : Forgejo et Nextcloud liraient un claim vide.
serveur_keycloak_groupes_claim: "groups" serveur_keycloak_groupes_claim: "groups"
serveur_keycloak_groupes_mapper_clients: [] serveur_keycloak_groupes_mapper_clients: []
# Mode d'ecriture de la federation LDAP. TROIS valeurs possibles, et une seule tient
# avec la doctrine :
#
# READ_ONLY Keycloak lit, n'ecrit jamais. Un changement de mot de passe echoue sur
# « Federated storage is not writable » — donc `pwdReset` est un cul-de-sac :
# l'annuaire EXIGE le changement, et Keycloak ne peut pas le faire.
# UNSYNCED Keycloak ecrit dans SA base. Le mot de passe ne serait plus dans LDAP :
# Dovecot et Postfix, qui s'y lient DIRECTEMENT (D-39), continueraient de
# valider l'ancien. Une identite, deux mots de passe — exactement ce que la
# doctrine interdit.
# WRITABLE Keycloak ecrit A TRAVERS vers LDAP. L'annuaire reste la source unique ;
# Keycloak n'en est qu'un client. C'est le seul mode coherent.
#
# Constate le 2026-08-07 : READ_ONLY etait code en dur, et la premiere reprise du
# sysadmin butait dessus.
serveur_keycloak_ldap_edit_mode: "WRITABLE"

View file

@ -39,11 +39,18 @@
if [ -n "$FEDID" ]; then if [ -n "$FEDID" ]; then
URL_ACTUELLE=$("$KC" get components/"$FEDID" -r {{ serveur_keycloak_realm }} 2>/dev/null \ URL_ACTUELLE=$("$KC" get components/"$FEDID" -r {{ serveur_keycloak_realm }} 2>/dev/null \
| grep -o '"connectionUrl" : \[ "[^"]*"' | grep -o 'ldaps\?://[^"]*' | head -1) | grep -o '"connectionUrl" : \[ "[^"]*"' | grep -o 'ldaps\?://[^"]*' | head -1)
if [ "$URL_ACTUELLE" != "{{ serveur_keycloak_ldap_url }}" ]; then # `|| true` en fin de chaine : avec `pipefail`, un grep sans correspondance
# tuerait le script entier. Et l'extraction ne s'ancre pas sur `$` — la ligne
# se termine par un guillemet, pas par la valeur.
MODE_ACTUEL=$("$KC" get components/"$FEDID" -r {{ serveur_keycloak_realm }} 2>/dev/null \
| sed -n 's/.*"editMode" : \[ "\([A-Z_]*\)".*/\1/p' | head -1 || true)
if [ "$URL_ACTUELLE" != "{{ serveur_keycloak_ldap_url }}" ] \
|| [ "$MODE_ACTUEL" != "{{ serveur_keycloak_ldap_edit_mode }}" ]; then
"$KC" update components/"$FEDID" -r {{ serveur_keycloak_realm }} \ "$KC" update components/"$FEDID" -r {{ serveur_keycloak_realm }} \
-s 'config.connectionUrl=["{{ serveur_keycloak_ldap_url }}"]' \ -s 'config.connectionUrl=["{{ serveur_keycloak_ldap_url }}"]' \
-s 'config.usersDn=["{{ serveur_keycloak_ldap_users_dn }}"]' \ -s 'config.usersDn=["{{ serveur_keycloak_ldap_users_dn }}"]' \
-s 'config.bindDn=["{{ serveur_keycloak_ldap_bind_dn }}"]' >/dev/null -s 'config.bindDn=["{{ serveur_keycloak_ldap_bind_dn }}"]' \
-s 'config.editMode=["{{ serveur_keycloak_ldap_edit_mode }}"]' >/dev/null
CHANGED=1 CHANGED=1
fi fi
fi fi
@ -53,7 +60,7 @@
-s providerType=org.keycloak.storage.UserStorageProvider -s parentId="$RID" \ -s providerType=org.keycloak.storage.UserStorageProvider -s parentId="$RID" \
-s 'config.enabled=["true"]' \ -s 'config.enabled=["true"]' \
-s 'config.vendor=["other"]' \ -s 'config.vendor=["other"]' \
-s 'config.editMode=["READ_ONLY"]' \ -s 'config.editMode=["{{ serveur_keycloak_ldap_edit_mode }}"]' \
-s 'config.importEnabled=["true"]' \ -s 'config.importEnabled=["true"]' \
-s 'config.connectionUrl=["{{ serveur_keycloak_ldap_url }}"]' \ -s 'config.connectionUrl=["{{ serveur_keycloak_ldap_url }}"]' \
-s 'config.usersDn=["{{ serveur_keycloak_ldap_users_dn }}"]' \ -s 'config.usersDn=["{{ serveur_keycloak_ldap_users_dn }}"]' \