#!/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."