tool/keycloak_bootstrap.sh importiert den Realm `holzleitner` idempotent über die Keycloak-Admin-REST-API und konfiguriert ihn passend zum Backend (Client holzleitner-app mit PKCE + Audience-Mapper holzleitner-api + personalnummer- Claim, Rolle driver, Service-Account-Client holzleitner-provisioner, Test-User). Löst den compose-`--import-realm`-Gotcha (greift nur beim ersten Start mit leerem Volume): das Skript wendet die Realm-JSON jederzeit an. - Startet Keycloak bei Bedarf (docker compose), wartet auf Erreichbarkeit, holt Admin-Token, legt den Realm via POST /admin/realms an. - Default non-destruktiv (überspringt vorhandenen Realm); --reset löscht + importiert neu (Warnung: provisionierte Fahrer-Konten gehen verloren). - Verifiziert die Clients und gibt die passenden [keycloak]-config.toml-Werte aus. Deps: bash + curl + python3 (kein jq). Verifiziert: Import gegen Test-Realm → Token für testfahrer trägt aud=holzleitner-api, personalnummer=1001 (int), Rolle driver. README: Keycloak-Abschnitt aktualisiert (Import-Gotcha + Skript). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
170 lines
8.0 KiB
Bash
Executable File
170 lines
8.0 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# Keycloak-Bootstrap: importiert den Realm `holzleitner` über die Admin-REST-API
|
|
# und konfiguriert ihn so, dass er zum Backend passt (Client `holzleitner-app`
|
|
# mit PKCE + Audience-Mapper `holzleitner-api` + `personalnummer`-Claim, Rolle
|
|
# `driver`, Service-Account-Client `holzleitner-provisioner`, Test-User).
|
|
#
|
|
# WARUM dieses Skript? Das docker-compose-`--import-realm` greift NUR beim
|
|
# allerersten Start (solange das `keycloak-data`-Volume leer ist). Spätere
|
|
# Änderungen an realm-holzleitner.json landen sonst NICHT in Keycloak. Dieses
|
|
# Skript wendet den Realm jederzeit idempotent über die REST-API an.
|
|
#
|
|
# Quelle der Wahrheit ist `keycloak/import/realm-holzleitner.json` — dieselbe
|
|
# Datei, die auch docker-compose importiert.
|
|
#
|
|
# Aufruf (vom Projekt-Root):
|
|
# ./tool/keycloak_bootstrap.sh # anlegen, falls Realm fehlt
|
|
# ./tool/keycloak_bootstrap.sh --reset # vorhandenen Realm LÖSCHEN + neu
|
|
# ./tool/keycloak_bootstrap.sh --no-up # Keycloak nicht via compose starten
|
|
#
|
|
# Umgebungs-Overrides:
|
|
# KC_URL Keycloak-Basis-URL (Default http://localhost:8080)
|
|
# KC_ADMIN Bootstrap-Admin-User (Default admin)
|
|
# KC_ADMIN_PASSWORD Bootstrap-Admin-Passwort (Default admin)
|
|
# REALM_FILE Pfad zur Realm-JSON (Default keycloak/import/realm-holzleitner.json)
|
|
#
|
|
# ACHTUNG: `--reset` löscht den kompletten Realm inkl. aller per ERP-Sync
|
|
# provisionierten Fahrer-Konten. Ohne `--reset` lässt das Skript einen bereits
|
|
# existierenden Realm unangetastet (und sagt, wie man ihn zurücksetzt).
|
|
|
|
set -euo pipefail
|
|
|
|
# ── Konfiguration ─────────────────────────────────────────────────────────
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
|
|
|
KC_URL="${KC_URL:-http://localhost:8080}"
|
|
KC_ADMIN="${KC_ADMIN:-admin}"
|
|
KC_ADMIN_PASSWORD="${KC_ADMIN_PASSWORD:-admin}"
|
|
REALM_FILE="${REALM_FILE:-${PROJECT_ROOT}/keycloak/import/realm-holzleitner.json}"
|
|
|
|
RESET=0
|
|
DO_UP=1
|
|
for arg in "$@"; do
|
|
case "$arg" in
|
|
--reset) RESET=1 ;;
|
|
--no-up) DO_UP=0 ;;
|
|
-h|--help)
|
|
sed -n '2,30p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
|
|
exit 0 ;;
|
|
*) echo "Unbekannte Option: $arg" >&2; exit 2 ;;
|
|
esac
|
|
done
|
|
|
|
# ── Vorbedingungen ────────────────────────────────────────────────────────
|
|
command -v curl >/dev/null 2>&1 || { echo "✗ curl fehlt." >&2; exit 1; }
|
|
command -v python3 >/dev/null 2>&1 || { echo "✗ python3 fehlt (für JSON-Parsing)." >&2; exit 1; }
|
|
[ -f "${REALM_FILE}" ] || { echo "✗ Realm-Datei nicht gefunden: ${REALM_FILE}" >&2; exit 1; }
|
|
|
|
# Realm-Name aus der JSON lesen (statt hart zu kodieren).
|
|
REALM="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["realm"])' "${REALM_FILE}")"
|
|
echo "→ Realm '${REALM}' aus ${REALM_FILE}"
|
|
echo "→ Keycloak: ${KC_URL}"
|
|
|
|
# ── 1. Keycloak (optional) starten ────────────────────────────────────────
|
|
if [ "${DO_UP}" -eq 1 ]; then
|
|
if command -v docker >/dev/null 2>&1; then
|
|
echo "→ Stelle sicher, dass Keycloak + Postgres laufen (docker compose up -d) …"
|
|
(cd "${PROJECT_ROOT}" && docker compose up -d keycloak postgres >/dev/null)
|
|
else
|
|
echo " (docker nicht gefunden — überspringe Start, erwarte laufendes Keycloak)"
|
|
fi
|
|
fi
|
|
|
|
# ── 2. Auf Keycloak warten ────────────────────────────────────────────────
|
|
echo -n "→ Warte auf Keycloak"
|
|
TOKEN=""
|
|
for _ in $(seq 1 60); do
|
|
TOKEN="$(curl -s --fail \
|
|
-d "client_id=admin-cli" \
|
|
-d "username=${KC_ADMIN}" \
|
|
-d "password=${KC_ADMIN_PASSWORD}" \
|
|
-d "grant_type=password" \
|
|
"${KC_URL}/realms/master/protocol/openid-connect/token" 2>/dev/null \
|
|
| python3 -c 'import json,sys
|
|
try:
|
|
print(json.load(sys.stdin).get("access_token",""))
|
|
except Exception:
|
|
print("")' 2>/dev/null || true)"
|
|
if [ -n "${TOKEN}" ]; then echo " — bereit."; break; fi
|
|
echo -n "."
|
|
sleep 2
|
|
done
|
|
if [ -z "${TOKEN}" ]; then
|
|
echo
|
|
echo "✗ Konnte kein Admin-Token von ${KC_URL} holen (Admin ${KC_ADMIN})." >&2
|
|
echo " Läuft Keycloak? Stimmen KC_ADMIN/KC_ADMIN_PASSWORD?" >&2
|
|
exit 1
|
|
fi
|
|
|
|
auth_header="Authorization: Bearer ${TOKEN}"
|
|
|
|
# ── 3. Existiert der Realm schon? ─────────────────────────────────────────
|
|
http_code="$(curl -s -o /dev/null -w '%{http_code}' -H "${auth_header}" "${KC_URL}/admin/realms/${REALM}")"
|
|
|
|
if [ "${http_code}" = "200" ]; then
|
|
if [ "${RESET}" -eq 1 ]; then
|
|
echo "→ Realm '${REALM}' existiert — wird wegen --reset GELÖSCHT (inkl. aller provisionierten Konten) …"
|
|
curl -s --fail -X DELETE -H "${auth_header}" "${KC_URL}/admin/realms/${REALM}" >/dev/null
|
|
echo " gelöscht."
|
|
else
|
|
echo
|
|
echo "✓ Realm '${REALM}' existiert bereits — keine Änderung."
|
|
echo " Zum sauberen Neu-Import (löscht provisionierte Fahrer-Konten!):"
|
|
echo " ./tool/keycloak_bootstrap.sh --reset"
|
|
exit 0
|
|
fi
|
|
elif [ "${http_code}" != "404" ]; then
|
|
echo "✗ Unerwartete Antwort beim Realm-Check: HTTP ${http_code}" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# ── 4. Realm importieren (volle RealmRepresentation) ──────────────────────
|
|
echo "→ Importiere Realm '${REALM}' …"
|
|
import_code="$(curl -s -o /tmp/kc_import_resp.txt -w '%{http_code}' \
|
|
-X POST -H "${auth_header}" -H "Content-Type: application/json" \
|
|
--data-binary "@${REALM_FILE}" \
|
|
"${KC_URL}/admin/realms")"
|
|
|
|
if [ "${import_code}" != "201" ]; then
|
|
echo "✗ Import fehlgeschlagen: HTTP ${import_code}" >&2
|
|
echo " Antwort: $(cat /tmp/kc_import_resp.txt 2>/dev/null)" >&2
|
|
exit 1
|
|
fi
|
|
echo " importiert (HTTP 201)."
|
|
|
|
# ── 5. Verifizieren ───────────────────────────────────────────────────────
|
|
echo "→ Verifiziere Konfiguration …"
|
|
clients_json="$(curl -s --fail -H "${auth_header}" "${KC_URL}/admin/realms/${REALM}/clients")"
|
|
echo "${clients_json}" | python3 -c '
|
|
import json, sys
|
|
clients = {c["clientId"]: c for c in json.load(sys.stdin)}
|
|
ok = True
|
|
for cid in ("holzleitner-app", "holzleitner-provisioner"):
|
|
present = cid in clients
|
|
print(" client " + cid + ": " + ("OK" if present else "FEHLT"))
|
|
ok = ok and present
|
|
sys.exit(0 if ok else 1)
|
|
'
|
|
|
|
# ── 6. Summary ────────────────────────────────────────────────────────────
|
|
ISSUER="${KC_URL}/realms/${REALM}"
|
|
echo
|
|
echo "✓ Fertig. Realm '${REALM}' ist eingerichtet und passt zum Backend."
|
|
echo " ───────────────────────────────────────────────────────────────"
|
|
echo " Backend-config.toml muss dazu passen ([keycloak]-Section):"
|
|
echo " issuer_url = \"${ISSUER}\""
|
|
echo " audience = \"holzleitner-api\""
|
|
echo " realm = \"${REALM}\""
|
|
echo " admin_url = \"${KC_URL}\""
|
|
echo " provisioner_client_id = \"holzleitner-provisioner\""
|
|
echo " provisioner_client_secret = \"provisioner-dev-secret\" (Dev-Wert aus dem Realm)"
|
|
echo " ───────────────────────────────────────────────────────────────"
|
|
echo " Admin-Console : ${KC_URL}/admin/ (${KC_ADMIN} / ***)"
|
|
echo " Test-Fahrer : testfahrer / test (Personalnummer 1001, Rolle driver)"
|
|
echo
|
|
echo " Hinweis: issuer_url muss EXAKT dem 'iss'-Claim entsprechen, das Keycloak"
|
|
echo " ausstellt (abhängig von KC_HOSTNAME). Bei LAN-/USB-Wechsel siehe"
|
|
echo " tool/dev_usb.sh."
|