Files
sicabe-core/deploy/scripts/test-authz.sh
T
2026-10-13 14:44:55 +07:00

189 lines
9.2 KiB
Bash
Executable File

#!/usr/bin/env bash
# =============================================================================
# test-authz.sh — uji model, CLI, dan kebijakan OPA (authorization)
# -----------------------------------------------------------------------------
# Yang dibuktikan (bukan sekadar "berkasnya ada"):
# 1. data.json SINKRON dengan authz.yml (hasil render), & render round-trip.
# 2. Policy Rego TIDAK lagi meng-hardcode nama catalog/schema (data-driven).
# 3. Mutasi CLI (role/grant/user) pada salinan model bekerja & bolak-balik bersih.
# 4. `--dry-run` benar-benar tidak menulis.
# 5. Kebijakan OPA (dievaluasi dengan OPA asli via docker) mempertahankan
# perilaku lama DAN mengganti-ganti catalog lewat data (uji rename).
# Bagian ini DILEWATI bila docker tak tersedia.
#
# Jalankan: ./scripts/test-authz.sh
# =============================================================================
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DEPLOY_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
CLI="${SCRIPT_DIR}/sicabe-authz"
MODEL="${DEPLOY_DIR}/config/opa/authz.yml"
DATA="${DEPLOY_DIR}/config/opa/data.json"
REGO="${DEPLOY_DIR}/config/opa/policies/trino.rego"
OUT="$(mktemp -d)"
trap 'rm -rf "${OUT}"' EXIT
GRN=$'\e[32m'; RED=$'\e[31m'; BLD=$'\e[1m'; NC=$'\e[0m'
PASS_N=0; FAIL_N=0; SKIP_N=0
ok() { printf " ${GRN}✅${NC} %s\n" "$1"; PASS_N=$((PASS_N + 1)); }
bad() { printf " ${RED}❌${NC} %s\n" "$1"; FAIL_N=$((FAIL_N + 1)); }
skip() { printf " ${BLD}…${NC} %s\n" "$1"; SKIP_N=$((SKIP_N + 1)); }
echo -e "${BLD}Uji authorization (model authz.yml + CLI + policy OPA)${NC}"
echo
# ─── A. Model & render ───────────────────────────────────────────────────────
echo -e "${BLD}A. Model & render${NC}"
[[ -f "${MODEL}" ]] && ok "authz.yml ada" || bad "authz.yml tidak ada"
if python3 "${DEPLOY_DIR}/lib/authz.py" --out "${OUT}/data.json" render >/dev/null 2>&1; then
ok "render authz.yml -> data.json berhasil"
else
bad "render gagal (model tidak valid?)"
fi
if "${CLI}" check >/dev/null 2>&1; then
ok "data.json SINKRON dengan authz.yml"
else
bad "data.json TIDAK sinkron (jalankan: sicabe-authz render)"
fi
if python3 - "${DATA}" "${OUT}/data.json" <<'PY'
import json, sys
a = json.load(open(sys.argv[1])); b = json.load(open(sys.argv[2]))
sys.exit(0 if a == b else 1)
PY
then ok "data.json identik dengan hasil render"; else bad "data.json berbeda dari hasil render"; fi
# ─── B. Policy data-driven ───────────────────────────────────────────────────
echo; echo -e "${BLD}B. Policy Rego data-driven${NC}"
if grep -q 'data.authz.catalogs' "${REGO}" && grep -q 'data.authz.write_schemas' "${REGO}"; then
ok "Rego membaca katalog/schema dari data.authz.*"
else
bad "Rego belum data-driven (data.authz.* tidak ditemukan)"
fi
if grep -qE '"iceberg"' "${REGO}"; then
bad "Rego masih meng-hardcode literal \"iceberg\""
else
ok "Rego bebas literal katalog \"iceberg\""
fi
# ─── C. Mutasi CLI pada salinan model ────────────────────────────────────────
echo; echo -e "${BLD}C. Mutasi CLI (salinan model)${NC}"
M="${OUT}/authz.yml"; cp "${MODEL}" "${M}"
cli() { "${CLI}" --model "${M}" "$@"; }
roles_out="$("${CLI}" list roles)"
case "${roles_out}" in *data-engineer*) ok "list roles memuat data-engineer" ;; *) bad "list roles keliru" ;; esac
"${CLI}" matrix --schema silver >/dev/null 2>&1 && ok "matrix --schema berjalan" || bad "matrix gagal"
"${CLI}" --model "${M}" role add data-scientist >/dev/null 2>&1
"${CLI}" --model "${M}" grant add data-scientist gold.orders_gold --caps select --columns id,total_value >/dev/null 2>&1
out="$(cli show role data-scientist)"
case "${out}" in *"gold.orders_gold"*"caps=[select]"*) ok "grant add tersimpan (select, kolom)" ;; *) bad "grant add tidak tersimpan: ${out}" ;; esac
"${CLI}" --model "${M}" grant set data-scientist gold.orders_gold --caps select,write --columns "*" >/dev/null 2>&1
out="$(cli show role data-scientist)"
case "${out}" in *"caps=[select,write]"*) ok "grant set menimpa caps" ;; *) bad "grant set gagal: ${out}" ;; esac
"${CLI}" --model "${M}" grant remove data-scientist gold.orders_gold --caps write >/dev/null 2>&1
out="$(cli show role data-scientist)"
case "${out}" in *"caps=[select]"*) ok "grant remove --caps menyisakan select" ;; *) bad "grant remove gagal: ${out}" ;; esac
"${CLI}" --model "${M}" user assign svc_ml data-scientist >/dev/null 2>&1
out="$(cli show user svc_ml)"
case "${out}" in *"svc_ml: data-scientist"*) ok "user assign bekerja" ;; *) bad "user assign gagal: ${out}" ;; esac
"${CLI}" --model "${M}" user unassign svc_ml >/dev/null 2>&1
"${CLI}" --model "${M}" role remove data-scientist >/dev/null 2>&1
python3 -c "import yaml,sys; sys.exit(0 if 'data-scientist' not in yaml.safe_load(open('${M}'))['roles'] else 1)" \
&& ok "role remove membersihkan model" || bad "role remove gagal"
# error & dry-run
if "${CLI}" --model "${M}" grant add data-nope bronze.orders --caps select >/dev/null 2>&1; then
bad "grant ke role tak ada seharusnya GAGAL"
else
ok "grant ke role tak ada ditolak"
fi
"${CLI}" --dry-run --model "${M}" role add data-ghost >/dev/null 2>&1
grep -q 'data-ghost' "${M}" && bad "--dry-run MENULIS berkas" || ok "--dry-run tidak menulis"
# ─── D. Policy OPA (OPA asli via docker) ────────────────────────────────────
echo; echo -e "${BLD}D. Evaluasi policy OPA (docker)${NC}"
if docker info >/dev/null 2>&1; then
if python3 - "${DEPLOY_DIR}" <<'PY'
import json, os, shutil, subprocess, sys, tempfile
from pathlib import Path
dep = Path(sys.argv[1]); opa = dep / "config" / "opa"; IMG = "openpolicyagent/opa:1.20.2"
def build(cases=None, variant=None):
ctx = Path(tempfile.mkdtemp())
shutil.copytree(opa / "policies", ctx / "policies")
data = json.loads((opa / "data.json").read_text())
if variant:
data.update(variant)
(ctx / "data.json").write_text(json.dumps(data))
dockerfile = f"FROM {IMG}\nCOPY policies /cfg/policies\nCOPY data.json /cfg/data.json\n"
if cases:
(ctx / "cases").mkdir()
for i, c in enumerate(cases):
(ctx / "cases" / f"{i}.json").write_text(json.dumps(c[0]))
dockerfile += "COPY cases /cfg/cases\n"
(ctx / "Dockerfile").write_text(dockerfile)
tag = f"sicabe-authz-test:{os.path.basename(ctx)}"
subprocess.run(["docker", "build", "-t", tag, str(ctx)], check=True, capture_output=True)
return tag
def ev(tag, i=None):
cmd = ["docker", "run", "--rm", "--user", "0", tag, "eval",
"-d", "/cfg/policies", "-d", "/cfg/data.json", "--format", "raw"]
if i is not None:
cmd += ["-i", f"/cfg/cases/{i}.json"]
cmd.append("data.trino.allow")
return subprocess.run(cmd, capture_output=True, text=True).stdout.strip()
def tin(op, cat, sch, tbl, cols, user, groups):
return {"action": {"operation": op, "resource": {"table": {"catalogName": cat, "schemaName": sch, "tableName": tbl, "columns": cols}}},
"context": {"identity": {"user": user, "groups": groups}}}
base_cases = [
({}, "false"), # deny-by-default
(tin("DROPTABLE", "iceberg", "gold", "orders_gold", [], "admin", ["data-admin"]), "true"),
(tin("SELECTFROMCOLUMNS", "iceberg", "silver", "orders_silver", ["id", "total_value"], "u", ["data-analyst"]), "true"),
(tin("SELECTFROMCOLUMNS", "iceberg", "silver", "orders_silver", ["id", "order_date"], "u", ["data-analyst"]), "false"),
]
ren_cases = [
(tin("SELECTFROMCOLUMNS", "lakehouse", "gold", "orders_gold", ["id"], "u", ["data-analyst"]), "true"),
(tin("SELECTFROMCOLUMNS", "iceberg", "gold", "orders_gold", ["id"], "u", ["data-analyst"]), "false"),
]
fails = 0
tb = build(base_cases)
if subprocess.run(["docker", "run", "--rm", "--user", "0", tb, "check", "/cfg/policies"],
capture_output=True).returncode != 0:
print(" opa check GAGAL"); fails += 1
for i, (_, want) in enumerate(base_cases):
got = ev(tb, i)
print(f" keputusan[{i}] -> {got or '(undefined)'} (harap {want})")
if got != want:
fails += 1
tr = build(ren_cases, {"authz": {"catalogs": ["lakehouse"], "write_schemas": ["bronze", "silver", "gold"]}})
for i, (_, want) in enumerate(ren_cases):
got = ev(tr, i)
print(f" rename[{i}] -> {got or '(undefined)'} (harap {want})")
if got != want:
fails += 1
sys.exit(1 if fails else 0)
PY
then ok "policy OPA: perilaku dasar + ganti catalog lewat data (via docker)"
else bad "policy OPA: ada keputusan yang tidak sesuai"
fi
else
skip "docker tak tersedia — lewati evaluasi policy OPA"
fi
# ─── Ringkasan ───────────────────────────────────────────────────────────────
echo
echo " skor: ${PASS_N} lulus · ${FAIL_N} gagal · ${SKIP_N} dilewati"
if [[ ${FAIL_N} -eq 0 ]]; then
echo -e " ${GRN}${BLD}SEMUA LULUS${NC} (${PASS_N} cek)"
exit 0
fi
echo -e " ${RED}${BLD}GAGAL${NC}: ${FAIL_N} masalah · ${PASS_N} lulus"
exit 1