479 lines
20 KiB
Bash
Executable File
479 lines
20 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# =============================================================================
|
|
# eBdesk SiCABE — Verifikasi Phase 1 (IAM): Keycloak + OpenLDAP
|
|
#
|
|
# Gate "kontrak IAM": memastikan konfigurasi Keycloak & LDAP benar-benar SIAP
|
|
# dipakai fase berikutnya (Trino+OPA, StarRocks, Airflow, OpenMetadata, Grafana)
|
|
# — termasuk saat memakai Keycloak/LDAP EXISTING (bukan container deploy ini).
|
|
#
|
|
# Yang diperiksa:
|
|
# A. Keycloak statis : realm, roles, 5 OIDC client, audience mapper, redirect URI
|
|
# B. Keycloak fungsional : login tiap user → preferred_username & roles
|
|
# C. LDAP : ou=users/ou=groups + pemetaan user→grup
|
|
# (filter PERSIS seperti config/trino/group-provider.properties)
|
|
# D. Cross-check : user Keycloak == uid LDAP ;
|
|
# grup LDAP == roles di config/opa/data.json == permitted_groups StarRocks
|
|
# E. Client secret : nilai tersimpan di Keycloak == nilai env deploy
|
|
#
|
|
# Exit code: 0 = semua gate lolos (WARN boleh ada), 1 = ada FAIL.
|
|
#
|
|
# Usage:
|
|
# ./scripts/verify-phase1-iam.sh
|
|
# # menunjuk IAM existing:
|
|
# KEYCLOAK_URL=http://kc.corp:8080 LDAP_HOST=ldap.corp LDAP_PORT=389 \
|
|
# ./scripts/verify-phase1-iam.sh
|
|
# =============================================================================
|
|
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}" "$@"
|
|
|
|
# Config RELEVAN: mode host (--env-file) = BUNDLE host (ter-render), bukan deploy/config/ sumber.
|
|
if [[ -n "${SICABE_BUNDLE_DIR:-}" ]]; then CONFIG_DIR="${SICABE_BUNDLE_DIR}/config"; else CONFIG_DIR="${SCRIPT_DIR}/config"; fi
|
|
|
|
KC="$(sicabe_local_url http KEYCLOAK_HOST "${KEYCLOAK_PORT:-12080}" KEYCLOAK_URL)"
|
|
REALM="${SICABE_REALM:-sicabe}"
|
|
LDAP_HOST="${LDAP_HOST:-localhost}"
|
|
LDAP_PORT="${LDAP_PORT:-389}"
|
|
LDAP_ADMIN_DN="${LDAP_ADMIN_DN:-cn=admin,dc=sicabe,dc=local}"
|
|
LDAP_BASE_DN="${LDAP_BASE_DN:-dc=sicabe,dc=local}"
|
|
USERS_DN="${LDAP_USERS_DN:-ou=users,${LDAP_BASE_DN}}"
|
|
GROUPS_DN="${LDAP_GROUPS_DN:-ou=groups,${LDAP_BASE_DN}}"
|
|
|
|
REQUIRED_CLIENTS=("trino" "starrocks" "airflow" "openmetadata" "grafana")
|
|
REQUIRED_ROLES=("admin" "engineer" "analyst" "viewer")
|
|
REQUIRED_USERS=("admin" "engineer" "analyst" "viewer")
|
|
AUDIENCE_MAPPER="audience-account"
|
|
|
|
PASS_N=0
|
|
FAIL_N=0
|
|
WARN_N=0
|
|
pass() { echo -e " ${PASS} $1"; PASS_N=$((PASS_N + 1)); }
|
|
FAIL_MSGS=()
|
|
fail() { echo -e " ${FAIL} $1"; FAIL_N=$((FAIL_N + 1)); FAIL_MSGS+=("$1"); }
|
|
warn() { echo -e " ${WARN} $1"; WARN_N=$((WARN_N + 1)); }
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# Keycloak — helper
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
kc_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
|
|
}
|
|
|
|
kc_user_login_probe() {
|
|
# → "preferred_username|roles". Membaca **id_token** (scope=openid): sejak
|
|
# Keycloak 26 access token bersifat "lightweight" sehingga tidak lagi memuat
|
|
# preferred_username / realm_access.
|
|
local user="$1" pw="$2"
|
|
curl -s -X POST "${KC}/realms/${REALM}/protocol/openid-connect/token" \
|
|
-d client_id=admin-cli -d "username=${user}" -d "password=${pw}" \
|
|
-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
|
|
tok=r.get('id_token') or r.get('access_token') or ''
|
|
if not tok:
|
|
print(''); raise SystemExit
|
|
p=tok.split('.')[1]; p+='='*(-len(p)%4)
|
|
try:
|
|
c=json.loads(base64.urlsafe_b64decode(p))
|
|
except Exception:
|
|
print(''); raise SystemExit
|
|
roles=c.get('realm_access',{}).get('roles') or []
|
|
print(c.get('preferred_username','') + '|' + ','.join(roles))
|
|
" 2>/dev/null || echo ""
|
|
}
|
|
|
|
# Deteksi "lightweight access token" (Keycloak 26+): access token tanpa
|
|
# preferred_username. Tidak fatal, tetapi perlu diketahui karena komponen yang
|
|
# membaca klaim dari ACCESS token (bukan id_token) harus menyesuaikan.
|
|
kc_access_token_is_lightweight() {
|
|
local user="$1" pw="$2"
|
|
curl -s -X POST "${KC}/realms/${REALM}/protocol/openid-connect/token" \
|
|
-d client_id=admin-cli -d "username=${user}" -d "password=${pw}" \
|
|
-d grant_type=password 2>/dev/null |
|
|
python3 -c "
|
|
import sys,json,base64
|
|
try:
|
|
t=json.load(sys.stdin).get('access_token','')
|
|
p=t.split('.')[1]; p+='='*(-len(p)%4)
|
|
c=json.loads(base64.urlsafe_b64decode(p))
|
|
except Exception:
|
|
print('unknown'); raise SystemExit
|
|
print('lightweight' if 'preferred_username' not in c else 'full')
|
|
" 2>/dev/null || echo "unknown"
|
|
}
|
|
|
|
check_keycloak_static() {
|
|
echo -e "${BOLD}A. Keycloak — statis (realm, roles, client, mapper)${NC}"
|
|
|
|
local discovery
|
|
discovery=$(curl -sf "${KC}/realms/${REALM}/.well-known/openid-configuration" 2>/dev/null || echo "")
|
|
if [[ -n "${discovery}" ]]; then
|
|
pass "OIDC discovery realm '${REALM}' OK ($(echo "${discovery}" | python3 -c "import sys,json;print(json.load(sys.stdin).get('issuer',''))" 2>/dev/null))"
|
|
else
|
|
fail "OIDC discovery realm '${REALM}' tidak merespon — cek ${KC}/realms/${REALM}"
|
|
return 1
|
|
fi
|
|
|
|
if [[ "${HAVE_ADMIN:-0}" -eq 0 ]]; then
|
|
warn "Tanpa admin Keycloak — cek realm/roles/client/mapper DILEWATI (didelegasikan ke admin IdP)."
|
|
return 0
|
|
fi
|
|
|
|
local roles
|
|
roles=$(kc_get "/roles" | python3 -c "import sys,json;print(' '.join(r['name'] for r in json.load(sys.stdin)))" 2>/dev/null || echo "")
|
|
local r missing_roles=""
|
|
for r in "${REQUIRED_ROLES[@]}"; do
|
|
[[ " ${roles} " == *" ${r} "* ]] || missing_roles+=" ${r}"
|
|
done
|
|
if [[ -z "${missing_roles}" ]]; then
|
|
pass "Roles lengkap: ${REQUIRED_ROLES[*]}"
|
|
else
|
|
fail "Role belum ada:${missing_roles}"
|
|
fi
|
|
|
|
local c cid mappers
|
|
for c in "${REQUIRED_CLIENTS[@]}"; do
|
|
cid=$(kc_get "/clients?clientId=${c}" | python3 -c "import sys,json;d=json.load(sys.stdin);print(d[0]['id'] if d else '')" 2>/dev/null || echo "")
|
|
if [[ -z "${cid}" ]]; then
|
|
fail "Client OIDC '${c}' tidak ada"
|
|
continue
|
|
fi
|
|
mappers=$(kc_get "/clients/${cid}/protocol-mappers/models" | python3 -c "import sys,json;print(' '.join(m['name'] for m in json.load(sys.stdin)))" 2>/dev/null || echo "")
|
|
if [[ " ${mappers} " == *" ${AUDIENCE_MAPPER} "* ]]; then
|
|
pass "Client '${c}' ada + audience mapper '${AUDIENCE_MAPPER}'"
|
|
else
|
|
fail "Client '${c}' TANPA audience mapper '${AUDIENCE_MAPPER}' (Trino OIDC akan menolak token)"
|
|
fi
|
|
done
|
|
|
|
local sr_redirect
|
|
sr_redirect=$(kc_get "/clients?clientId=starrocks" | python3 -c "
|
|
import sys,json
|
|
d=json.load(sys.stdin)
|
|
print(' '.join(d[0].get('redirectUris',[])) if d else '')" 2>/dev/null || echo "")
|
|
if [[ " ${sr_redirect} " == *"starrocks-fe:8030"* ]]; then
|
|
pass "Client 'starrocks' punya redirect URI internal FE"
|
|
else
|
|
warn "Client 'starrocks' belum memuat redirect internal (http://starrocks-fe:8030/api/oauth2)"
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
check_client_secrets() {
|
|
echo -e "${BOLD}E. Client secret — Keycloak vs env deploy${NC}"
|
|
if [[ "${HAVE_ADMIN:-0}" -eq 0 ]]; then
|
|
warn "Tanpa admin Keycloak — cek secret via Admin API DILEWATI (diganti cek client_credentials §F)."
|
|
return 0
|
|
fi
|
|
local pairs=("trino:${TRINO_CLIENT_SECRET}" "starrocks:${STARROCKS_CLIENT_SECRET}"
|
|
"airflow:${AIRFLOW_CLIENT_SECRET}" "openmetadata:${OPENMETADATA_CLIENT_SECRET}"
|
|
"grafana:${GRAFANA_CLIENT_SECRET}")
|
|
local pair cid expected actual bad=0
|
|
for pair in "${pairs[@]}"; do
|
|
cid="${pair%%:*}"
|
|
expected="${pair#*:}"
|
|
actual=$(kc_get "/clients?clientId=${cid}" | python3 -c "
|
|
import sys,json
|
|
d=json.load(sys.stdin)
|
|
print(d[0].get('secret','') if d else '')" 2>/dev/null || echo "")
|
|
if [[ -z "${actual}" ]]; then
|
|
warn "Client '${cid}' tidak ditemukan — lewati cek secret"
|
|
elif [[ "${actual}" == "${expected}" ]]; then
|
|
pass "Secret '${cid}' sinkron dengan env deploy"
|
|
else
|
|
fail "Secret '${cid}' BEDA dari env deploy (komponen akan gagal login)"
|
|
bad=$((bad + 1))
|
|
fi
|
|
done
|
|
[[ ${bad} -eq 0 ]] || warn "Samakan secret di .env dengan nilai di Keycloak."
|
|
}
|
|
|
|
check_keycloak_users_login() {
|
|
echo -e "${BOLD}B. Keycloak — fungsional (login tiap user)${NC}"
|
|
local user pw claim preferred
|
|
for user in "${REQUIRED_USERS[@]}"; do
|
|
case "${user}" in
|
|
admin) pw="${KEYCLOAK_ADMIN_PASSWORD}" ;;
|
|
*) pw="${user}123" ;;
|
|
esac
|
|
claim=$(kc_user_login_probe "${user}" "${pw}")
|
|
preferred="${claim%%|*}"
|
|
if [[ "${preferred}" == "${user}" ]]; then
|
|
pass "Login '${user}' OK (preferred_username=${preferred})"
|
|
elif [[ -n "${preferred}" ]]; then
|
|
fail "Login '${user}' OK tapi preferred_username='${preferred}' — harus sama dengan uid LDAP"
|
|
elif [[ "${HAVE_ADMIN:-0}" -eq 0 ]]; then
|
|
warn "Login '${user}' tak bisa diverifikasi (password user dikelola admin IdP) — dilewati"
|
|
else
|
|
fail "Login '${user}' gagal (password realm di luar default → isi manual)"
|
|
fi
|
|
done
|
|
|
|
local lt
|
|
lt=$(kc_access_token_is_lightweight "engineer" "engineer123")
|
|
if [[ "${lt}" == "lightweight" ]]; then
|
|
warn "Access token 'lightweight' (Keycloak 26): tanpa preferred_username/realm_access di access token."
|
|
warn " Komponen yang membaca klaim dari access token harus memakai id_token / userinfo."
|
|
fi
|
|
}
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# LDAP — helper (container lokal atau LDAP existing)
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
ldap_is_local_container() {
|
|
docker ps --format "{{.Names}}" 2>/dev/null | grep -q "^${LDAP_CONTAINER_NAME:-sicabe-openldap}$"
|
|
}
|
|
|
|
ldap_endpoint() {
|
|
if ldap_is_local_container; then
|
|
echo "ldap://localhost"
|
|
else
|
|
echo "ldap://${LDAP_HOST}:${LDAP_PORT}"
|
|
fi
|
|
}
|
|
|
|
ldap_run() {
|
|
if ldap_is_local_container; then
|
|
docker exec -i "${LDAP_CONTAINER_NAME:-sicabe-openldap}" "$@"
|
|
else
|
|
"$@"
|
|
fi
|
|
}
|
|
|
|
ldap_search() {
|
|
ldap_run ldapsearch -LLL -x -H "$(ldap_endpoint)" \
|
|
-D "${LDAP_ADMIN_DN}" -w "${LDAP_ADMIN_PASSWORD}" "$@" 2>/dev/null
|
|
}
|
|
|
|
# Catatan: JANGAN menyalurkan keluaran ldap_search ke `grep -q`. Dengan
|
|
# `set -o pipefail`, `grep -q` yang berhenti lebih awal membuat `docker exec`
|
|
# kena SIGPIPE (exit 141) sehingga pipa dilaporkan gagal padahal cocok.
|
|
# Karena itu hasil ditampung dulu ke variabel.
|
|
ldap_entry_exists() {
|
|
local base="$1" filter="$2" attr="${3:-cn}" out
|
|
out=$(ldap_search -b "${base}" -s one "${filter}" "${attr}")
|
|
[[ -n "${out}" ]]
|
|
}
|
|
|
|
ldap_dn_exists() {
|
|
local out
|
|
out=$(ldap_search -b "$1" -s base "(objectClass=*)" "*")
|
|
[[ -n "${out}" ]]
|
|
}
|
|
|
|
check_ldap_structure() {
|
|
echo -e "${BOLD}C. LDAP — struktur & pemetaan user→grup${NC}"
|
|
if ! ldap_is_local_container && ! command -v ldapsearch >/dev/null 2>&1; then
|
|
fail "ldapsearch tidak ada di host (mode LDAP existing). Install ldap-utils."
|
|
return 1
|
|
fi
|
|
|
|
local result
|
|
result=$(ldap_search -b "${LDAP_BASE_DN}" -s one "(objectClass=*)" dn)
|
|
if [[ -n "${result}" ]]; then
|
|
pass "LDAP terjangkau di $(ldap_endpoint) — base '${LDAP_BASE_DN}'"
|
|
else
|
|
fail "LDAP tidak terjangkau / bind gagal di $(ldap_endpoint)"
|
|
return 1
|
|
fi
|
|
|
|
local u g dn
|
|
for u in "${REQUIRED_USERS[@]}"; do
|
|
dn="uid=${u},${USERS_DN}"
|
|
if ldap_dn_exists "${dn}"; then
|
|
pass "User LDAP '${u}' ada"
|
|
else
|
|
fail "User LDAP '${u}' tidak ada (${dn}) — Trino tidak akan menemukan grupnya"
|
|
fi
|
|
done
|
|
|
|
local user_groups
|
|
for u in "${REQUIRED_USERS[@]}"; do
|
|
user_groups=$(ldap_search -b "${GROUPS_DN}" "(member=uid=${u},${USERS_DN})" cn |
|
|
sed -n 's/^cn: //p' | tr '\n' ',' | sed 's/,$//')
|
|
if [[ -n "${user_groups}" ]]; then
|
|
pass " ${u} → ${user_groups}"
|
|
else
|
|
fail " ${u} tidak berada di grup mana pun (otorisasi akan selalu ditolak)"
|
|
fi
|
|
done
|
|
for g in data-admin data-engineer data-analyst data-viewer; do
|
|
ldap_entry_exists "${GROUPS_DN}" "(cn=${g})" cn ||
|
|
warn "Grup LDAP '${g}' tidak ada"
|
|
done
|
|
}
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# D. Cross-check antar-sistem
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
check_cross_reference() {
|
|
echo -e "${BOLD}D. Cross-check antar-sistem${NC}"
|
|
|
|
if [[ "${HAVE_ADMIN:-0}" -eq 1 ]]; then
|
|
local kc_users
|
|
kc_users=$(kc_get "/users?max=200" |
|
|
python3 -c "import sys,json;print(' '.join(sorted(u['username'] for u in json.load(sys.stdin))))" 2>/dev/null || echo "")
|
|
local u ldap_missing=""
|
|
for u in "${REQUIRED_USERS[@]}"; do
|
|
[[ " ${kc_users} " == *" ${u} "* ]] || ldap_missing+=" ${u}"
|
|
done
|
|
if [[ -z "${ldap_missing}" ]]; then
|
|
pass "User Keycloak ⊇ user wajib (${REQUIRED_USERS[*]})"
|
|
else
|
|
fail "User Keycloak tidak punya:${ldap_missing} — tidak akan bisa login OIDC"
|
|
fi
|
|
else
|
|
warn "Tanpa admin Keycloak — cek daftar user DILEWATI (verifikasi user lewat login §B)."
|
|
fi
|
|
|
|
local opa_roles
|
|
opa_roles=$(python3 -c "
|
|
import json
|
|
print(' '.join(sorted(json.load(open('${CONFIG_DIR}/opa/data.json'))['roles'])))" 2>/dev/null || echo "")
|
|
if [[ -n "${opa_roles}" ]]; then
|
|
pass "Roles di OPA (data.json): ${opa_roles}"
|
|
else
|
|
fail "Tidak bisa membaca roles dari config/opa/data.json"
|
|
fi
|
|
|
|
# Role dapat diberikan lewat dua jalur: (a) grup LDAP dengan cn sama, atau
|
|
# (b) pemetaan service account di data.users (untuk akun yang tidak ada di LDAP).
|
|
local svc_roles
|
|
svc_roles=$(python3 -c "
|
|
import json
|
|
print(' '.join(sorted(set(json.load(open('${CONFIG_DIR}/opa/data.json')).get('users', {}).values()))))" 2>/dev/null || echo "")
|
|
|
|
local g no_ldap="" svc_only=""
|
|
for g in ${opa_roles}; do
|
|
if ldap_entry_exists "${GROUPS_DN}" "(cn=${g})" cn; then
|
|
continue
|
|
fi
|
|
if [[ " ${svc_roles} " == *" ${g} "* ]]; then
|
|
svc_only+=" ${g}"
|
|
else
|
|
no_ldap+=" ${g}"
|
|
fi
|
|
done
|
|
if [[ -z "${no_ldap}" ]]; then
|
|
pass "Semua role OPA dapat diberikan (via grup LDAP atau pemetaan service account)"
|
|
[[ -n "${svc_only}" ]] && pass " role khusus service account:${svc_only}"
|
|
else
|
|
fail "Role OPA tanpa grup LDAP & tanpa pemetaan service:${no_ldap}"
|
|
fi
|
|
|
|
local sr_groups
|
|
sr_groups=$(grep -oE "'permitted_groups' = '[^']+'" "${SCRIPT_DIR}/bootstrap.sh" 2>/dev/null |
|
|
head -1 | sed "s/.*= '//; s/'//" | tr ',' ' ')
|
|
if [[ -n "${sr_groups}" ]]; then
|
|
local grp mismatch=""
|
|
for grp in ${sr_groups}; do
|
|
[[ " ${opa_roles} " == *" ${grp} "* ]] || mismatch+=" ${grp}"
|
|
done
|
|
if [[ -z "${mismatch}" ]]; then
|
|
pass "permitted_groups StarRocks ⊆ roles OPA"
|
|
else
|
|
warn "permitted_groups StarRocks memuat grup yang tidak ada di OPA:${mismatch}"
|
|
fi
|
|
else
|
|
warn "Tidak bisa membaca permitted_groups StarRocks dari bootstrap.sh"
|
|
fi
|
|
}
|
|
|
|
# F. client_credentials — TANPA admin: membuktikan klien ADA + secret SINKRON +
|
|
# audience mapper jalan (aud=account). Ini pengganti cek statis §A/§E saat IAM
|
|
# dikelola pihak lain (tidak ada akses admin). Butuh serviceAccountsEnabled=true.
|
|
check_client_credentials() {
|
|
echo -e "${BOLD}F. Keycloak — client_credentials (klien + secret + audience; tanpa admin)${NC}"
|
|
local pairs=("trino:${TRINO_CLIENT_SECRET}" "starrocks:${STARROCKS_CLIENT_SECRET}"
|
|
"airflow:${AIRFLOW_CLIENT_SECRET}" "openmetadata:${OPENMETADATA_CLIENT_SECRET}"
|
|
"grafana:${GRAFANA_CLIENT_SECRET}")
|
|
local pair cid secret tok aud
|
|
for pair in "${pairs[@]}"; do
|
|
cid="${pair%%:*}"; secret="${pair#*:}"
|
|
tok=$(curl -s -X POST "${KC}/realms/${REALM}/protocol/openid-connect/token" \
|
|
-d grant_type=client_credentials -d "client_id=${cid}" -d "client_secret=${secret}" 2>/dev/null |
|
|
python3 -c "import sys,json;print(json.load(sys.stdin).get('access_token',''))" 2>/dev/null || echo "")
|
|
if [[ -z "${tok}" ]]; then
|
|
fail "client_credentials '${cid}' GAGAL (klien tak ada / secret beda / service account OFF)"
|
|
continue
|
|
fi
|
|
aud=$(printf '%s' "${tok}" | python3 -c "
|
|
import sys,json,base64
|
|
t=sys.stdin.read().strip(); p=t.split('.')[1]; p+='='*(-len(p)%4)
|
|
d=json.loads(base64.urlsafe_b64decode(p)); a=d.get('aud') or []
|
|
print(','.join(a if isinstance(a,list) else [a]))" 2>/dev/null || echo "")
|
|
if [[ " ${aud} " == *" account "* ]]; then
|
|
pass "client_credentials '${cid}' OK + aud=account (mapper audience ada, secret sinkron)"
|
|
else
|
|
warn "client_credentials '${cid}' OK tapi aud tanpa 'account' (aud='${aud}') — cek audience mapper"
|
|
fi
|
|
done
|
|
}
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# Main
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
main() {
|
|
print_section "Verifikasi Phase 1 — Kontrak IAM (Keycloak + LDAP)"
|
|
echo -e " Keycloak : ${CYAN}${KC}${NC} (realm ${REALM})"
|
|
echo -e " LDAP : ${CYAN}$(ldap_endpoint)${NC}"
|
|
echo ""
|
|
|
|
KC_TOKEN="$(kc_token)"
|
|
HAVE_ADMIN=1
|
|
if [[ -z "${KC_TOKEN}" ]]; then
|
|
HAVE_ADMIN=0
|
|
echo -e "${WARN} Tanpa akses admin Keycloak (${KC}) — lanjut mode READ-ONLY."
|
|
echo -e " ${INFO} Cek statis realm/role/user/client di Keycloak DILEWATI (didelegasikan ke admin IdP)."
|
|
echo -e " ${INFO} Tetap diperiksa: OIDC discovery · client_credentials (klien ada + secret sinkron +"
|
|
echo -e " ${INFO} aud=account) · LDAP · cross-check lokal (OPA/StarRocks)."
|
|
fi
|
|
|
|
check_keycloak_static
|
|
check_keycloak_users_login
|
|
check_client_credentials
|
|
check_client_secrets
|
|
check_ldap_structure
|
|
check_cross_reference
|
|
|
|
echo ""
|
|
echo -e "${BOLD}Ringkasan:${NC} ${GREEN}${PASS_N} PASS${NC} · ${RED}${FAIL_N} FAIL${NC} · ${YELLOW}${WARN_N} WARN${NC}"
|
|
if [[ ${FAIL_N} -gt 0 ]]; then
|
|
echo ""
|
|
echo -e "${BOLD}Yang gagal (${FAIL_N}):${NC}"
|
|
local _i=1 _m
|
|
for _m in "${FAIL_MSGS[@]}"; do
|
|
echo -e " ${RED}${_i}.${NC} ${_m}"
|
|
_i=$((_i + 1))
|
|
done
|
|
echo -e "${INFO} Perbaiki item di atas, lalu jalankan ulang: ./scripts/${0##*/}"
|
|
fi
|
|
if [[ ${FAIL_N} -eq 0 ]]; then
|
|
echo -e "${OK} ${GREEN}Kontrak IAM Phase 1 terpenuhi — aman lanjut ke fase berikutnya.${NC}"
|
|
exit 0
|
|
fi
|
|
echo -e "${ERR} Ada ${FAIL_N} gate yang gagal — perbaiki sebelum melanjutkan."
|
|
exit 1
|
|
}
|
|
|
|
main "$@"
|