La sonde demande a Keycloak de s authentifier aupres de LDAP avec la federation enregistree (testLDAPConnection, secret masque). Eprouvee sur les deux locataires ; mise en defaut par parametre. Le repere d archive deja posee est le marqueur de construction de la version, pas /tmp : changed=0 au redeploiement. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
114 lines
5.8 KiB
Django/Jinja
114 lines
5.8 KiB
Django/Jinja
#!/bin/bash
|
|
# GENERE par Set-OPS (role serveur_keycloak). Ne pas editer a la main.
|
|
#
|
|
# SONDE « identite » — le realm sert sa decouverte OIDC, et Keycloak ATTEINT l'annuaire
|
|
#
|
|
# Contrat : docs/supervision-conception.md (API des greffons Nagios) — une ligne, 0/1/2.
|
|
# Mise en defaut PAR PARAMETRE : `serveur_keycloak_sonde_realm` (realm inexistant) pour la
|
|
# premiere etape, `serveur_keycloak_sonde_federation` (federation inexistante) pour la seconde.
|
|
#
|
|
# LA FEDERATION, DEPUIS LE 2026-09-28. Cette sonde s'arretait a la decouverte OIDC. Or les
|
|
# comptes vivent dans LDAP, pas dans Keycloak : un lien rompu vers l'annuaire (certificat
|
|
# renouvele mais non recharge, mot de passe de liaison tourne d'un seul cote, slapd tombe)
|
|
# laisse la decouverte PARFAITE et refuse chaque connexion. On demande donc a Keycloak de
|
|
# s'authentifier aupres de l'annuaire AVEC SA PROPRE configuration — celle de la federation
|
|
# enregistree (`componentId`, secret masque) : c'est le meme chemin qu'une vraie connexion.
|
|
#
|
|
# LE MOT DE PASSE ADMIN EST LU LA OU IL EST, pas recopie : `/etc/keycloak/keycloak.env`,
|
|
# 0640 root:keycloak. La sonde tourne en root ; il n'est ni affiche, ni passe en argument.
|
|
set -uo pipefail
|
|
|
|
URL={{ serveur_keycloak_sonde_url }}
|
|
REALM={{ serveur_keycloak_sonde_realm }}
|
|
FEDERATION="{{ serveur_keycloak_sonde_federation | trim }}"
|
|
ENV=/etc/keycloak/keycloak.env
|
|
|
|
s=$(curl -s --max-time 10 "${URL}/realms/${REALM}/.well-known/openid-configuration" 2>/dev/null)
|
|
[[ -n "${s}" ]] || { echo "Keycloak ne repond pas sur ${URL}."; exit 2; }
|
|
printf '%s' "${s}" | grep -q '"issuer"' \
|
|
|| { echo "Le realm « ${REALM} » ne sert pas sa decouverte OIDC : ${s:0:70}"; exit 2; }
|
|
|
|
# LES POINTS D'ENTREE QUE LES CLIENTS UTILISENT VRAIMENT. Un document de decouverte
|
|
# tronque se lit comme du JSON valide et casse chaque client a la premiere redirection.
|
|
for cle in authorization_endpoint token_endpoint jwks_uri; do
|
|
printf '%s' "${s}" | grep -q "\"${cle}\"" \
|
|
|| { echo "Decouverte OIDC incomplete pour « ${REALM} » : ${cle} absent."; exit 2; }
|
|
done
|
|
|
|
if [[ -z "${FEDERATION}" ]]; then
|
|
echo "Realm « ${REALM} » : decouverte OIDC complete (pas de federation a verifier)."
|
|
exit 0
|
|
fi
|
|
|
|
KC_UTIL=$(grep -hoP '^KC_BOOTSTRAP_ADMIN_USERNAME=\K\S+' "${ENV}" 2>/dev/null | head -1)
|
|
KC_MDP=$(grep -hoP '^KC_BOOTSTRAP_ADMIN_PASSWORD=\K\S+' "${ENV}" 2>/dev/null | head -1)
|
|
if [[ -z "${KC_UTIL}" || -z "${KC_MDP}" ]]; then
|
|
echo "Realm « ${REALM} » : decouverte OIDC complete, mais federation NON verifiee (identifiants admin illisibles dans ${ENV})."
|
|
exit 1
|
|
fi
|
|
|
|
KC_UTIL="${KC_UTIL}" KC_MDP="${KC_MDP}" python3 - "${URL}" "${REALM}" "${FEDERATION}" <<'PY'
|
|
import json, os, sys, urllib.parse, urllib.request
|
|
url, realm, nom = sys.argv[1:4]
|
|
|
|
def appel(chemin, data=None, jeton=None, form=False):
|
|
h = {"Authorization": "Bearer " + jeton} if jeton else {}
|
|
if data is not None:
|
|
if form:
|
|
data = urllib.parse.urlencode(data).encode()
|
|
h["Content-Type"] = "application/x-www-form-urlencoded"
|
|
else:
|
|
data = json.dumps(data).encode()
|
|
h["Content-Type"] = "application/json"
|
|
try:
|
|
with urllib.request.urlopen(urllib.request.Request(url + chemin, data=data, headers=h), timeout=8) as r:
|
|
b = r.read()
|
|
return r.status, (json.loads(b) if b else None)
|
|
except urllib.error.HTTPError as e:
|
|
b = e.read()
|
|
try:
|
|
return e.code, json.loads(b)
|
|
except ValueError:
|
|
return e.code, {"errorMessage": f"HTTP {e.code}"}
|
|
except OSError as e:
|
|
return 0, {"errorMessage": str(e)[:80]}
|
|
|
|
c, t = appel("/realms/master/protocol/openid-connect/token",
|
|
{"grant_type": "password", "client_id": "admin-cli",
|
|
"username": os.environ["KC_UTIL"], "password": os.environ["KC_MDP"]}, form=True)
|
|
if c != 200 or not isinstance(t, dict) or "access_token" not in t:
|
|
print(f"Realm « {realm} » : decouverte OIDC complete, mais federation NON verifiee "
|
|
f"(jeton admin refuse, HTTP {c}).")
|
|
sys.exit(1)
|
|
jeton = t["access_token"]
|
|
|
|
c, comps = appel(f"/admin/realms/{realm}/components?type=org.keycloak.storage.UserStorageProvider", jeton=jeton)
|
|
if c != 200 or not isinstance(comps, list):
|
|
print(f"Realm « {realm} » : la liste des federations est illisible (HTTP {c}).")
|
|
sys.exit(1)
|
|
fed = next((x for x in comps if x.get("name") == nom), None)
|
|
if fed is None:
|
|
print(f"Realm « {realm} » : la federation « {nom} » N'EXISTE PAS "
|
|
f"({', '.join(x.get('name', '?') for x in comps) or 'aucune'}) — les comptes de l'annuaire ne peuvent pas se connecter.")
|
|
sys.exit(2)
|
|
cfg = fed.get("config", {})
|
|
val = lambda k, d="": (cfg.get(k) or [d])[0]
|
|
if val("enabled", "true") != "true":
|
|
print(f"Realm « {realm} » : la federation « {nom} » est DESACTIVEE — les comptes de l'annuaire ne peuvent pas se connecter.")
|
|
sys.exit(2)
|
|
|
|
# `**********` : Keycloak substitue le secret ENREGISTRE pour ce `componentId`. On teste donc
|
|
# la liaison telle qu'une vraie connexion la ferait, sans jamais manipuler le secret LDAP.
|
|
corps = {"action": "testAuthentication", "componentId": fed["id"], "bindCredential": "**********",
|
|
"connectionUrl": val("connectionUrl"), "bindDn": val("bindDn"),
|
|
"authType": val("authType", "simple"), "startTls": val("startTls", "false"),
|
|
"useTruststoreSpi": val("useTruststoreSpi", "always"),
|
|
"connectionTimeout": val("connectionTimeout")}
|
|
c, r = appel(f"/admin/realms/{realm}/testLDAPConnection", corps, jeton=jeton)
|
|
if c != 204:
|
|
motif = (r or {}).get("errorMessage", f"HTTP {c}") if isinstance(r, dict) else f"HTTP {c}"
|
|
print(f"Realm « {realm} » : decouverte OIDC complete, mais Keycloak N'ATTEINT PAS l'annuaire "
|
|
f"({val('connectionUrl')} : {motif}) — toute connexion echoue.")
|
|
sys.exit(2)
|
|
print(f"Realm « {realm} » : decouverte OIDC complete, federation « {nom} » authentifiee aupres de {val('connectionUrl')}.")
|
|
PY
|