sync dari sicabe-doc/deploy @ bc1a6e6
This commit is contained in:
Executable
+171
@@ -0,0 +1,171 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# eBdesk SiCABE — Cek konsistensi keystore TLS Trino + password.db ANTAR-HOST
|
||||
#
|
||||
# Dua hal yang WAJIB benar agar klien (Great Expectations / OpenMetadata / dbt)
|
||||
# bisa bicara ke Trino:
|
||||
# 1) keystore TLS (generated/host-<nama>/config/trino/keystore.p12) IDENTIK di
|
||||
# semua host → hasil SATU `--generate` + `--sync`.
|
||||
# 2) password.db punya service account (svc_dbt) & cocok dgn TRINO_SVC_DBT_PASSWORD
|
||||
# di bundle .env (kalau tidak → 401 "Access Denied: Invalid credentials").
|
||||
# Catatan: bcrypt memakai salt acak → berkas password.db TIDAK dibandingkan
|
||||
# byte-per-byte antar-host, melainkan DIVERIFIKASI thd password yang diharapkan.
|
||||
#
|
||||
# Pemakaian:
|
||||
# ./scripts/check-trino-keystore.sh # host ini saja
|
||||
# ./scripts/check-trino-keystore.sh --inventory cluster.p3.yml
|
||||
# ./scripts/check-trino-keystore.sh --expect <sha256-keystore> # CI (exit 1 bila beda)
|
||||
#
|
||||
# Opsi: --host <nama> (paksa host ini) · --user <u> · --dest <dir-remote>
|
||||
# default: user $USER, dest = cermin lokasi repo ini di bawah $HOME
|
||||
# (mis. /home/u/sicabe-core/deploy → ~/sicabe-core/deploy)
|
||||
#
|
||||
# Keluar: 0 = semua OK · 1 = ada BEDA/MISSING · 2 = error (berkas/inventory tak ada)
|
||||
# =============================================================================
|
||||
set -uo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "${SCRIPT_DIR}"
|
||||
# shellcheck disable=SC1091
|
||||
source "${SCRIPT_DIR}/lib/common.sh" 2>/dev/null || true
|
||||
source "${SCRIPT_DIR}/config/environment.defaults.sh" 2>/dev/null || true
|
||||
if [[ -f "${SCRIPT_DIR}/.env" ]]; then set -a; source "${SCRIPT_DIR}/.env"; set +a; fi
|
||||
|
||||
INV=""; SSH_USER="${USER:-root}"; EXPECT=""; FORCE_HOST=""
|
||||
# Tujuan remote default = cermin lokasi repo ini di bawah $HOME (bebas nama repo:
|
||||
# `sicabe-core` → ~/sicabe-core/deploy). Fallback bila lib/common.sh gagal dimuat.
|
||||
if declare -F sicabe_default_dest >/dev/null 2>&1; then
|
||||
DEST="$(sicabe_default_dest "${SCRIPT_DIR}")"
|
||||
else
|
||||
DEST="~/$(basename "$(dirname "${SCRIPT_DIR}")")/deploy"
|
||||
fi
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--inventory) INV="${2:-}"; shift 2 ;;
|
||||
--user) SSH_USER="${2:-}"; shift 2 ;;
|
||||
--dest) DEST="${2:-}"; shift 2 ;;
|
||||
--expect) EXPECT="${2:-}"; shift 2 ;;
|
||||
--host) FORCE_HOST="${2:-}"; shift 2 ;;
|
||||
-h|--help) sed -n '2,24p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
|
||||
*) echo "Argumen tidak dikenal: $1" >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
C_RED=$'\033[31m'; C_GRN=$'\033[32m'; C_YEL=$'\033[33m'; C_BLD=$'\033[1m'; C_RST=$'\033[0m'
|
||||
|
||||
# ── keystore ─────────────────────────────────────────────────────────────────
|
||||
ks_path() { echo "${SCRIPT_DIR}/generated/host-$1/config/trino/keystore.p12"; }
|
||||
ks_sha() { sha256sum "$1" 2>/dev/null | cut -d' ' -f1; }
|
||||
ks_pw() { sed -n 's/^http-server.https.keystore.key=//p' \
|
||||
"${SCRIPT_DIR}/generated/host-$1/config/trino/config-coordinator.properties" 2>/dev/null | head -1; }
|
||||
ks_fp() { openssl pkcs12 -in "$(ks_path "$1")" -nokeys -passin "pass:$(ks_pw "$1")" 2>/dev/null \
|
||||
| openssl x509 -noout -fingerprint -sha256 2>/dev/null | sed 's/^sha256 Fingerprint=//'; }
|
||||
|
||||
# ── password.db (service account) ────────────────────────────────────────────
|
||||
pw_path() { echo "${SCRIPT_DIR}/generated/host-$1/config/trino/password.db"; }
|
||||
pw_expect() { sed -n 's/^TRINO_SVC_DBT_PASSWORD=//p' \
|
||||
"${SCRIPT_DIR}/generated/host-$1/.env" 2>/dev/null | head -1; }
|
||||
# Status lokal: NOFILE / MISSING / PWBAD / OK
|
||||
pw_verify() { # <host>
|
||||
local f exp; f="$(pw_path "$1")"
|
||||
[[ -f "${f}" ]] || { echo NOFILE; return; }
|
||||
grep -q '^svc_dbt:' "${f}" 2>/dev/null || { echo MISSING; return; }
|
||||
exp="$(pw_expect "$1")"
|
||||
if command -v htpasswd >/dev/null 2>&1 && [[ -n "${exp}" ]]; then
|
||||
htpasswd -vb "${f}" svc_dbt "${exp}" >/dev/null 2>&1 && echo OK || echo PWBAD
|
||||
else
|
||||
echo OK # ada, tapi tak bisa diverifikasi (htpasswd/pw tak ada)
|
||||
fi
|
||||
}
|
||||
|
||||
# ── host ini ─────────────────────────────────────────────────────────────────
|
||||
detect_self() {
|
||||
local lip d addr; lip="$(hostname -I 2>/dev/null || true)"
|
||||
for d in "${SCRIPT_DIR}"/generated/host-*; do
|
||||
[[ -f "${d}/.env" ]] || continue
|
||||
addr="$(sed -n 's/^SICABE_HOST_ADDRESS=//p' "${d}/.env" | head -1)"
|
||||
[[ -n "${addr}" && " ${lip} " == *" ${addr} "* ]] && { basename "${d}" | sed 's/^host-//'; return 0; }
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
SELF="${FORCE_HOST}"
|
||||
[[ -z "${SELF}" ]] && SELF="$(detect_self || true)"
|
||||
if [[ -z "${SELF}" ]]; then
|
||||
mapfile -t _dirs < <(ls -d "${SCRIPT_DIR}"/generated/host-* 2>/dev/null)
|
||||
[[ ${#_dirs[@]} -eq 1 ]] && SELF="$(basename "${_dirs[0]}" | sed 's/^host-//')"
|
||||
fi
|
||||
if [[ -z "${SELF}" || ! -f "$(ks_path "${SELF}")" ]]; then
|
||||
echo "${C_RED}Keystore host ini tak ditemukan.${C_RST} Jalankan ./install.sh --generate dulu, atau pakai --host <nama>." >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
SELF_SHA="$(ks_sha "$(ks_path "${SELF}")")"
|
||||
EXPECT_PW="$(pw_expect "${SELF}")"
|
||||
echo "${C_BLD}Cek konsistensi keystore + password.db Trino${C_RST}"
|
||||
echo " host ini : ${SELF}"
|
||||
echo " keystore : sha256=${SELF_SHA}"
|
||||
echo " cert : $(ks_fp "${SELF}")"
|
||||
echo " pw svc_dbt: $(pw_verify "${SELF}") (OK/MISSING/PWBAD/NOFILE)"
|
||||
echo ""
|
||||
|
||||
if [[ -n "${EXPECT}" ]]; then
|
||||
if [[ "${SELF_SHA}" == "${EXPECT}" ]]; then
|
||||
echo "${C_GRN}✓ sha256 keystore cocok dengan --expect.${C_RST}"; exit 0
|
||||
fi
|
||||
echo "${C_RED}✗ sha256 keystore BERBEDA dari --expect (${EXPECT}).${C_RST}"; exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "${INV}" ]]; then
|
||||
echo " (tanpa --inventory — bandingkan di tiap host, atau pakai --inventory)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
[[ -f "${INV}" ]] || { echo "${C_RED}Inventory tak ada: ${INV}${C_RST}" >&2; exit 2; }
|
||||
PAIRS="$(python3 "${SCRIPT_DIR}/lib/cluster/inventory.py" hosts "${INV}" 2>/dev/null)" \
|
||||
|| { echo "${C_RED}Gagal membaca inventory: ${INV}${C_RST}" >&2; exit 2; }
|
||||
|
||||
# Verifikasi password.db di host LAIN via SSH (bcrypt salt acak → verifikasi, bukan hash).
|
||||
remote_pw_status() { # <name> <addr>
|
||||
ssh -o ConnectTimeout=8 -o BatchMode=yes "${SSH_USER}@${2}" \
|
||||
"f='${DEST}/generated/host-${1}/config/trino/password.db'
|
||||
[ -f \"\$f\" ] || { echo NOFILE; exit 0; }
|
||||
grep -q '^svc_dbt:' \"\$f\" || { echo MISSING; exit 0; }
|
||||
if command -v htpasswd >/dev/null 2>&1; then htpasswd -vb \"\$f\" svc_dbt '${EXPECT_PW}' >/dev/null 2>&1 && echo OK || echo PWBAD; else echo OK; fi" \
|
||||
2>/dev/null || echo SSHFAIL
|
||||
}
|
||||
pw_icon() { case "$1" in OK) printf '%s%s%s' "${C_GRN}" "$1" "${C_RST}" ;; *) printf '%s%s%s' "${C_RED}" "$1" "${C_RST}" ;; esac; }
|
||||
|
||||
mismatch=0; checked=0
|
||||
while read -r name addr; do
|
||||
[[ -z "${name}" ]] && continue
|
||||
if [[ "${name}" == "${SELF}" ]]; then
|
||||
_ksok=1; _pw="$(pw_verify "${SELF}")"
|
||||
else
|
||||
_rsha="$(ssh -o ConnectTimeout=8 -o BatchMode=yes "${SSH_USER}@${addr}" \
|
||||
"sha256sum ${DEST}/generated/host-${name}/config/trino/keystore.p12 2>/dev/null | cut -d' ' -f1" 2>/dev/null || true)"
|
||||
if [[ -z "${_rsha}" ]]; then _ksok=-1; elif [[ "${_rsha}" == "${SELF_SHA}" ]]; then _ksok=1; else _ksok=0; fi
|
||||
_pw="$(remote_pw_status "${name}" "${addr}")"
|
||||
fi
|
||||
_kslabel="(host ini)"; _kscol=""
|
||||
case "${_ksok}" in
|
||||
1) [[ "${name}" != "${SELF}" ]] && { _kslabel="keystore ✓"; _kscol="${C_GRN}"; } ;;
|
||||
0) _kslabel="keystore ✗ BEDA"; _kscol="${C_RED}"; mismatch=1 ;;
|
||||
-1) _kslabel="keystore ? ssh gagal"; _kscol="${C_YEL}"; mismatch=1 ;;
|
||||
esac
|
||||
[[ "${_pw}" == "OK" ]] || mismatch=1
|
||||
printf ' %-9s %-15s %s%-16s%s pw svc_dbt %s\n' \
|
||||
"${name}" "${addr}" "${_kscol}" "${_kslabel}" "${C_RST}" "$(pw_icon "${_pw}")"
|
||||
checked=$((checked + 1))
|
||||
done <<< "${PAIRS}"
|
||||
|
||||
echo ""
|
||||
if [[ ${mismatch} -eq 0 ]]; then
|
||||
echo "${C_GRN}✓ keystore identik + password.db OK di semua host (${checked} host).${C_RST}"
|
||||
exit 0
|
||||
fi
|
||||
echo "${C_RED}✗ Ada ketidakcocokan (keystore BEDA / password.db MISSING/PWBAD).${C_RST}"
|
||||
echo " Perbaiki (di SATU host — control):"
|
||||
echo " ./install.sh --generate --inventory ${INV} && ./install.sh --sync"
|
||||
echo " lalu di tiap host: ./install.sh --host <nama> (atau recreate Trino & Airflow)"
|
||||
exit 1
|
||||
@@ -285,7 +285,7 @@ precheck() {
|
||||
ok "semua port 12xxx yang dibutuhkan masih bebas"
|
||||
else
|
||||
warn "port sudah terpakai:${busy}"
|
||||
echo " Bila itu stack SiCABE yang lama: matikan dulu (RAM tidak cukup untuk dua stack)."
|
||||
echo " Bila itu stack SiCABE yang lama: matikan dulu — stack single-host & multi-host tidak untuk berjalan bersamaan di satu host."
|
||||
fi
|
||||
else
|
||||
inf "daftar port tidak terbaca dari environment.defaults.sh — lewati pemeriksaan port"
|
||||
|
||||
Executable
+84
@@ -0,0 +1,84 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# eBdesk SiCABE — Orkestrasi alur data E2E (satu perintah) — jalankan DI CONTROL.
|
||||
#
|
||||
# Menghilangkan langkah manual berulang pada cold start. Dua "chicken-egg" yang
|
||||
# ditangani otomatis:
|
||||
# (a) aksi Great Expectations menempel ke ENTITAS tabel di OM, tetapi
|
||||
# `gold.orders_gold` DIBUAT oleh DAG → DAG #1 gagal di GE (wajar).
|
||||
# (b) lineage dbt dibaca dari ARTEFAK (`manifest.json`+`catalog.json`+`run_results.json`
|
||||
# di /tmp/dbt/target) yang DIBUAT oleh `dbt docs generate` — DAG hanya `dbt run`
|
||||
# (tanpa catalog.json), dan artefak HILANG bila container Airflow di-recreate.
|
||||
#
|
||||
# Urutan: ingest metadata → DAG #1 (data) → dbt docs generate → ingest --all →
|
||||
# DAG #2 (GE) → verify-phase6.
|
||||
#
|
||||
# PRASYARAT (sekali, DI HOST MASING-MASING — di luar skrip ini):
|
||||
# (state) ./scripts/seed-source-postgres.sh --env-file generated/host-state/.env
|
||||
# (compute) ./scripts/seed-starrocks.sh --env-file generated/host-compute/.env
|
||||
# DQ yang bekerja = Jalur A (Great Expectations pada task validate_gold/cdc_validate di DAG).
|
||||
# seed-native-dq.sh = Jalur B (DQ native OM) opt-in & TERBLOKIR bug upstream §8.10 —
|
||||
# JANGAN dipakai di alur ini (menempel ke entitas gold yang baru ada setelah DAG+ingest).
|
||||
#
|
||||
# Pemakaian: ./scripts/run-e2e.sh [--env-file <path>] [--dag <nama>]
|
||||
# =============================================================================
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "${SCRIPT_DIR}"
|
||||
# shellcheck disable=SC1091
|
||||
source "${SCRIPT_DIR}/lib/common.sh" 2>/dev/null || true
|
||||
|
||||
ENV_FILE="${SCRIPT_DIR}/generated/host-control/.env"
|
||||
DAG="${SICABE_DAG:-sicabe_batch_daily}"
|
||||
WEB="${SICABE_AIRFLOW_WEB:-sicabe-airflow-webserver}"
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--env-file) ENV_FILE="${2:-}"; shift 2 ;;
|
||||
--dag) DAG="${2:-}"; shift 2 ;;
|
||||
-h|--help) sed -n '2,25p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
|
||||
*) echo "Argumen tidak dikenal: $1" >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
[[ -f "${ENV_FILE}" ]] || ENV_FILE="${SCRIPT_DIR}/.env"
|
||||
[[ -f "${ENV_FILE}" ]] || { echo "env file tak ditemukan: ${ENV_FILE}" >&2; exit 2; }
|
||||
|
||||
# Password service account dbt (untuk `dbt docs generate` di dalam container).
|
||||
DBT_PW="$(sed -n 's/^TRINO_SVC_DBT_PASSWORD=//p' "${ENV_FILE}" | head -1)"
|
||||
DBT_USER="${DBT_TRINO_USER:-svc_dbt}"
|
||||
|
||||
step() { echo; echo -e "\033[1m▶ $*\033[0m"; }
|
||||
|
||||
echo -e "\033[1mAlur data E2E (SiCABE)\033[0m"
|
||||
echo " env-file : ${ENV_FILE}"
|
||||
echo " DAG : ${DAG}"
|
||||
echo " Prasyarat: seed sudah dijalankan di tiap host (state/compute)."
|
||||
|
||||
step "1/6 ingest metadata (service + entitas Trino; TANPA dbt — artefak belum ada pra-DAG)"
|
||||
./scripts/ingest-openmetadata.sh --env-file "${ENV_FILE}" \
|
||||
|| echo " PERHATIAN: ingest metadata gagal (mis. tabel belum ada pra-DAG) — lanjut; di-ingest ulang di step 4."
|
||||
|
||||
step "2/6 DAG #1 — buat data (bronze→silver→gold). GE boleh gagal: entitas gold belum di OM."
|
||||
docker exec -e SICABE_DAG_RETRIES=0 "${WEB}" airflow dags test "${DAG}" \
|
||||
|| echo " PERHATIAN: DAG #1 gagal (biasanya di GE karena gold belum di OM) — lanjut; wajar pada cold start."
|
||||
|
||||
step "3/6 artefak dbt (manifest+catalog+run_results) untuk lineage"
|
||||
echo " (wajib: DAG hanya 'dbt run' → catalog.json absen; artefak hilang bila container di-recreate)"
|
||||
docker exec \
|
||||
-e DBT_TRINO_USER="${DBT_USER}" -e DBT_TRINO_PASSWORD="${DBT_PW}" \
|
||||
-e DBT_PROFILES_DIR=/opt/airflow/dbt -e DBT_TARGET_PATH=/tmp/dbt/target \
|
||||
-e DBT_LOG_PATH=/tmp/dbt/logs -w /opt/airflow/dbt/sicabe \
|
||||
"${WEB}" bash -lc 'dbt docs generate --profile trino --target prod' \
|
||||
|| echo " PERHATIAN: dbt docs generate gagal — lineage dbt mungkin terlewat."
|
||||
|
||||
step "4/6 ingest ulang — entitas gold + lineage dbt (artefak sudah ada)"
|
||||
./scripts/ingest-openmetadata.sh --env-file "${ENV_FILE}" --all
|
||||
|
||||
step "5/6 DAG #2 — GE kini menemukan entitas gold di OM (target: hijau)"
|
||||
docker exec -e SICABE_DAG_RETRIES=0 "${WEB}" airflow dags test "${DAG}"
|
||||
|
||||
step "6/6 verifikasi governance (target FAIL=0)"
|
||||
./scripts/verify-phase6-governance.sh --env-file "${ENV_FILE}"
|
||||
|
||||
echo
|
||||
echo -e "\033[1;32m✓ Alur E2E selesai.\033[0m"
|
||||
@@ -16,6 +16,10 @@
|
||||
#
|
||||
# Idempoten: test case & pipeline yang sudah ada dilewati.
|
||||
# Dijalankan dari host; pekerjaannya dieksekusi DI DALAM container Airflow.
|
||||
#
|
||||
# PRASYARAT: entitas tabel `trino_sicabe.iceberg.gold.orders_gold` SUDAH ada di OM
|
||||
# (setelah DAG + `ingest-openmetadata.sh --all`). Bila belum → worker gagal
|
||||
# `HTTP 404: table instance ... not found`.
|
||||
# =============================================================================
|
||||
set -euo pipefail
|
||||
|
||||
@@ -66,4 +70,8 @@ done
|
||||
docker exec \
|
||||
-e OPENMETADATA_ADMIN_PASSWORD="${OPENMETADATA_ADMIN_PASSWORD}" \
|
||||
-e TRINO_SVC_OM_PASSWORD="${TRINO_SVC_OM_PASSWORD}" \
|
||||
"${AIRFLOW_CONTAINER}" python "${WORKER}" "${WORKER_ARGS[@]+"${WORKER_ARGS[@]}"}"
|
||||
"${AIRFLOW_CONTAINER}" python "${WORKER}" "${WORKER_ARGS[@]+"${WORKER_ARGS[@]}"}" || {
|
||||
echo " (catatan) Bila error 404 'table instance ... not found' → tabel gold belum ada di OM." >&2
|
||||
echo " Jalankan DAG + ingest-openmetadata dulu, atau pakai Jalur A (GE di DAG)." >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
@@ -82,6 +82,54 @@ else
|
||||
bad "step_tiers() belum memetakan step:${missing}"
|
||||
fi
|
||||
|
||||
# step_container() juga harus memetakan step 1..9 (dipakai mode --host untuk melewati
|
||||
# step yang komponennya dikelola SISTEM LAIN).
|
||||
sc_patterns="$(awk '/^step_container\(\) \{/{f=1} f{print} f&&/^\}/{exit}' "${DEPLOY_DIR}/bootstrap.sh" \
|
||||
| grep -E '^[[:space:]]*[0-9]+\)' | sed 's/).*//' | tr -d ' ')"
|
||||
sc_missing=""
|
||||
for s in 1 2 3 4 5 6 7 8 9; do
|
||||
grep -qx "${s}" <<<"${sc_patterns}" || sc_missing+=" ${s}"
|
||||
done
|
||||
if [[ -z "${sc_missing}" ]]; then
|
||||
ok "step_container() memetakan step 1..9"
|
||||
else
|
||||
bad "step_container() belum memetakan step:${sc_missing}"
|
||||
fi
|
||||
|
||||
# Mode --host HARUS melewati step milik sistem lain (bukan mencoba bootstrap-nya),
|
||||
# kecuali step 2 (MinIO → mode verifikasi).
|
||||
if grep -q 'sicabe_host_expects_container "${_step_ctr}"' "${DEPLOY_DIR}/bootstrap.sh" \
|
||||
&& grep -q '"${step}" != "2"' "${DEPLOY_DIR}/bootstrap.sh"; then
|
||||
ok "bootstrap --host melewati step komponen eksternal (step 2 dikecualikan)"
|
||||
else
|
||||
bad "bootstrap --host tidak melewati step komponen eksternal"
|
||||
fi
|
||||
|
||||
# Bundle host harus di-set agar sicabe_host_expects_container membaca bundle yang benar.
|
||||
if grep -q 'export SICABE_BUNDLE_DIR=' "${DEPLOY_DIR}/bootstrap.sh"; then
|
||||
ok "SICABE_BUNDLE_DIR di-set pada mode --host"
|
||||
else
|
||||
bad "SICABE_BUNDLE_DIR tidak di-set (deteksi komponen per-host salah)"
|
||||
fi
|
||||
|
||||
# Step 2 (MinIO): buat bucket hanya bila MinIO milik host ini; storage sistem lain → verifikasi.
|
||||
if grep -q 'mc_mode="create"' "${DEPLOY_DIR}/bootstrap.sh" \
|
||||
&& grep -q 'mc_mode="verify"' "${DEPLOY_DIR}/bootstrap.sh"; then
|
||||
ok "bootstrap_minio: create (lokal) vs verify (sistem lain)"
|
||||
else
|
||||
bad "bootstrap_minio tidak membedakan create vs verify"
|
||||
fi
|
||||
|
||||
# Sinkron redirectUris Keycloak harus memuat ORIGIN HOST client (bukan hanya localhost),
|
||||
# kalau tidak login lewat alamat (mis. https://<ip>:12443) ditolak "invalid redirect_uri".
|
||||
if grep -q 'CLIENT_ORIGIN' "${DEPLOY_DIR}/bootstrap.sh" \
|
||||
&& grep -q '_client_origin' "${DEPLOY_DIR}/bootstrap.sh" \
|
||||
&& grep -q 'host + "/\*"' "${DEPLOY_DIR}/bootstrap.sh"; then
|
||||
ok "bootstrap: sinkron redirectUris memuat ORIGIN HOST client"
|
||||
else
|
||||
bad "bootstrap: sinkron redirectUris TIDAK memuat origin host (login via alamat ditolak)"
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "── B. penjagaan dinamis (butuh stack SiCABE berjalan) ──"
|
||||
if ! docker ps --format '{{.Names}}' 2>/dev/null | grep -q '^sicabe-'; then
|
||||
|
||||
@@ -109,6 +109,170 @@ for pair in "MINIO_IMAGE:quay.io/minio/minio" "MINIO_MC_IMAGE:quay.io/minio/mc"
|
||||
fi
|
||||
done
|
||||
|
||||
# ─── 0e. Komponen EKSTERNAL ber-`address` (skenario P3) ──────────────────────
|
||||
# `components.<c>.address` = komponen dikelola SISTEM LAIN → TIDAK dijalankan cluster
|
||||
# (tak muncul di services host mana pun) + endpoint-nya = alamat itu. Diuji via cluster.p3.yml.
|
||||
p3_problems="$(python3 - "${DEPLOY_DIR}" <<'PY'
|
||||
import sys
|
||||
sys.path.insert(0, sys.argv[1] + "/lib/cluster")
|
||||
import inventory
|
||||
d = inventory.apply_preset(inventory.load(sys.argv[1] + "/cluster.p3.yml"))
|
||||
pl = inventory.plan(d)
|
||||
svc = {s for h in pl.values() for s in h["services"]}
|
||||
p = []
|
||||
for c in ("keycloak", "minio", "nessie"):
|
||||
if c in svc:
|
||||
p.append(c + " masih dijalankan")
|
||||
if not inventory.external_address(d, c):
|
||||
p.append(c + ": address hilang")
|
||||
if "openldap" not in pl.get("control", {}).get("services", []):
|
||||
p.append("openldap tak di control")
|
||||
print("; ".join(p))
|
||||
PY
|
||||
)"
|
||||
if [[ -z "${p3_problems}" ]]; then
|
||||
ok "P3: components.<c>.address → eksternal tak dijalankan, openldap lokal di control"
|
||||
else
|
||||
bad "P3: perilaku components.<c>.address salah — ${p3_problems}"
|
||||
fi
|
||||
|
||||
# ─── 0f. Deteksi keystore Trino BEDA antar-host ──────────────────────────────
|
||||
# Keystore Trino harus identik di semua host (satu --generate + --sync). Skrip ini
|
||||
# mendeteksi divergensi (mis. --generate dijalankan di >1 host).
|
||||
cks="${DEPLOY_DIR}/scripts/check-trino-keystore.sh"
|
||||
if [[ -x "${cks}" ]] && bash -n "${cks}" 2>/dev/null \
|
||||
&& grep -q -- '--inventory' "${cks}" && grep -q -- '--expect' "${cks}" \
|
||||
&& grep -q 'keystore.p12' "${cks}" && grep -q 'password.db' "${cks}" \
|
||||
&& grep -q 'svc_dbt' "${cks}"; then
|
||||
ok "check-trino-keystore.sh: deteksi keystore + password.db antar-host (--inventory/--expect)"
|
||||
else
|
||||
bad "check-trino-keystore.sh hilang/tidak lengkap (divergensi keystore/password.db tak terdeteksi)"
|
||||
fi
|
||||
# Fungsional (bila bundle host sudah ada): cetak sha256 & hormati --expect.
|
||||
_first_bundle="$(ls -d "${DEPLOY_DIR}"/generated/host-* 2>/dev/null | head -1)"
|
||||
if [[ -n "${_first_bundle}" ]]; then
|
||||
_bh="$(basename "${_first_bundle}" | sed 's/^host-//')"
|
||||
_sha="$("${cks}" --host "${_bh}" 2>/dev/null | sed -n 's/^ keystore : sha256=//p')"
|
||||
if [[ "${_sha}" =~ ^[0-9a-f]{64}$ ]]; then
|
||||
ok "check-trino-keystore.sh: sha256 host '${_bh}' terbaca"
|
||||
"${cks}" --host "${_bh}" --expect "${_sha}" >/dev/null 2>&1 \
|
||||
&& ok "check-trino-keystore.sh: --expect cocok → exit 0" \
|
||||
|| bad "check-trino-keystore.sh: --expect cocok seharusnya exit 0"
|
||||
else
|
||||
bad "check-trino-keystore.sh: sha256 tidak terbaca untuk host '${_bh}'"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ─── 0g. .env auto-generate memuat SEMUA var WAJIB compose ───────────────────
|
||||
# Regresi nyata: STARROCKS_ROOT_PASSWORD (dipakai `${VAR:?}` di compose) TIDAK dibuat
|
||||
# install.sh → `docker compose --env-file .env` (tanpa shell env) gagal
|
||||
# "required variable ... is missing a value".
|
||||
_required="$(grep -oE '\$\{[A-Z_][A-Z0-9_]*:\?' "${DEPLOY_DIR}/docker-compose.yml" | sed 's/\${//;s/:?//' | sort -u)"
|
||||
_envvars="$(awk '/^[[:space:]]*cat > .*\/\.env" <<EOF/{f=1;next} f&&/^EOF$/{exit} f' "${DEPLOY_DIR}/install.sh" \
|
||||
| grep -oE '^[A-Z_][A-Z0-9_]*=' | tr -d '=' | sort -u)"
|
||||
_miss_env=""
|
||||
for _v in ${_required}; do grep -qx "${_v}" <<<"${_envvars}" || _miss_env+=" ${_v}"; done
|
||||
if [[ -z "${_miss_env}" ]]; then
|
||||
ok ".env auto-generate memuat semua var wajib compose (${_required//$'\n'/ })"
|
||||
else
|
||||
bad "var wajib compose TIDAK di-generate ke .env:${_miss_env}"
|
||||
fi
|
||||
|
||||
# ─── 0h. Orkestrasi alur data E2E (run-e2e.sh) ───────────────────────────────
|
||||
e2e="${DEPLOY_DIR}/scripts/run-e2e.sh"
|
||||
if [[ -x "${e2e}" ]] && bash -n "${e2e}" 2>/dev/null \
|
||||
&& grep -q 'ingest-openmetadata.sh' "${e2e}" \
|
||||
&& grep -q 'airflow dags test' "${e2e}" \
|
||||
&& grep -q 'verify-phase6-governance.sh' "${e2e}"; then
|
||||
ok "run-e2e.sh: orkestrasi alur data E2E (ingest→DAG→ingest→DAG→verify)"
|
||||
else
|
||||
bad "run-e2e.sh hilang/tidak lengkap"
|
||||
fi
|
||||
|
||||
# ─── 0i. Prometheus: target MinIO EKSTERNAL → alamat:port publik ─────────────
|
||||
# Prometheus tak mendukung substitusi env, dan render-config hanya memetakan komponen
|
||||
# milik host lain — komponen EKSTERNAL (components.minio.address) harus di-rewrite
|
||||
# generate.py, kalau tidak target tetap `minio:9000` → scrape gagal dari host prometheus.
|
||||
_tmp_prom="$(mktemp -d)"; _prom_ok=""
|
||||
if [[ -f "${DEPLOY_DIR}/.env" ]]; then
|
||||
cp "${DEPLOY_DIR}/.env" "${_tmp_prom}/e.env"
|
||||
printf '\nMINIO_INTERNAL_PORT=12900\n' >> "${_tmp_prom}/e.env"
|
||||
if python3 "${DEPLOY_DIR}/lib/cluster/generate.py" render \
|
||||
"${DEPLOY_DIR}/cluster.p3.yml" "${DEPLOY_DIR}/docker-compose.yml" \
|
||||
"${_tmp_prom}/e.env" "${_tmp_prom}/out" "${DEPLOY_DIR}/scripts/render-config.sh" \
|
||||
>/dev/null 2>&1; then
|
||||
_pyml="${_tmp_prom}/out/host-control/config/prometheus/prometheus.yml"
|
||||
if grep -q 'targets: \["100.93.54.108:12900"\]' "${_pyml}" 2>/dev/null \
|
||||
&& ! grep -q 'targets: \["minio:9000"\]' "${_pyml}" 2>/dev/null; then
|
||||
_prom_ok=1
|
||||
fi
|
||||
fi
|
||||
if [[ -n "${_prom_ok}" ]]; then
|
||||
ok "prometheus: target MinIO eksternal → 100.93.54.108:12900 (bukan minio:9000)"
|
||||
else
|
||||
bad "prometheus: target MinIO eksternal salah (masih minio:9000?)"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ─── 0j. Verifikasi Keycloak lengkap (verify-keycloak.sh) ────────────────────
|
||||
vk="${DEPLOY_DIR}/scripts/verify-keycloak.sh"
|
||||
if [[ -x "${vk}" ]] && bash -n "${vk}" 2>/dev/null \
|
||||
&& grep -q 'client_credentials' "${vk}" && grep -q 'redirectUris' "${vk}" \
|
||||
&& grep -q 'publicClient' "${vk}" && grep -q 'origin host' "${vk}" \
|
||||
&& grep -q 'kc_auth_probe' "${vk}" && grep -q 'password masih DEFAULT' "${vk}"; then
|
||||
ok "verify-keycloak.sh: realm/roles/client(redirectUri origin-host,secret,publicClient)/user/password-default + fungsional"
|
||||
else
|
||||
bad "verify-keycloak.sh hilang/tidak lengkap"
|
||||
fi
|
||||
|
||||
# ─── 0k. Trino OAuth2: token/jwks pakai KEYCLOAK_HOST (bukan literal `keycloak`) ──
|
||||
# Kls bug sama dgn target MinIO Prometheus: Trino server-ke-server ke Keycloak. Bila
|
||||
# Keycloak EKSTERNAL (P3), literal `keycloak:` tak resolve → penukaran token gagal →
|
||||
# callback `/oauth2/callback` balas "Authentication response could not be verified".
|
||||
_tc="${DEPLOY_DIR}/config/trino/config-coordinator.properties"
|
||||
if grep -q 'token-url=http://${ENV:KEYCLOAK_HOST}:${ENV:KEYCLOAK_INTERNAL_PORT}' "${_tc}" 2>/dev/null \
|
||||
&& grep -q 'jwks-url=http://${ENV:KEYCLOAK_HOST}:' "${_tc}" 2>/dev/null \
|
||||
&& grep -q 'oauth2.state-key=${ENV:TRINO_OAUTH_STATE_KEY}' "${_tc}" 2>/dev/null \
|
||||
&& grep -q 'KEYCLOAK_HOST: ${KEYCLOAK_HOST' "${DEPLOY_DIR}/docker-compose.yml" 2>/dev/null \
|
||||
&& grep -q 'TRINO_OAUTH_STATE_KEY: ${TRINO_OAUTH_STATE_KEY' "${DEPLOY_DIR}/docker-compose.yml" 2>/dev/null \
|
||||
&& ! grep -qE 'token-url=http://keycloak:' "${_tc}" 2>/dev/null; then
|
||||
ok "trino oauth2: token-url/jwks-url pakai KEYCLOAK_HOST + state-key tetap"
|
||||
else
|
||||
bad "trino oauth2: token-url/jwks-url literal 'keycloak:' atau state-key belum diset"
|
||||
fi
|
||||
|
||||
# ─── 0l. StarRocks OIDC: redirect_url = alamat browser (bukan localhost) ──────
|
||||
if ! grep -qE "redirect_url' = 'http://localhost" "${DEPLOY_DIR}/bootstrap.sh" 2>/dev/null \
|
||||
&& grep -q 'sr_redirect_url=' "${DEPLOY_DIR}/bootstrap.sh" 2>/dev/null \
|
||||
&& grep -q 'bootstrap_browser_addr' "${DEPLOY_DIR}/bootstrap.sh" 2>/dev/null; then
|
||||
ok "StarRocks OIDC: redirect_url pakai alamat browser (bukan localhost literal)"
|
||||
else
|
||||
bad "StarRocks OIDC: redirect_url masih localhost literal"
|
||||
fi
|
||||
|
||||
# ─── 0m. Tujuan `--sync` BEBAS nama repo (cermin $HOME, bukan ~/sicabe-doc) ───
|
||||
# Regresi: dest default dipatok `~/sicabe-doc/deploy` → repo distribusi bersih
|
||||
# (`sicabe-core`, isinya hanya deploy/) salah tujuan & membuat salinan kedua
|
||||
# `~/sicabe-doc/deploy`. Nilai harus DITURUNKAN dari lokasi script.
|
||||
dest_probe="$(HOME=/home/u bash -c '
|
||||
# shellcheck disable=SC1091
|
||||
source "'"${DEPLOY_DIR}"'/lib/common.sh" >/dev/null 2>&1 || exit 9
|
||||
for d in /home/u/sicabe-core/deploy /home/u/sicabe-doc/deploy /opt/apps/sicabe-core/deploy; do
|
||||
printf "%s|" "$(sicabe_default_dest "$d")"
|
||||
done' 2>/dev/null)"
|
||||
if [[ "${dest_probe}" == "~/sicabe-core/deploy|~/sicabe-doc/deploy|~/sicabe-core/deploy|" ]]; then
|
||||
ok "--sync: tujuan default mencerminkan nama repo (sicabe-core → ~/sicabe-core/deploy)"
|
||||
else
|
||||
bad "--sync: sicabe_default_dest salah → '${dest_probe}'"
|
||||
fi
|
||||
if ! grep -q 'dest="~/sicabe-doc/deploy"' "${DEPLOY_DIR}/install.sh" \
|
||||
&& ! grep -q 'DEST="~/sicabe-doc/deploy"' "${DEPLOY_DIR}/scripts/check-trino-keystore.sh" \
|
||||
&& grep -q 'sicabe_default_dest' "${DEPLOY_DIR}/install.sh"; then
|
||||
ok "--sync: tidak ada lagi path repo ter-hardcode (~/sicabe-doc/deploy)"
|
||||
else
|
||||
bad "--sync: masih ada path repo ter-hardcode (~/sicabe-doc/deploy)"
|
||||
fi
|
||||
|
||||
# ─── 1. generate ────────────────────────────────────────────────────────────
|
||||
if ! python3 "${DEPLOY_DIR}/lib/cluster/generate.py" render \
|
||||
"${INVENTORY}" "${DEPLOY_DIR}/docker-compose.yml" "${DEPLOY_DIR}/.env" \
|
||||
|
||||
Executable
+355
@@ -0,0 +1,355 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# eBdesk SiCABE — Verifikasi Keycloak (realm, client, user) — LEBIH LENGKAP dari
|
||||
# verify-phase1. Tujuan: menjamin konfigurasi Keycloak siap dipakai fase berikutnya
|
||||
# & alur uji E2E (OIDC Trino/StarRocks/Airflow/OM/Grafana; login user engineer/…).
|
||||
#
|
||||
# Yang diperiksa:
|
||||
# A. Realm & roles : discovery/issuer; role admin/engineer/analyst/viewer.
|
||||
# B. Clients (statis) : per client — ada, enabled, publicClient=false,
|
||||
# standardFlow, serviceAccounts, client-secret,
|
||||
# redirectUris/webOrigins (localhost + PUBLIC_URL), audience mapper.
|
||||
# C. Users (statis) : per user — ada, enabled, realm role benar, dan
|
||||
# apakah password masih DEFAULT (hardcoded: engineer123/…).
|
||||
# D. Fungsional : client_credentials tiap service client → token OK (uji SECRET).
|
||||
# E. Fungsional : login user (password grant) → preferred_username + roles.
|
||||
# F. Redirect URI : validasi NYATA via /auth — Keycloak MENERIMA redirect alamat
|
||||
# host (dan MENOLAK yang ngawur). Bukan sekadar banding daftar.
|
||||
#
|
||||
# Mode READ-ONLY (tanpa admin Keycloak): bagian statis (B/C) DILEWATI; D/E tetap jalan.
|
||||
# Exit: 0 = semua gate lolos (WARN boleh), 1 = ada FAIL.
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/verify-keycloak.sh
|
||||
# ./scripts/verify-keycloak.sh --env-file generated/host-control/.env
|
||||
# KEYCLOAK_URL=http://kc.corp:8080 ./scripts/verify-keycloak.sh # IdP existing
|
||||
# =============================================================================
|
||||
set -uo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
# shellcheck source=../lib/common.sh
|
||||
source "${SCRIPT_DIR}/lib/common.sh"
|
||||
# shellcheck source=../config/environment.defaults.sh
|
||||
source "${SCRIPT_DIR}/config/environment.defaults.sh"
|
||||
sicabe_load_env "${SCRIPT_DIR}" "$@" || exit 1
|
||||
|
||||
KC="$(sicabe_local_url http KEYCLOAK_HOST "${KEYCLOAK_PORT:-12080}" KEYCLOAK_URL)"
|
||||
REALM="${SICABE_REALM:-sicabe}"
|
||||
|
||||
REQUIRED_CLIENTS=("trino" "starrocks" "airflow" "openmetadata" "grafana")
|
||||
REQUIRED_ROLES=("admin" "engineer" "analyst" "viewer")
|
||||
REQUIRED_USERS=("admin" "engineer" "analyst" "viewer")
|
||||
AUDIENCE_MAPPER="audience-account"
|
||||
|
||||
# Peran yang Diharapkan per user.
|
||||
declare -A USER_ROLE=([admin]=admin [engineer]=engineer [analyst]=analyst [viewer]=viewer)
|
||||
# Password DEFAULT (hardcoded oleh bootstrap.sh) — untuk mendeteksi password lemah.
|
||||
declare -A USER_DEFAULT_PW=(
|
||||
[admin]="${KEYCLOAK_ADMIN_PASSWORD:-}"
|
||||
[engineer]="engineer123"
|
||||
[analyst]="analyst123"
|
||||
[viewer]="viewer123"
|
||||
)
|
||||
# Secret client diharapkan (harus sama dgn env deploy) + URL publik (redirect/webOrigins).
|
||||
declare -A CLIENT_SECRET=(
|
||||
[trino]="${TRINO_CLIENT_SECRET:-}" [starrocks]="${STARROCKS_CLIENT_SECRET:-}"
|
||||
[airflow]="${AIRFLOW_CLIENT_SECRET:-}" [openmetadata]="${OPENMETADATA_CLIENT_SECRET:-}"
|
||||
[grafana]="${GRAFANA_CLIENT_SECRET:-}"
|
||||
)
|
||||
declare -A CLIENT_PUBLIC=(
|
||||
[trino]="${TRINO_PUBLIC_URL:-}" [starrocks]="${STARROCKS_PUBLIC_URL:-}"
|
||||
[airflow]="${AIRFLOW_PUBLIC_URL:-}" [openmetadata]="${OPENMETADATA_PUBLIC_URL:-}"
|
||||
[grafana]="${GRAFANA_PUBLIC_URL:-}"
|
||||
)
|
||||
# Origin (skema://alamat:port) tempat BROWSER membuka komponen — WAJIB ada di
|
||||
# redirectUris. Deteksi kesalahan umum: redirectUris hanya `https://localhost:12443/*`
|
||||
# padahal user membuka `https://<ip-host>:12443` → Keycloak menolak "invalid redirect_uri".
|
||||
# <client> = "<scheme> <host_var> <port_var> <default_port>"
|
||||
declare -A CLIENT_ORIGIN=(
|
||||
[trino]="https TRINO_HOST TRINO_HTTPS_PORT 12443"
|
||||
[grafana]="http GRAFANA_HOST GRAFANA_PORT 12300"
|
||||
[openmetadata]="http OPENMETADATA_HOST OPENMETADATA_PORT 12085"
|
||||
[airflow]="http AIRFLOW_HOST AIRFLOW_PORT 12082"
|
||||
[starrocks]="http STARROCKS_FE_HOST STARROCKS_HTTP_PORT 12030"
|
||||
)
|
||||
# Alamat efektif: nilai var bila berupa ALAMAT (mengandung titik); kalau kosong/co-located
|
||||
# → alamat host ini (SICABE_HOST_ADDRESS, multi-host). Kalau KEDUANYA tak ada → KOSONG
|
||||
# (JANGAN fallback ke localhost: itu membuat cek origin terhadap localhost selalu PASS).
|
||||
comp_addr() {
|
||||
local v="${!1:-}"
|
||||
if [[ "${v}" == *.* && "${v}" != "127.0.0.1" ]]; then echo "${v}"; return; fi
|
||||
echo "${SICABE_HOST_ADDRESS:-}"
|
||||
}
|
||||
expected_origin() { # <client> → "skema://alamat:port" (atau kosong bila alamat tak diketahui)
|
||||
local spec="${CLIENT_ORIGIN[$1]:-}"; [[ -n "${spec}" ]] || { echo ""; return; }
|
||||
local scheme hv pv dport addr; read -r scheme hv pv dport <<< "${spec}"
|
||||
addr="$(comp_addr "${hv}")"; [[ -n "${addr}" ]] || { echo ""; return; }
|
||||
printf '%s://%s:%s' "${scheme}" "${addr}" "${!pv:-${dport}}"
|
||||
}
|
||||
|
||||
PASS=0; FAIL_N=0; WARN_N=0
|
||||
pass() { echo -e " ${PASS} $1"; PASS=$((PASS + 1)); }
|
||||
fail() { echo -e " ${FAIL} $1"; FAIL_N=$((FAIL_N + 1)); }
|
||||
warn() { echo -e " ${WARN} $1"; WARN_N=$((WARN_N + 1)); }
|
||||
info() { echo -e " ${INFO} $1"; }
|
||||
|
||||
# ── Keycloak helper ──────────────────────────────────────────────────────────
|
||||
kc_admin_token() {
|
||||
curl -sf -X POST "${KC}/realms/master/protocol/openid-connect/token" \
|
||||
-d client_id=admin-cli -d "username=${KEYCLOAK_ADMIN:-admin}" \
|
||||
-d "password=${KEYCLOAK_ADMIN_PASSWORD:-}" -d grant_type=password 2>/dev/null |
|
||||
python3 -c "import sys,json;print(json.load(sys.stdin).get('access_token',''))" 2>/dev/null || echo ""
|
||||
}
|
||||
kc_get() { curl -sf -H "Authorization: Bearer ${KC_TOKEN}" "${KC}/admin/realms/${REALM}$1" 2>/dev/null; }
|
||||
|
||||
# client_credentials → cetak "OK" atau "HTTP<code>" atau "ERR".
|
||||
cc_grant() { # <client_id> <secret> → "OK" / "HTTP<code>"
|
||||
local code
|
||||
code="$(curl -s -o /dev/null -w '%{http_code}' -X POST \
|
||||
"${KC}/realms/${REALM}/protocol/openid-connect/token" \
|
||||
-d "client_id=$1" -d "client_secret=$2" -d grant_type=client_credentials 2>/dev/null || true)"
|
||||
code="${code:-000}"
|
||||
[[ "${code}" == "200" ]] && echo OK || echo "HTTP${code}"
|
||||
}
|
||||
|
||||
# Uji NYATA redirect_uri via endpoint /auth (publik — tak perlu admin): apakah Keycloak
|
||||
# MENERIMA redirect (→ halaman/redirect login) atau MENOLAK ("Invalid parameter: redirect_uri").
|
||||
kc_auth_probe() { # <client_id> <redirect_uri> → OK | REJECTED | HTTP<code>
|
||||
local out code
|
||||
out="$(curl -s -w $'\n%{http_code}' -G "${KC}/realms/${REALM}/protocol/openid-connect/auth" \
|
||||
--data-urlencode "client_id=$1" --data-urlencode "redirect_uri=$2" \
|
||||
--data-urlencode "response_type=code" --data-urlencode "scope=openid" 2>/dev/null || true)"
|
||||
code="${out##*$'\n'}"
|
||||
if printf '%s' "${out}" | grep -qi 'Invalid parameter: redirect_uri' || [[ "${code}" == "400" ]]; then
|
||||
echo REJECTED; return
|
||||
fi
|
||||
case "${code}" in 200|302|303) echo OK ;; *) echo "HTTP${code:-000}" ;; esac
|
||||
}
|
||||
# password grant → cetak "preferred_username|roles" (dari id_token) atau "".
|
||||
user_login() { # <user> <pw>
|
||||
curl -s -X POST "${KC}/realms/${REALM}/protocol/openid-connect/token" \
|
||||
-d client_id=admin-cli -d "username=$1" -d "password=$2" \
|
||||
-d grant_type=password -d scope=openid 2>/dev/null |
|
||||
python3 -c "
|
||||
import sys,json,base64
|
||||
try: r=json.load(sys.stdin)
|
||||
except Exception: print(''); raise SystemExit
|
||||
t=r.get('id_token') or r.get('access_token') or ''
|
||||
if not t: print(''); raise SystemExit
|
||||
p=t.split('.')[1]; p+='='*(-len(p)%4)
|
||||
try: c=json.loads(base64.urlsafe_b64decode(p))
|
||||
except Exception: print(''); raise SystemExit
|
||||
print(c.get('preferred_username','') + '|' + ','.join(c.get('realm_access',{}).get('roles') or []))
|
||||
" 2>/dev/null || echo ""
|
||||
}
|
||||
|
||||
# ── A. Realm & roles ─────────────────────────────────────────────────────────
|
||||
check_realm_roles() {
|
||||
local disc issuer
|
||||
disc="$(curl -sf "${KC}/realms/${REALM}/.well-known/openid-configuration" 2>/dev/null || true)"
|
||||
if [[ -z "${disc}" ]]; then
|
||||
fail "OIDC discovery realm '${REALM}' tidak merespon (${KC})"; return
|
||||
fi
|
||||
issuer="$(echo "${disc}" | python3 -c "import sys,json;print(json.load(sys.stdin).get('issuer',''))" 2>/dev/null)"
|
||||
pass "OIDC discovery OK (issuer=${issuer})"
|
||||
if [[ -z "${KC_TOKEN}" ]]; then
|
||||
warn "Tanpa admin Keycloak — cek statis (roles/client/user) DILEWATI"; return
|
||||
fi
|
||||
local roles missing=""
|
||||
roles="$(kc_get "/roles" | python3 -c "import sys,json;print(' '.join(r['name'] for r in json.load(sys.stdin)))" 2>/dev/null || echo "")"
|
||||
for r in "${REQUIRED_ROLES[@]}"; do [[ " ${roles} " == *" ${r} "* ]] || missing+=" ${r}"; done
|
||||
[[ -z "${missing}" ]] && pass "Roles lengkap: ${REQUIRED_ROLES[*]}" || fail "Role belum ada:${missing}"
|
||||
}
|
||||
|
||||
# ── B. Clients (statis) ──────────────────────────────────────────────────────
|
||||
check_clients() {
|
||||
[[ -n "${KC_TOKEN}" ]] || return 0
|
||||
local cid js ok
|
||||
# Bila origin host TAK BISA dihitung (env tanpa alamat cluster) → beri tahu SEKALI;
|
||||
# cek origin per client di-skip (bukan PASS palsu). Sumber benar: --env-file bundle
|
||||
# atau SICABE_CLIENT_REDIRECT_ORIGINS.
|
||||
local _c _have=0
|
||||
[[ -n "${SICABE_CLIENT_REDIRECT_ORIGINS:-}" ]] && _have=1
|
||||
for _c in "${REQUIRED_CLIENTS[@]}"; do [[ -n "$(expected_origin "${_c}")" ]] && { _have=1; break; }; done
|
||||
[[ ${_have} -eq 0 ]] && warn "Origin host tak bisa dihitung (env tanpa alamat cluster) — jalankan dgn --env-file generated/host-<nama>/.env, atau set SICABE_CLIENT_REDIRECT_ORIGINS. Cek origin DILEWATI (bukan PASS)."
|
||||
for cid in "${REQUIRED_CLIENTS[@]}"; do
|
||||
js="$(kc_get "/clients?clientId=${cid}")"
|
||||
if [[ -z "${js}" || "${js}" == "{}" || "${js}" == "[]" ]]; then
|
||||
fail "Client '${cid}' TIDAK ADA"; continue
|
||||
fi
|
||||
# Ambil bidang penting + secret + redirect + mapper dalam satu pass.
|
||||
read -r ok pub std svc auth secret redirects mapper < <(echo "${js}" | python3 -c "
|
||||
import sys,json
|
||||
d=json.load(sys.stdin) or []
|
||||
if not d: print('0 - - - - - - -'); raise SystemExit
|
||||
c=d[0]
|
||||
print(
|
||||
'1' if c.get('enabled') else '0',
|
||||
c.get('publicClient'),
|
||||
'1' if c.get('standardFlowEnabled') else '0',
|
||||
'1' if c.get('serviceAccountsEnabled') else '0',
|
||||
c.get('clientAuthenticatorType',''),
|
||||
c.get('secret',''),
|
||||
','.join(c.get('redirectUris') or []),
|
||||
','.join(c.get('redirectUris') or []))
|
||||
" 2>/dev/null)
|
||||
[[ "${ok}" == "1" ]] && pass "Client '${cid}' enabled" || fail "Client '${cid}' DISABLED"
|
||||
[[ "${pub}" == "False" ]] && pass " ${cid}: publicClient=false (confidential)" \
|
||||
|| fail " ${cid}: publicClient=${pub} (harus False)"
|
||||
[[ "${std}" == "1" ]] && pass " ${cid}: standardFlowEnabled" || fail " ${cid}: standardFlow OFF"
|
||||
[[ "${svc}" == "1" ]] && pass " ${cid}: serviceAccountsEnabled" || warn " ${cid}: serviceAccounts OFF"
|
||||
[[ "${auth}" == "client-secret" ]] && pass " ${cid}: clientAuthenticatorType=client-secret" \
|
||||
|| fail " ${cid}: authenticator='${auth}' (harus client-secret)"
|
||||
# Secret sinkron dgn env.
|
||||
if [[ -n "${CLIENT_SECRET[$cid]}" ]]; then
|
||||
[[ "${secret}" == "${CLIENT_SECRET[$cid]}" ]] && pass " ${cid}: secret sinkron dgn env" \
|
||||
|| fail " ${cid}: secret BEDA dari env (komponen akan gagal login)"
|
||||
else
|
||||
warn " ${cid}: *_CLIENT_SECRET kosong di env — tak bisa membandingkan secret"
|
||||
fi
|
||||
# redirectUris: localhost + (bila ada) PUBLIC_URL.
|
||||
if [[ "${redirects}" == *"http://localhost:*"* || "${redirects}" == *"https://localhost:*"* ]]; then
|
||||
pass " ${cid}: redirectUris memuat localhost"
|
||||
else
|
||||
warn " ${cid}: redirectUris TANPA localhost (${redirects})"
|
||||
fi
|
||||
local puburl="${CLIENT_PUBLIC[$cid]}"
|
||||
if [[ -n "${puburl}" ]]; then
|
||||
if [[ "${redirects}" == *"${puburl}"* ]]; then
|
||||
pass " ${cid}: redirectUris memuat PUBLIC_URL (${puburl})"
|
||||
else
|
||||
fail " ${cid}: redirectUris TANPA PUBLIC_URL '${puburl}' (login browser via alamat publik akan ditolak)"
|
||||
fi
|
||||
fi
|
||||
# Origin host (tempat browser membuka komponen) — WAJIB ada, bukan hanya localhost.
|
||||
local exp; exp="$(expected_origin "${cid}")"
|
||||
if [[ -n "${exp}" ]]; then
|
||||
if [[ "${redirects}" == *"${exp}"* ]]; then
|
||||
pass " ${cid}: redirectUris memuat origin host (${exp})"
|
||||
else
|
||||
fail " ${cid}: redirectUris TANPA origin host '${exp}' — login browser via alamat itu DITOLAK (invalid redirect_uri)"
|
||||
fi
|
||||
fi
|
||||
# Origin client dari CLUSTER (SICABE_CLIENT_REDIRECT_ORIGINS) — host existing yang
|
||||
# menjalankan bootstrap tak tahu alamat host cluster, jadi daftar ini disuplai ke .env.
|
||||
local o
|
||||
for o in ${SICABE_CLIENT_REDIRECT_ORIGINS:-}; do
|
||||
[[ -n "${o}" ]] || continue
|
||||
if [[ "${redirects}" == *"${o}"* ]]; then
|
||||
pass " ${cid}: redirectUris memuat origin cluster (${o})"
|
||||
else
|
||||
fail " ${cid}: redirectUris TANPA origin cluster '${o}'"
|
||||
fi
|
||||
done
|
||||
done
|
||||
}
|
||||
|
||||
# ── C. Users (statis) + password default ─────────────────────────────────────
|
||||
check_users() {
|
||||
[[ -n "${KC_TOKEN}" ]] || return 0
|
||||
local u js
|
||||
for u in "${REQUIRED_USERS[@]}"; do
|
||||
js="$(kc_get "/users?username=${u}&exact=true")"
|
||||
if [[ -z "${js}" || "${js}" == "[]" ]]; then fail "User '${u}' TIDAK ADA"; continue; fi
|
||||
local uid enabled roles
|
||||
uid="$(echo "${js}" | python3 -c "import sys,json;d=json.load(sys.stdin);print(d[0]['id'] if d else '')" 2>/dev/null)"
|
||||
enabled="$(echo "${js}" | python3 -c "import sys,json;d=json.load(sys.stdin);print('1' if d and d[0].get('enabled') else '0')" 2>/dev/null)"
|
||||
roles="$(kc_get "/users/${uid}/role-mappings/realm" | python3 -c "import sys,json;print(' '.join(r['name'] for r in json.load(sys.stdin)))" 2>/dev/null || echo "")"
|
||||
[[ "${enabled}" == "1" ]] && pass "User '${u}' ada + enabled" || fail "User '${u}' DISABLED"
|
||||
[[ " ${roles} " == *" ${USER_ROLE[$u]} "* ]] && pass " ${u}: realm role '${USER_ROLE[$u]}'" \
|
||||
|| fail " ${u}: role '${USER_ROLE[$u]}' TIDAK ada (peran: ${roles:-—})"
|
||||
# Password masih DEFAULT (hardcoded)?
|
||||
local dp="${USER_DEFAULT_PW[$u]}"
|
||||
if [[ -n "${dp}" ]] && [[ "$(user_login "${u}" "${dp}")" == "${u}|"* ]]; then
|
||||
warn " ${u}: password masih DEFAULT (hardcoded) — ganti untuk produksi"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# ── D. client_credentials ────────────────────────────────────────────────────
|
||||
check_cc() {
|
||||
local cid res
|
||||
for cid in "${REQUIRED_CLIENTS[@]}"; do
|
||||
[[ -n "${CLIENT_SECRET[$cid]}" ]] || { warn "cc ${cid}: secret env kosong"; continue; }
|
||||
res="$(cc_grant "${cid}" "${CLIENT_SECRET[$cid]}")"
|
||||
[[ "${res}" == "OK" ]] && pass "client_credentials '${cid}' → token OK" \
|
||||
|| fail "client_credentials '${cid}' GAGAL (${res})"
|
||||
done
|
||||
}
|
||||
|
||||
# ── E2. Redirect URI — validasi NYATA via /auth (bukan sekadar bandingkan daftar) ──
|
||||
check_redirect_validation() {
|
||||
local cid o exp rs any=0
|
||||
for cid in "${REQUIRED_CLIENTS[@]}"; do
|
||||
exp="$(expected_origin "${cid}")"
|
||||
if [[ -n "${exp}" ]]; then
|
||||
any=1
|
||||
rs="$(kc_auth_probe "${cid}" "${exp}/")"
|
||||
case "${rs}" in
|
||||
OK) pass "${cid}: Keycloak MENERIMA redirect '${exp}/*' (uji nyata via /auth)";;
|
||||
REJECTED) fail "${cid}: Keycloak MENOLAK redirect '${exp}/*' — invalid redirect_uri (belum terdaftar di client)";;
|
||||
*) warn "${cid}: uji redirect '${exp}/*' tak konklusif (${rs})";;
|
||||
esac
|
||||
fi
|
||||
done
|
||||
# Origin dari daftar cluster (SICABE_CLIENT_REDIRECT_ORIGINS) — uji terhadap tiap client.
|
||||
for o in ${SICABE_CLIENT_REDIRECT_ORIGINS:-}; do
|
||||
[[ -n "${o}" ]] || continue
|
||||
any=1
|
||||
for cid in "${REQUIRED_CLIENTS[@]}"; do
|
||||
rs="$(kc_auth_probe "${cid}" "${o}/")"
|
||||
[[ "${rs}" == "OK" ]] && pass "${cid}: Keycloak MENERIMA origin cluster '${o}/*'" \
|
||||
|| fail "${cid}: Keycloak MENOLAK origin cluster '${o}/*' (${rs})"
|
||||
done
|
||||
done
|
||||
if [[ ${any} -eq 0 ]]; then
|
||||
warn "redirect_uri tak bisa diuji (env tanpa alamat cluster / SICABE_CLIENT_REDIRECT_ORIGINS)"
|
||||
return
|
||||
fi
|
||||
# Kontrol: redirect ngawur HARUS ditolak → membuktikan Keycloak memang memvalidasi.
|
||||
rs="$(kc_auth_probe "trino" "https://sicabe-invalid.example/blocked")"
|
||||
[[ "${rs}" == "REJECTED" ]] \
|
||||
&& pass "kontrol: redirect ngawur DITOLAK Keycloak (validasi redirect aktif)" \
|
||||
|| warn "kontrol: redirect ngawur '${rs}' — Keycloak mungkin TIDAK memvalidasi redirect_uri"
|
||||
}
|
||||
|
||||
# ── E. Login user (password grant) ───────────────────────────────────────────
|
||||
check_user_login() {
|
||||
local u out pref
|
||||
for u in "${REQUIRED_USERS[@]}"; do
|
||||
local pw="${USER_DEFAULT_PW[$u]}"
|
||||
[[ -n "${pw}" ]] || { warn "login '${u}': password tak diketahui — lewati"; continue; }
|
||||
out="$(user_login "${u}" "${pw}")"
|
||||
pref="${out%%|*}"
|
||||
if [[ "${pref}" == "${u}" ]]; then
|
||||
pass "login '${u}' OK (preferred_username=${pref}; roles=${out#*|})"
|
||||
elif [[ -z "${out}" ]]; then
|
||||
fail "login '${u}' GAGAL (user/password salah, atau user belum ada)"
|
||||
else
|
||||
fail "login '${u}' OK tapi preferred_username='${pref}' (harus = uid LDAP '${u}')"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# ── main ─────────────────────────────────────────────────────────────────────
|
||||
echo -e "${BOLD}Verifikasi Keycloak — realm '${REALM}' @ ${KC}${NC}"
|
||||
echo ""
|
||||
KC_TOKEN="$(kc_admin_token)"
|
||||
if [[ -z "${KC_TOKEN}" ]]; then
|
||||
warn "Mode READ-ONLY (login admin gagal / tanpa admin) — cek statis dilewati"
|
||||
else
|
||||
info "Token admin diperoleh — cek statis dijalankan"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
echo -e "${BOLD}A. Realm & roles${NC}"; check_realm_roles
|
||||
echo -e "${BOLD}B. Clients (statis)${NC}"; check_clients
|
||||
echo -e "${BOLD}C. Users (statis)${NC}"; check_users
|
||||
echo -e "${BOLD}D. Fungsional — client_credentials (uji SECRET client, bukan redirect)${NC}"; check_cc
|
||||
echo -e "${BOLD}E. Fungsional — login user${NC}"; check_user_login
|
||||
echo -e "${BOLD}F. Redirect URI — validasi NYATA via /auth${NC}"; check_redirect_validation
|
||||
echo ""
|
||||
echo -e "${BOLD}Ringkasan: ${PASS} PASS · ${FAIL_N} FAIL · ${WARN_N} WARN${NC}"
|
||||
[[ ${FAIL_N} -eq 0 ]] && { echo -e "${GREEN}✓ Keycloak siap.${NC}"; exit 0; }
|
||||
echo -e "${RED}✗ Ada ${FAIL_N} kegagalan — perbaiki sebelum melanjutkan.${NC}"
|
||||
exit 1
|
||||
@@ -97,7 +97,7 @@ try:
|
||||
except Exception:
|
||||
print(''); raise SystemExit
|
||||
roles=c.get('realm_access',{}).get('roles') or []
|
||||
print(c.get('preferred_username',''), '|', ','.join(roles))
|
||||
print(c.get('preferred_username','') + '|' + ','.join(roles))
|
||||
" 2>/dev/null || echo ""
|
||||
}
|
||||
|
||||
@@ -216,7 +216,7 @@ check_keycloak_users_login() {
|
||||
*) pw="${user}123" ;;
|
||||
esac
|
||||
claim=$(kc_user_login_probe "${user}" "${pw}")
|
||||
preferred="${claim%% |*}"
|
||||
preferred="${claim%%|*}"
|
||||
if [[ "${preferred}" == "${user}" ]]; then
|
||||
pass "Login '${user}' OK (preferred_username=${preferred})"
|
||||
elif [[ -n "${preferred}" ]]; then
|
||||
|
||||
@@ -268,10 +268,14 @@ check_security() {
|
||||
fail " ${OIDC_NAME}: tidak memuat ${pat} (cek bootstrap.sh --step 5)"
|
||||
fi
|
||||
done
|
||||
if [[ "${oidc}" == *"redirect_url\" = \"http://localhost:${STARROCKS_HTTP_PORT:-12030}/api/oauth2\""* ]]; then
|
||||
pass " ${OIDC_NAME}: redirect_url → http://localhost:${STARROCKS_HTTP_PORT:-12030}/api/oauth2"
|
||||
# redirect_url harus = alamat BROWSER (host FE) — bukan localhost/nama container.
|
||||
local _sraddr="${STARROCKS_FE_HOST:-}"
|
||||
[[ "${_sraddr}" == *.* && "${_sraddr}" != "127.0.0.1" ]] || _sraddr="${SICABE_HOST_ADDRESS:-localhost}"
|
||||
local _srexp="http://${_sraddr}:${STARROCKS_HTTP_PORT:-12030}/api/oauth2"
|
||||
if [[ "${oidc}" == *"redirect_url\" = \"${_srexp}\""* ]]; then
|
||||
pass " ${OIDC_NAME}: redirect_url → ${_srexp}"
|
||||
else
|
||||
warn " ${OIDC_NAME}: redirect_url tidak sesuai STARROCKS_HTTP_PORT (${STARROCKS_HTTP_PORT:-12030})"
|
||||
warn " ${OIDC_NAME}: redirect_url bukan '${_srexp}' (cek bootstrap.sh --step 5)"
|
||||
fi
|
||||
local g miss="" groups
|
||||
IFS=',' read -ra groups <<< "${EXPECTED_GROUPS}"
|
||||
|
||||
Reference in New Issue
Block a user