189 lines
9.2 KiB
Bash
Executable File
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
|