keycloak_bootstrap: parametrisierbar für baremetal-Prod
Das Bootstrap-Skript funktioniert jetzt gegen jede Keycloak-Instanz (Dev-Docker wie baremetal-Prod) und nimmt Realm + Server-Adresse als Parameter: - --url <basis-inkl-port>, --realm <name> (überschreibt den Namen in der JSON-Payload), --admin/--admin-password. - --provisioner-secret <s>: ersetzt das Dev-Secret in der Payload (Prod). - --no-test-user: entfernt "testfahrer" aus der Payload (Prod). - Docker wird per Default NICHT mehr angefasst (Prod=baremetal); nur via --up. - Alle Optionen auch via Env; CLI hat Vorrang. README: parametrisierte Nutzung (Dev + Prod-Beispiel) + Abschnitt "Wie die Authentifizierung funktioniert" (Skript→Keycloak Admin-Grant; Laufzeit App↔Backend↔Keycloak: PKCE-Login, JWKS-JWT-Validierung mit aud/iss, Provisioner client-credentials, X-Admin-Api-Key für /admin). Verifiziert gegen Dev-Keycloak: custom Realm + überschriebenes Secret + --no-test-user erzeugt korrekten Realm; Test-Realm danach wieder entfernt. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
57
README.md
57
README.md
@ -47,20 +47,61 @@ Admin-UI Änderungen macht, sollte sie **in die JSON zurückspielen**.
|
|||||||
|
|
||||||
**Wichtig:** Das compose-`--import-realm` greift nur beim **ersten** Start
|
**Wichtig:** Das compose-`--import-realm` greift nur beim **ersten** Start
|
||||||
(solange das `keycloak-data`-Volume leer ist) — spätere JSON-Änderungen landen
|
(solange das `keycloak-data`-Volume leer ist) — spätere JSON-Änderungen landen
|
||||||
NICHT automatisch in Keycloak. Zum Anwenden/Bootstrappen:
|
NICHT automatisch in Keycloak. Realm anwenden/bootstrappen geht daher über
|
||||||
|
`tool/keycloak_bootstrap.sh` (spricht nur die Admin-REST-API an, funktioniert
|
||||||
|
gegen **Dev-Docker wie baremetal-Prod**):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Importiert den Realm über die Admin-REST-API (idempotent, startet Keycloak
|
# Dev (lokales Docker mit hochfahren, Default-Realm "holzleitner")
|
||||||
# bei Bedarf via docker compose). Tut nichts, falls der Realm schon existiert.
|
./tool/keycloak_bootstrap.sh --up
|
||||||
./tool/keycloak_bootstrap.sh
|
|
||||||
|
# Production (baremetal Keycloak, eigener Realm, frisches Secret, kein Test-User)
|
||||||
|
./tool/keycloak_bootstrap.sh \
|
||||||
|
--url https://auth.holzleitner.de \
|
||||||
|
--realm holzleitner-prod \
|
||||||
|
--admin kc-admin --admin-password "$KC_PW" \
|
||||||
|
--provisioner-secret "$(openssl rand -hex 24)" \
|
||||||
|
--no-test-user
|
||||||
|
|
||||||
# Sauberer Neu-Import (LÖSCHT den Realm inkl. provisionierter Fahrer-Konten):
|
# Sauberer Neu-Import (LÖSCHT den Realm inkl. provisionierter Fahrer-Konten):
|
||||||
./tool/keycloak_bootstrap.sh --reset
|
./tool/keycloak_bootstrap.sh --url ... --realm ... --reset
|
||||||
```
|
```
|
||||||
|
|
||||||
Das Skript gibt am Ende die `[keycloak]`-Werte aus, die in die `config.toml`
|
Wichtigste Optionen: `--url` (Keycloak-Basis inkl. Port), `--realm` (Realm-Name),
|
||||||
gehören (issuer_url, audience, provisioner-secret …). Overrides via
|
`--admin`/`--admin-password`, `--provisioner-secret`, `--no-test-user`, `--reset`,
|
||||||
`KC_URL` / `KC_ADMIN` / `KC_ADMIN_PASSWORD` / `REALM_FILE`.
|
`--up`. Alles auch via Env (`KC_URL`, `REALM`, `KC_ADMIN`, `KC_ADMIN_PASSWORD`,
|
||||||
|
`PROVISIONER_SECRET`, `REALM_FILE`). Default ohne `--up` wird **kein** Docker
|
||||||
|
angefasst. Das Skript gibt am Ende die passenden `[keycloak]`-Werte für die
|
||||||
|
`config.toml` aus.
|
||||||
|
|
||||||
|
### Wie die Authentifizierung funktioniert
|
||||||
|
|
||||||
|
Es gibt zwei getrennte Auth-Kontexte:
|
||||||
|
|
||||||
|
**a) Bootstrap-Skript → Keycloak (einmalig/selten).** Das Skript meldet sich am
|
||||||
|
`master`-Realm über den eingebauten Client `admin-cli` per Passwort-Grant an
|
||||||
|
(`--admin`/`--admin-password`) und nutzt das Admin-Token nur, um den Realm
|
||||||
|
anzulegen. In Prod ein echtes Admin-Konto + HTTPS verwenden; das Passwort besser
|
||||||
|
über die Env-Variable `KC_ADMIN_PASSWORD` setzen (nicht in der History).
|
||||||
|
|
||||||
|
**b) Laufzeit — App ↔ Backend ↔ Keycloak.**
|
||||||
|
1. Die Flutter-App loggt sich per **OIDC Authorization Code + PKCE** gegen
|
||||||
|
`{KC_URL}/realms/{realm}` ein und erhält ein **Access-Token (JWT, RS256)** mit
|
||||||
|
`aud=holzleitner-api`, Claim `personalnummer` und Realm-Rolle `driver`.
|
||||||
|
2. Das Backend **validiert das JWT public-key-basiert**: es holt die Realm-JWKS
|
||||||
|
von `{issuer}/protocol/openid-connect/certs`, prüft Signatur, `iss` (==
|
||||||
|
`config.toml [keycloak] issuer_url`), `aud` (enthält `holzleitner-api`) und
|
||||||
|
Ablauf. Kein gemeinsames Secret nötig — nur der öffentliche Schlüssel.
|
||||||
|
3. Die `/admin`-Maschinen-Endpunkte des Backends laufen NICHT über Keycloak,
|
||||||
|
sondern über den statischen `X-Admin-Api-Key` (siehe `[admin] api_key`).
|
||||||
|
4. Der **Provisioner** (`holzleitner-provisioner`, confidential, Client-
|
||||||
|
Credentials-Grant mit `manage-users`) wird vom ERP-Sync genutzt, um neue
|
||||||
|
Fahrer-Konten im Realm anzulegen — dafür gilt das `provisioner_client_secret`.
|
||||||
|
|
||||||
|
Damit b) klappt, müssen `issuer_url`/`realm`/`admin_url` in der `config.toml`
|
||||||
|
**exakt** auf denselben Server + Realm zeigen, den das Skript angelegt hat — und
|
||||||
|
`issuer_url` muss dem von Keycloak ausgestellten `iss`-Claim entsprechen (in
|
||||||
|
Prod also Keycloaks Frontend-/Hostname-URL korrekt setzen, sonst `invalid issuer`).
|
||||||
|
|
||||||
### Token für Dev-Tests holen
|
### Token für Dev-Tests holen
|
||||||
|
|
||||||
|
|||||||
@ -1,169 +1,221 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
#
|
#
|
||||||
# Keycloak-Bootstrap: importiert den Realm `holzleitner` über die Admin-REST-API
|
# Keycloak-Bootstrap: importiert den Holzleitner-Realm über die Admin-REST-API
|
||||||
# und konfiguriert ihn so, dass er zum Backend passt (Client `holzleitner-app`
|
# und konfiguriert ihn passend zum Backend (Client `holzleitner-app` mit PKCE +
|
||||||
# mit PKCE + Audience-Mapper `holzleitner-api` + `personalnummer`-Claim, Rolle
|
# Audience-Mapper `holzleitner-api` + `personalnummer`-Claim, Rolle `driver`,
|
||||||
# `driver`, Service-Account-Client `holzleitner-provisioner`, Test-User).
|
# Service-Account-Client `holzleitner-provisioner`, optional ein Test-User).
|
||||||
#
|
#
|
||||||
# WARUM dieses Skript? Das docker-compose-`--import-realm` greift NUR beim
|
# Funktioniert gegen JEDE Keycloak-Instanz (Dev-Docker ODER baremetal-Prod) —
|
||||||
# allerersten Start (solange das `keycloak-data`-Volume leer ist). Spätere
|
# es spricht nur die REST-API an. Quelle der Realm-Definition ist
|
||||||
# Änderungen an realm-holzleitner.json landen sonst NICHT in Keycloak. Dieses
|
# `keycloak/import/realm-holzleitner.json`; Realm-Name, Keycloak-Adresse,
|
||||||
# Skript wendet den Realm jederzeit idempotent über die REST-API an.
|
# Provisioner-Secret und Test-User lassen sich beim Aufruf überschreiben.
|
||||||
#
|
#
|
||||||
# Quelle der Wahrheit ist `keycloak/import/realm-holzleitner.json` — dieselbe
|
# ── Aufruf ────────────────────────────────────────────────────────────────
|
||||||
# Datei, die auch docker-compose importiert.
|
# ./tool/keycloak_bootstrap.sh \
|
||||||
|
# --url https://auth.holzleitner.de \
|
||||||
|
# --realm holzleitner \
|
||||||
|
# --admin admin --admin-password '****'
|
||||||
#
|
#
|
||||||
# Aufruf (vom Projekt-Root):
|
# Production-Beispiel (eigener Realm, neues Secret, ohne Test-User):
|
||||||
# ./tool/keycloak_bootstrap.sh # anlegen, falls Realm fehlt
|
# ./tool/keycloak_bootstrap.sh \
|
||||||
# ./tool/keycloak_bootstrap.sh --reset # vorhandenen Realm LÖSCHEN + neu
|
# --url https://auth.holzleitner.de \
|
||||||
# ./tool/keycloak_bootstrap.sh --no-up # Keycloak nicht via compose starten
|
# --realm holzleitner-prod \
|
||||||
|
# --admin kc-admin --admin-password "$KC_PW" \
|
||||||
|
# --provisioner-secret "$(openssl rand -hex 24)" \
|
||||||
|
# --no-test-user
|
||||||
#
|
#
|
||||||
# Umgebungs-Overrides:
|
# Dev (lokales Docker mit hochfahren):
|
||||||
# KC_URL Keycloak-Basis-URL (Default http://localhost:8080)
|
# ./tool/keycloak_bootstrap.sh --up
|
||||||
# KC_ADMIN Bootstrap-Admin-User (Default admin)
|
#
|
||||||
# KC_ADMIN_PASSWORD Bootstrap-Admin-Passwort (Default admin)
|
# ── Optionen ────────────────────────────────────────────────────────────────
|
||||||
# REALM_FILE Pfad zur Realm-JSON (Default keycloak/import/realm-holzleitner.json)
|
# --url <url> Keycloak-Basis-URL inkl. Port (Default http://localhost:8080)
|
||||||
|
# --realm <name> Realm-Name (Default: aus der JSON, "holzleitner")
|
||||||
|
# --admin <user> Bootstrap-/Admin-User im master-Realm (Default admin)
|
||||||
|
# --admin-password <pw> Admin-Passwort (Default admin) [besser via Env KC_ADMIN_PASSWORD]
|
||||||
|
# --realm-file <pfad> Realm-JSON (Default keycloak/import/realm-holzleitner.json)
|
||||||
|
# --provisioner-secret <s> Überschreibt das Client-Secret von holzleitner-provisioner
|
||||||
|
# --no-test-user Entfernt den Test-User "testfahrer" aus der Payload (Prod)
|
||||||
|
# --reset Vorhandenen Realm LÖSCHEN + neu importieren (destruktiv!)
|
||||||
|
# --up Vor dem Import `docker compose up -d` (nur Dev)
|
||||||
|
#
|
||||||
|
# Alles auch via Env: KC_URL, REALM, KC_ADMIN, KC_ADMIN_PASSWORD, REALM_FILE,
|
||||||
|
# PROVISIONER_SECRET. CLI-Flags haben Vorrang.
|
||||||
|
#
|
||||||
|
# Deps: bash, curl, python3 (kein jq nötig).
|
||||||
#
|
#
|
||||||
# ACHTUNG: `--reset` löscht den kompletten Realm inkl. aller per ERP-Sync
|
# ACHTUNG: `--reset` löscht den kompletten Realm inkl. aller per ERP-Sync
|
||||||
# provisionierten Fahrer-Konten. Ohne `--reset` lässt das Skript einen bereits
|
# provisionierten Fahrer-Konten.
|
||||||
# existierenden Realm unangetastet (und sagt, wie man ihn zurücksetzt).
|
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
# ── Konfiguration ─────────────────────────────────────────────────────────
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||||
|
|
||||||
|
# ── Defaults (Env-Overrides) ───────────────────────────────────────────────
|
||||||
KC_URL="${KC_URL:-http://localhost:8080}"
|
KC_URL="${KC_URL:-http://localhost:8080}"
|
||||||
|
REALM="${REALM:-}" # leer ⇒ aus JSON lesen
|
||||||
KC_ADMIN="${KC_ADMIN:-admin}"
|
KC_ADMIN="${KC_ADMIN:-admin}"
|
||||||
KC_ADMIN_PASSWORD="${KC_ADMIN_PASSWORD:-admin}"
|
KC_ADMIN_PASSWORD="${KC_ADMIN_PASSWORD:-admin}"
|
||||||
REALM_FILE="${REALM_FILE:-${PROJECT_ROOT}/keycloak/import/realm-holzleitner.json}"
|
REALM_FILE="${REALM_FILE:-${PROJECT_ROOT}/keycloak/import/realm-holzleitner.json}"
|
||||||
|
PROVISIONER_SECRET="${PROVISIONER_SECRET:-}" # leer ⇒ Wert aus JSON behalten
|
||||||
|
NO_TEST_USER=0
|
||||||
RESET=0
|
RESET=0
|
||||||
DO_UP=1
|
DO_UP=0
|
||||||
for arg in "$@"; do
|
|
||||||
case "$arg" in
|
# ── Arg-Parsing (Flags haben Vorrang vor Env) ──────────────────────────────
|
||||||
--reset) RESET=1 ;;
|
while [ $# -gt 0 ]; do
|
||||||
--no-up) DO_UP=0 ;;
|
case "$1" in
|
||||||
-h|--help)
|
--url) KC_URL="$2"; shift 2 ;;
|
||||||
sed -n '2,30p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
|
--realm) REALM="$2"; shift 2 ;;
|
||||||
exit 0 ;;
|
--admin) KC_ADMIN="$2"; shift 2 ;;
|
||||||
*) echo "Unbekannte Option: $arg" >&2; exit 2 ;;
|
--admin-password) KC_ADMIN_PASSWORD="$2"; shift 2 ;;
|
||||||
|
--realm-file) REALM_FILE="$2"; shift 2 ;;
|
||||||
|
--provisioner-secret) PROVISIONER_SECRET="$2"; shift 2 ;;
|
||||||
|
--no-test-user) NO_TEST_USER=1; shift ;;
|
||||||
|
--reset) RESET=1; shift ;;
|
||||||
|
--up) DO_UP=1; shift ;;
|
||||||
|
-h|--help) sed -n '2,60p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; exit 0 ;;
|
||||||
|
*) echo "Unbekannte Option: $1" >&2; exit 2 ;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
# ── Vorbedingungen ────────────────────────────────────────────────────────
|
KC_URL="${KC_URL%/}" # evtl. abschließenden Slash entfernen
|
||||||
|
|
||||||
|
# ── Vorbedingungen ──────────────────────────────────────────────────────────
|
||||||
command -v curl >/dev/null 2>&1 || { echo "✗ curl fehlt." >&2; exit 1; }
|
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; }
|
command -v python3 >/dev/null 2>&1 || { echo "✗ python3 fehlt (für JSON)." >&2; exit 1; }
|
||||||
[ -f "${REALM_FILE}" ] || { echo "✗ Realm-Datei nicht gefunden: ${REALM_FILE}" >&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-Name: CLI/Env hat Vorrang, sonst aus der JSON.
|
||||||
REALM="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["realm"])' "${REALM_FILE}")"
|
if [ -z "${REALM}" ]; then
|
||||||
echo "→ Realm '${REALM}' aus ${REALM_FILE}"
|
REALM="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["realm"])' "${REALM_FILE}")"
|
||||||
echo "→ Keycloak: ${KC_URL}"
|
fi
|
||||||
|
|
||||||
# ── 1. Keycloak (optional) starten ────────────────────────────────────────
|
echo "→ Keycloak : ${KC_URL}"
|
||||||
|
echo "→ Realm : ${REALM}"
|
||||||
|
echo "→ Quelle : ${REALM_FILE}"
|
||||||
|
|
||||||
|
# ── 1. (Dev) Keycloak via docker compose starten ────────────────────────────
|
||||||
if [ "${DO_UP}" -eq 1 ]; then
|
if [ "${DO_UP}" -eq 1 ]; then
|
||||||
if command -v docker >/dev/null 2>&1; then
|
if command -v docker >/dev/null 2>&1; then
|
||||||
echo "→ Stelle sicher, dass Keycloak + Postgres laufen (docker compose up -d) …"
|
echo "→ docker compose up -d keycloak postgres (Dev) …"
|
||||||
(cd "${PROJECT_ROOT}" && docker compose up -d keycloak postgres >/dev/null)
|
(cd "${PROJECT_ROOT}" && docker compose up -d keycloak postgres >/dev/null)
|
||||||
else
|
else
|
||||||
echo " (docker nicht gefunden — überspringe Start, erwarte laufendes Keycloak)"
|
echo " (docker nicht gefunden — überspringe --up)"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ── 2. Auf Keycloak warten ────────────────────────────────────────────────
|
# ── 2. Payload aufbauen (Realm-Name/Secret/Test-User anwenden) ──────────────
|
||||||
echo -n "→ Warte auf Keycloak"
|
PAYLOAD="$(mktemp -t kc-realm.XXXXXX.json)"
|
||||||
|
trap 'rm -f "${PAYLOAD}" /tmp/kc_import_resp.$$' EXIT
|
||||||
|
python3 - "${REALM_FILE}" "${PAYLOAD}" "${REALM}" "${PROVISIONER_SECRET}" "${NO_TEST_USER}" <<'PY'
|
||||||
|
import json, sys
|
||||||
|
src, dst, realm, secret, no_test_user = sys.argv[1:6]
|
||||||
|
d = json.load(open(src))
|
||||||
|
d["realm"] = realm
|
||||||
|
if secret:
|
||||||
|
for c in d.get("clients", []):
|
||||||
|
if c.get("clientId") == "holzleitner-provisioner":
|
||||||
|
c["secret"] = secret
|
||||||
|
if no_test_user == "1":
|
||||||
|
d["users"] = [u for u in d.get("users", []) if u.get("username") != "testfahrer"]
|
||||||
|
json.dump(d, open(dst, "w"))
|
||||||
|
PY
|
||||||
|
|
||||||
|
# ── 3. Auf Keycloak warten + Admin-Token holen ──────────────────────────────
|
||||||
|
# Authentifizierung des Skripts gegen die Admin-REST-API: Resource-Owner-
|
||||||
|
# Password-Grant im master-Realm über den eingebauten Client `admin-cli`.
|
||||||
|
echo -n "→ Admin-Login (${KC_ADMIN} @ master) "
|
||||||
TOKEN=""
|
TOKEN=""
|
||||||
for _ in $(seq 1 60); do
|
for _ in $(seq 1 60); do
|
||||||
TOKEN="$(curl -s --fail \
|
TOKEN="$(curl -s --fail \
|
||||||
-d "client_id=admin-cli" \
|
-d "client_id=admin-cli" \
|
||||||
-d "username=${KC_ADMIN}" \
|
-d "username=${KC_ADMIN}" \
|
||||||
-d "password=${KC_ADMIN_PASSWORD}" \
|
--data-urlencode "password=${KC_ADMIN_PASSWORD}" \
|
||||||
-d "grant_type=password" \
|
-d "grant_type=password" \
|
||||||
"${KC_URL}/realms/master/protocol/openid-connect/token" 2>/dev/null \
|
"${KC_URL}/realms/master/protocol/openid-connect/token" 2>/dev/null \
|
||||||
| python3 -c 'import json,sys
|
| python3 -c 'import json,sys
|
||||||
try:
|
try: print(json.load(sys.stdin).get("access_token",""))
|
||||||
print(json.load(sys.stdin).get("access_token",""))
|
except Exception: print("")' 2>/dev/null || true)"
|
||||||
except Exception:
|
[ -n "${TOKEN}" ] && { echo "— ok."; break; }
|
||||||
print("")' 2>/dev/null || true)"
|
|
||||||
if [ -n "${TOKEN}" ]; then echo " — bereit."; break; fi
|
|
||||||
echo -n "."
|
echo -n "."
|
||||||
sleep 2
|
sleep 2
|
||||||
done
|
done
|
||||||
if [ -z "${TOKEN}" ]; then
|
if [ -z "${TOKEN}" ]; then
|
||||||
echo
|
echo
|
||||||
echo "✗ Konnte kein Admin-Token von ${KC_URL} holen (Admin ${KC_ADMIN})." >&2
|
echo "✗ Kein Admin-Token von ${KC_URL}. Läuft Keycloak? Stimmen --admin/--admin-password?" >&2
|
||||||
echo " Läuft Keycloak? Stimmen KC_ADMIN/KC_ADMIN_PASSWORD?" >&2
|
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
AUTH="Authorization: Bearer ${TOKEN}"
|
||||||
|
|
||||||
auth_header="Authorization: Bearer ${TOKEN}"
|
# ── 4. Realm vorhanden? ─────────────────────────────────────────────────────
|
||||||
|
CODE="$(curl -s -o /dev/null -w '%{http_code}' -H "${AUTH}" "${KC_URL}/admin/realms/${REALM}")"
|
||||||
# ── 3. Existiert der Realm schon? ─────────────────────────────────────────
|
if [ "${CODE}" = "200" ]; then
|
||||||
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
|
if [ "${RESET}" -eq 1 ]; then
|
||||||
echo "→ Realm '${REALM}' existiert — wird wegen --reset GELÖSCHT (inkl. aller provisionierten Konten) …"
|
echo "→ Realm '${REALM}' existiert — wird wegen --reset GELÖSCHT (inkl. provisionierter Konten) …"
|
||||||
curl -s --fail -X DELETE -H "${auth_header}" "${KC_URL}/admin/realms/${REALM}" >/dev/null
|
curl -s --fail -X DELETE -H "${AUTH}" "${KC_URL}/admin/realms/${REALM}" >/dev/null
|
||||||
echo " gelöscht."
|
echo " gelöscht."
|
||||||
else
|
else
|
||||||
echo
|
echo
|
||||||
echo "✓ Realm '${REALM}' existiert bereits — keine Änderung."
|
echo "✓ Realm '${REALM}' existiert bereits — keine Änderung."
|
||||||
echo " Zum sauberen Neu-Import (löscht provisionierte Fahrer-Konten!):"
|
echo " Sauberer Neu-Import (löscht provisionierte Konten!): … --reset"
|
||||||
echo " ./tool/keycloak_bootstrap.sh --reset"
|
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
elif [ "${http_code}" != "404" ]; then
|
elif [ "${CODE}" != "404" ]; then
|
||||||
echo "✗ Unerwartete Antwort beim Realm-Check: HTTP ${http_code}" >&2
|
echo "✗ Unerwartete Antwort beim Realm-Check: HTTP ${CODE}" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ── 4. Realm importieren (volle RealmRepresentation) ──────────────────────
|
# ── 5. Importieren ──────────────────────────────────────────────────────────
|
||||||
echo "→ Importiere Realm '${REALM}' …"
|
echo "→ Importiere Realm '${REALM}' …"
|
||||||
import_code="$(curl -s -o /tmp/kc_import_resp.txt -w '%{http_code}' \
|
RESP="/tmp/kc_import_resp.$$"
|
||||||
-X POST -H "${auth_header}" -H "Content-Type: application/json" \
|
IMP="$(curl -s -o "${RESP}" -w '%{http_code}' \
|
||||||
--data-binary "@${REALM_FILE}" \
|
-X POST -H "${AUTH}" -H "Content-Type: application/json" \
|
||||||
"${KC_URL}/admin/realms")"
|
--data-binary "@${PAYLOAD}" "${KC_URL}/admin/realms")"
|
||||||
|
if [ "${IMP}" != "201" ]; then
|
||||||
if [ "${import_code}" != "201" ]; then
|
echo "✗ Import fehlgeschlagen: HTTP ${IMP}" >&2
|
||||||
echo "✗ Import fehlgeschlagen: HTTP ${import_code}" >&2
|
echo " Antwort: $(cat "${RESP}" 2>/dev/null)" >&2
|
||||||
echo " Antwort: $(cat /tmp/kc_import_resp.txt 2>/dev/null)" >&2
|
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
echo " importiert (HTTP 201)."
|
echo " importiert (HTTP 201)."
|
||||||
|
|
||||||
# ── 5. Verifizieren ───────────────────────────────────────────────────────
|
# ── 6. Verifizieren ─────────────────────────────────────────────────────────
|
||||||
echo "→ Verifiziere Konfiguration …"
|
echo "→ Verifiziere Clients …"
|
||||||
clients_json="$(curl -s --fail -H "${auth_header}" "${KC_URL}/admin/realms/${REALM}/clients")"
|
curl -s --fail -H "${AUTH}" "${KC_URL}/admin/realms/${REALM}/clients" | python3 -c '
|
||||||
echo "${clients_json}" | python3 -c '
|
|
||||||
import json, sys
|
import json, sys
|
||||||
clients = {c["clientId"]: c for c in json.load(sys.stdin)}
|
ids = {c["clientId"] for c in json.load(sys.stdin)}
|
||||||
ok = True
|
ok = True
|
||||||
for cid in ("holzleitner-app", "holzleitner-provisioner"):
|
for cid in ("holzleitner-app", "holzleitner-provisioner"):
|
||||||
present = cid in clients
|
present = cid in ids
|
||||||
print(" client " + cid + ": " + ("OK" if present else "FEHLT"))
|
print(" client " + cid + ": " + ("OK" if present else "FEHLT"))
|
||||||
ok = ok and present
|
ok = ok and present
|
||||||
sys.exit(0 if ok else 1)
|
sys.exit(0 if ok else 1)
|
||||||
'
|
'
|
||||||
|
|
||||||
# ── 6. Summary ────────────────────────────────────────────────────────────
|
# ── 7. Summary ──────────────────────────────────────────────────────────────
|
||||||
ISSUER="${KC_URL}/realms/${REALM}"
|
ISSUER="${KC_URL}/realms/${REALM}"
|
||||||
|
if [ -n "${PROVISIONER_SECRET}" ]; then
|
||||||
|
SECRET_LINE="provisioner_client_secret = \"${PROVISIONER_SECRET}\" (gesetzt)"
|
||||||
|
else
|
||||||
|
SECRET_LINE="provisioner_client_secret = \"provisioner-dev-secret\" (Dev-Wert aus der JSON — in Prod via --provisioner-secret setzen!)"
|
||||||
|
fi
|
||||||
echo
|
echo
|
||||||
echo "✓ Fertig. Realm '${REALM}' ist eingerichtet und passt zum Backend."
|
echo "✓ Fertig. Realm '${REALM}' ist eingerichtet und passt zum Backend."
|
||||||
echo " ───────────────────────────────────────────────────────────────"
|
echo " ───────────────────────────────────────────────────────────────"
|
||||||
echo " Backend-config.toml muss dazu passen ([keycloak]-Section):"
|
echo " config.toml [keycloak]:"
|
||||||
echo " issuer_url = \"${ISSUER}\""
|
echo " issuer_url = \"${ISSUER}\""
|
||||||
echo " audience = \"holzleitner-api\""
|
echo " audience = \"holzleitner-api\""
|
||||||
echo " realm = \"${REALM}\""
|
echo " realm = \"${REALM}\""
|
||||||
echo " admin_url = \"${KC_URL}\""
|
echo " admin_url = \"${KC_URL}\""
|
||||||
echo " provisioner_client_id = \"holzleitner-provisioner\""
|
echo " provisioner_client_id = \"holzleitner-provisioner\""
|
||||||
echo " provisioner_client_secret = \"provisioner-dev-secret\" (Dev-Wert aus dem Realm)"
|
echo " ${SECRET_LINE}"
|
||||||
echo " ───────────────────────────────────────────────────────────────"
|
echo " ───────────────────────────────────────────────────────────────"
|
||||||
echo " Admin-Console : ${KC_URL}/admin/ (${KC_ADMIN} / ***)"
|
echo " Admin-Console : ${KC_URL}/admin/"
|
||||||
echo " Test-Fahrer : testfahrer / test (Personalnummer 1001, Rolle driver)"
|
if [ "${NO_TEST_USER}" -eq 0 ]; then
|
||||||
|
echo " Test-Fahrer : testfahrer / test (Personalnummer 1001, Rolle driver)"
|
||||||
|
fi
|
||||||
echo
|
echo
|
||||||
echo " Hinweis: issuer_url muss EXAKT dem 'iss'-Claim entsprechen, das Keycloak"
|
echo " WICHTIG: issuer_url muss EXAKT dem 'iss'-Claim entsprechen, das Keycloak"
|
||||||
echo " ausstellt (abhängig von KC_HOSTNAME). Bei LAN-/USB-Wechsel siehe"
|
echo " ausstellt. In Prod dazu Keycloaks Frontend-/Hostname-URL passend setzen"
|
||||||
echo " tool/dev_usb.sh."
|
echo " (KC_HOSTNAME bzw. --hostname), sonst lehnt das Backend Tokens mit"
|
||||||
|
echo " 'invalid issuer' ab."
|
||||||
|
|||||||
Reference in New Issue
Block a user