From 71ed179b0c6b19d050044ca48394f4639eedc8d1 Mon Sep 17 00:00:00 2001 From: Daniel Allaire Date: Fri, 7 Aug 2026 18:16:34 -0400 Subject: [PATCH] keycloak : federation LDAP en WRITABLE, et reconciliee MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- CHANGELOG.md | 27 +++++++++++++++++++ roles/serveur_keycloak/defaults/main.yml | 17 ++++++++++++ .../tasks/federation-ldap.yml | 13 ++++++--- 3 files changed, 54 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 945fa5f..5611956 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,33 @@ ## 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 Première tentative de connexion du sysadmin : refusée. Ni le jeton ni le compte n'étaient en diff --git a/roles/serveur_keycloak/defaults/main.yml b/roles/serveur_keycloak/defaults/main.yml index 355a216..d8206a9 100644 --- a/roles/serveur_keycloak/defaults/main.yml +++ b/roles/serveur_keycloak/defaults/main.yml @@ -95,3 +95,20 @@ serveur_keycloak_groupes_roles: [] # jamais les services : Forgejo et Nextcloud liraient un claim vide. serveur_keycloak_groupes_claim: "groups" 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" diff --git a/roles/serveur_keycloak/tasks/federation-ldap.yml b/roles/serveur_keycloak/tasks/federation-ldap.yml index f35360e..d0432aa 100644 --- a/roles/serveur_keycloak/tasks/federation-ldap.yml +++ b/roles/serveur_keycloak/tasks/federation-ldap.yml @@ -39,11 +39,18 @@ 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 + # `|| 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 }} \ -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 + -s 'config.bindDn=["{{ serveur_keycloak_ldap_bind_dn }}"]' \ + -s 'config.editMode=["{{ serveur_keycloak_ldap_edit_mode }}"]' >/dev/null CHANGED=1 fi fi @@ -53,7 +60,7 @@ -s providerType=org.keycloak.storage.UserStorageProvider -s parentId="$RID" \ -s 'config.enabled=["true"]' \ -s 'config.vendor=["other"]' \ - -s 'config.editMode=["READ_ONLY"]' \ + -s 'config.editMode=["{{ serveur_keycloak_ldap_edit_mode }}"]' \ -s 'config.importEnabled=["true"]' \ -s 'config.connectionUrl=["{{ serveur_keycloak_ldap_url }}"]' \ -s 'config.usersDn=["{{ serveur_keycloak_ldap_users_dn }}"]' \