#!/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