sync a58faaa refactor(authz)

This commit is contained in:
2026-10-13 18:28:09 +07:00
parent 5519ae2de7
commit 678885f681
6 changed files with 290 additions and 40 deletions
+14 -3
View File
@@ -6,9 +6,10 @@
# ./scripts/sicabe-authz render # authz.yml -> data.json # ./scripts/sicabe-authz render # authz.yml -> data.json
# ./scripts/sicabe-authz apply # render + reload OPA # ./scripts/sicabe-authz apply # render + reload OPA
# ./scripts/sicabe-authz fmt # rapikan format berkas ini # ./scripts/sicabe-authz fmt # rapikan format berkas ini
# Policy Rego membaca data.authz.catalogs / data.authz.write_schemas — nama # Policy Rego membaca data.authz.catalogs + grant schema (data.roles.<r>.schema_grants)
# catalog/schema TIDAK di-hardcode di Rego. Ganti nama catalog = ubah `catalogs:` # — nama catalog/schema & cakupan GRANT TIDAK di-hardcode di Rego. Ganti nama
# di sini -> render -> reload. Dokumentasi: docs/authorization-cli.md # catalog = ubah `catalogs:` di sini -> render -> reload. Grant tingkat-schema:
# `roles.<r>.schemas: [{schema, caps, columns}]`. Dokumentasi: docs/authorization-cli.md
# ============================================================================= # =============================================================================
version: 1 version: 1
catalogs: [iceberg] catalogs: [iceberg]
@@ -29,6 +30,16 @@ roles:
- table: gold.orders_gold - table: gold.orders_gold
caps: [select, write, ddl] caps: [select, write, ddl]
columns: '*' columns: '*'
schemas:
- schema: bronze
caps: [select, write, ddl]
columns: '*'
- schema: silver
caps: [select, write, ddl]
columns: '*'
- schema: gold
caps: [select, write, ddl]
columns: '*'
data-catalog: data-catalog:
grants: grants:
- table: bronze.orders - table: bronze.orders
+21 -1
View File
@@ -39,6 +39,26 @@
"ddl": true, "ddl": true,
"columns": "*" "columns": "*"
} }
},
"schema_grants": {
"bronze": {
"select": true,
"write": true,
"ddl": true,
"columns": "*"
},
"silver": {
"select": true,
"write": true,
"ddl": true,
"columns": "*"
},
"gold": {
"select": true,
"write": true,
"ddl": true,
"columns": "*"
}
} }
}, },
"data-catalog": { "data-catalog": {
@@ -90,6 +110,6 @@
}, },
"_generated": { "_generated": {
"from": "authz.yml", "from": "authz.yml",
"sha256": "8c7de053a5683e00c27f8d92157463c65ff6d45d5802e1e03b48bb9a2a933cb5" "sha256": "36620fa9962357759a498f40d22a3a729d8fa7a8fa69d9941dbd270806968aad"
} }
} }
+86 -20
View File
@@ -38,9 +38,7 @@ identity_user := input.context.identity.user
# SPESIFIK (data.authz.*) — JANGAN meneruskan `data` utuh ke builtin, sebab itu # SPESIFIK (data.authz.*) — JANGAN meneruskan `data` utuh ke builtin, sebab itu
# membuat compiler menganggapnya bergantung pada `allow` (rego_recursion_error). # membuat compiler menganggapnya bergantung pada `allow` (rego_recursion_error).
default data_catalogs := [] default data_catalogs := []
default write_schemas := []
data_catalogs := data.authz.catalogs data_catalogs := data.authz.catalogs
write_schemas := data.authz.write_schemas
# Catalog yang boleh di-listing = catalog terdaftar + 'system' (bawaan Trino). # Catalog yang boleh di-listing = catalog terdaftar + 'system' (bawaan Trino).
allowed_catalogs contains c if { some c in data_catalogs } allowed_catalogs contains c if { some c in data_catalogs }
@@ -157,6 +155,15 @@ base_table_name(name) := name if {
table_id(schema, table) := sprintf("%s_%s", [schema, base_table_name(table)]) table_id(schema, table) := sprintf("%s_%s", [schema, base_table_name(table)])
# ── Grant tingkat-SCHEMA: berlaku untuk SEMUA tabel di schema tersebut ───────
# Bentuk data (dari authz.yml `roles.<role>.schemas`):
# data.roles.<role>.schema_grants.<schema> = { select/write/ddl, columns }
# Digabung (union) dengan grant per-tabel. Tabel baru (termasuk sementara dbt/Sling
# milik engineer) otomatis ikut ter-cover bila grant-nya di level schema.
schema_grant(role, schema) := sg if {
sg := data.roles[role].schema_grants[schema]
}
allow if { allow if {
cap := op_caps[op] cap := op_caps[op]
res := input.action.resource res := input.action.resource
@@ -257,30 +264,89 @@ allow if {
any_cap(grant) any_cap(grant)
} }
# ── Grant tingkat-SCHEMA (union dgn grant per-tabel di atas) ─────────────────
# Operasi per-tabel (non-SELECT) yang dicakup cap.
allow if {
cap := op_caps[op]
res := input.action.resource
some r in effective_roles
sg := schema_grant(r, res.table.schemaName)
sg[cap] == true
}
# SELECT per-kolom pada tabel di schema yang di-grant.
allow if {
op == "SELECTFROMCOLUMNS"
res := input.action.resource
res.table.catalogName in data_catalogs
cols := res.table.columns
count(cols) > 0
some r in effective_roles
sg := schema_grant(r, res.table.schemaName)
sg.select == true
cols_all_ok(sg.columns, cols)
}
# SELECT tanpa kolom eksplisit (count(*)) pada schema yang di-grant.
allow if {
op == "SELECTFROMCOLUMNS"
res := input.action.resource
res.table.catalogName in data_catalogs
count(res.table.columns) == 0
some r in effective_roles
sg := schema_grant(r, res.table.schemaName)
sg.select == true
}
# Listing tabel: terlihat bila schema punya grant apapun.
allow if {
op == "FILTERTABLES"
res := input.action.resource
res.table.catalogName in data_catalogs
some r in effective_roles
sg := schema_grant(r, res.table.schemaName)
any_cap(sg)
}
# FilterColumns (visibilitas kolom) utk schema yang di-grant.
allow if {
op == "FILTERCOLUMNS"
res := input.action.resource
some r in effective_roles
sg := schema_grant(r, res.table.schemaName)
sg.select == true
sg.columns == "*"
}
allow if {
op == "FILTERCOLUMNS"
res := input.action.resource
cols := res.table.columns
count(cols) > 0
some r in effective_roles
sg := schema_grant(r, res.table.schemaName)
sg.select == true
cols_all_ok(sg.columns, cols)
}
allow if { allow if {
op == "SetSystemSessionProperty" op == "SetSystemSessionProperty"
} }
# ── DEV/EVAL: data-engineer bebas DDL + tulis di schema bronze ─────────────── # ── Blok luas: role dengan schema-grant 'write' = OPERASI APA PUN di schema ──
# Diminta eksplisit (2026-09-22) untuk mempermudah ingestion (Sling) dan # Dulu HARDCODE (role data-engineer + schema bronze/silver/gold via write_schemas).
# transform (dbt) di layer bronze. # Kini DATA-DRIVEN dari authz.yml `roles.<role>.schemas` — eksplisit & terport.
# #
# ⚠️ Cakupan blok ini SENGAJA LEBIH LUAS dari least-privilege per-tabel: # Cakupan sengaja LEBIH LUAS dari least-privilege per-tabel, karena:
# 1. Berlaku untuk OPERASI APA PUN yang menargetkan tabel di schema bronze — # 1. Menangkap OPERASI APA PUN di schema itu — termasuk operasi level-kolom yang
# termasuk operasi level-kolom yang dikirim Trino untuk UPDATE/MERGE, yang # dikirim Trino untuk UPDATE/MERGE dan TIDAK punya entri di op_caps. Tanpa ini
# tidak punya entri di op_caps sehingga sebelumnya jatuh ke deny-by-default. # `MERGE` (upsert) gagal: "Access Denied: Cannot update columns [...]".
# Tanpa ini, `MERGE` (upsert) selalu gagal: "Access Denied: Cannot update # 2. Berlaku untuk tabel apa pun di schema itu, termasuk tabel sementara
# columns [...]". # `<nama>_tmp` (Sling) dan tabel baru → tak perlu grant per-tabel.
# 2. Berlaku untuk tabel apa pun, termasuk tabel sementara `<nama>_tmp` milik # Hanya bila schema-grant punya 'write' — role 'select'-only (mis. data-analyst)
# Sling. Tabel itu tidak akan cocok dengan grant biasa karena policy hanya # TIDAK terpengaruh (tetap least-privilege).
# men-strip sufiks milik dbt (`__dbt_tmp`, `__dbt_backup`).
#
# Efek: tabel baru di bronze tidak lagi perlu baris grant di data.json.
# Mempersempit kembali: hapus blok ini, atau tambahkan syarat daftar nama tabel
# eksplisit. Layer lain (silver/gold) TIDAK terpengaruh — tetap least-privilege.
allow if { allow if {
res := input.action.resource res := input.action.resource
res.table.schemaName in write_schemas
some r in effective_roles some r in effective_roles
r == "data-engineer" sg := schema_grant(r, res.table.schemaName)
sg.write == true
} }
+121 -13
View File
@@ -43,9 +43,10 @@ HEADER = """\
# ./scripts/sicabe-authz render # authz.yml -> data.json # ./scripts/sicabe-authz render # authz.yml -> data.json
# ./scripts/sicabe-authz apply # render + reload OPA # ./scripts/sicabe-authz apply # render + reload OPA
# ./scripts/sicabe-authz fmt # rapikan format berkas ini # ./scripts/sicabe-authz fmt # rapikan format berkas ini
# Policy Rego membaca data.authz.catalogs / data.authz.write_schemas — nama # Policy Rego membaca data.authz.catalogs + grant schema (data.roles.<r>.schema_grants)
# catalog/schema TIDAK di-hardcode di Rego. Ganti nama catalog = ubah `catalogs:` # — nama catalog/schema & cakupan GRANT TIDAK di-hardcode di Rego. Ganti nama
# di sini -> render -> reload. Dokumentasi: docs/authorization-cli.md # catalog = ubah `catalogs:` di sini -> render -> reload. Grant tingkat-schema:
# `roles.<r>.schemas: [{schema, caps, columns}]`. Dokumentasi: docs/authorization-cli.md
# ============================================================================= # =============================================================================
""" """
@@ -92,6 +93,22 @@ def _grant(grant: dict) -> dict:
return {"table": grant["table"], "caps": list(caps), "columns": columns} return {"table": grant["table"], "caps": list(caps), "columns": columns}
def _schema_grant(sg: dict) -> dict:
if not isinstance(sg, dict) or "schema" not in sg:
raise ModelError(f"schema grant tidak valid: {sg!r}")
schema = str(sg["schema"])
if not schema or "." in schema:
raise ModelError(f"schema grant harus nama schema (tanpa '.'): {sg['schema']!r}")
caps = _as_list(sg.get("caps", []), f"caps untuk schema {schema}")
unknown = [c for c in caps if c not in CAPS]
if unknown:
raise ModelError(f"caps tak dikenal {unknown} pada schema {schema}")
columns = sg.get("columns", "*")
if columns != "*" and not isinstance(columns, list):
raise ModelError(f"columns harus '*' atau daftar, pada schema {schema}")
return {"schema": schema, "caps": list(caps), "columns": columns}
def validate(model: dict) -> None: def validate(model: dict) -> None:
if not _as_list(model.get("catalogs", []), "catalogs"): if not _as_list(model.get("catalogs", []), "catalogs"):
raise ModelError("catalogs tidak boleh kosong") raise ModelError("catalogs tidak boleh kosong")
@@ -104,6 +121,8 @@ def validate(model: dict) -> None:
raise ModelError(f"role {name!r} harus objek") raise ModelError(f"role {name!r} harus objek")
for grant in (spec or {}).get("grants", []): for grant in (spec or {}).get("grants", []):
_grant(grant) _grant(grant)
for sg in (spec or {}).get("schemas", []) or []:
_schema_grant(sg)
users = model.get("users", {}) users = model.get("users", {})
if not isinstance(users, dict): if not isinstance(users, dict):
raise ModelError("users harus berupa objek") raise ModelError("users harus berupa objek")
@@ -118,17 +137,29 @@ def _render_role(spec: dict) -> dict:
spec = spec or {} spec = spec or {}
if spec.get("superuser"): if spec.get("superuser"):
return {"superuser": True} return {"superuser": True}
out: dict = {}
tables: dict = {} tables: dict = {}
for grant in spec.get("grants", []): for grant in spec.get("grants", []):
key = _table_key(grant["table"]) g: dict = {}
out: dict = {}
caps = grant.get("caps", []) caps = grant.get("caps", [])
for cap in CAPS: for cap in CAPS:
if cap in caps: if cap in caps:
out[cap] = True g[cap] = True
out["columns"] = grant.get("columns", "*") g["columns"] = grant.get("columns", "*")
tables[key] = out tables[_table_key(grant["table"])] = g
return {"tables": tables} if tables else {} if tables:
out["tables"] = tables
schemas: dict = {}
for sg in spec.get("schemas", []) or []:
g = {}
for cap in CAPS:
if cap in sg.get("caps", []):
g[cap] = True
g["columns"] = sg.get("columns", "*")
schemas[sg["schema"]] = g
if schemas:
out["schema_grants"] = schemas
return out
def render(model: dict, digest: str) -> dict: def render(model: dict, digest: str) -> dict:
@@ -243,6 +274,66 @@ def remove_grant(model: dict, role: str, table: str, caps: list | None) -> str:
return f"hapus cap {','.join(caps)} pada {role} -> {table}" return f"hapus cap {','.join(caps)} pada {role} -> {table}"
def get_schema_grant(spec: dict, schema: str):
for sg in (spec or {}).get("schemas", []) or []:
if sg["schema"] == schema:
return sg
return None
def _spec_for(model: dict, role: str) -> dict:
roles = model.setdefault("roles", {})
if role not in roles:
raise ModelError(f"role {role!r} belum ada — buat dulu: role add {role}")
spec = roles[role]
if spec is None:
spec = {}
roles[role] = spec
if spec.get("superuser"):
raise ModelError(f"role {role!r} superuser — tak perlu grant")
return spec
def set_schema_grant(model: dict, role: str, schema: str, caps: list, columns, replace: bool) -> str:
if not schema or "." in schema:
raise ModelError(f"schema harus nama schema (tanpa '.'): {schema!r}")
for c in caps:
if c not in CAPS:
raise ModelError(f"cap tak dikenal: {c} (pilihan: {', '.join(CAPS)})")
spec = _spec_for(model, role)
sgs = spec.setdefault("schemas", [])
existing = get_schema_grant(spec, schema)
if existing is None:
sgs.append({"schema": schema, "caps": list(dict.fromkeys(caps)),
"columns": columns if columns is not None else "*"})
elif replace:
existing["caps"] = list(dict.fromkeys(caps))
existing["columns"] = columns if columns is not None else existing.get("columns", "*")
else:
existing["caps"] = list(dict.fromkeys(list(existing.get("caps", [])) + list(caps)))
if columns is not None:
existing["columns"] = columns
return ("set" if replace else "set/merge") + f" grant {role} -> {schema}.*"
def remove_schema_grant(model: dict, role: str, schema: str, caps: list | None) -> str:
spec = (model.get("roles", {}) or {}).get(role)
if not spec or not spec.get("schemas"):
raise ModelError(f"role {role!r} tak punya schema grant")
existing = get_schema_grant(spec, schema)
if existing is None:
raise ModelError(f"schema grant {role} -> {schema}.* tidak ada")
if not caps:
spec["schemas"] = [sg for sg in spec["schemas"] if sg["schema"] != schema]
return f"hapus schema grant {role} -> {schema}.*"
left = [c for c in existing.get("caps", []) if c not in caps]
if left:
existing["caps"] = left
else:
spec["schemas"] = [sg for sg in spec["schemas"] if sg["schema"] != schema]
return f"hapus cap {','.join(caps)} pada {role} -> {schema}.*"
def roles_referencing(model: dict, role: str) -> list: def roles_referencing(model: dict, role: str) -> list:
return [u for u, r in model.get("users", {}).items() if r == role] return [u for u, r in model.get("users", {}).items() if r == role]
@@ -337,6 +428,14 @@ def cmd_matrix(model_path: Path, schema: str | None) -> int:
cells.append(f"{caps_letters(g.get('caps', [])):>3}" if g else " .") cells.append(f"{caps_letters(g.get('caps', [])):>3}" if g else " .")
print(f" {r:<{w}} " + " ".join(cells)) print(f" {r:<{w}} " + " ".join(cells))
print("\n Legenda caps: s=select w=write d=ddl | ALL=superuser | .=tak ada grant") print("\n Legenda caps: s=select w=write d=ddl | ALL=superuser | .=tak ada grant")
schema_lines = []
for r in roles:
spec = (model.get("roles", {}) or {}).get(r) or {}
for sg in spec.get("schemas", []) or []:
schema_lines.append(f" {r:<{w}} {sg['schema']}.* [{caps_letters(sg.get('caps', []))}]")
if schema_lines:
print("\n Grant tingkat-SCHEMA (berlaku untuk SEMUA tabel di schema):")
print("\n".join(schema_lines))
return 0 return 0
@@ -350,7 +449,9 @@ def cmd_show(model_path: Path, what: str, name: str) -> int:
print(f"{name}: SUPERUSER (semua operasi)") print(f"{name}: SUPERUSER (semua operasi)")
for g in spec.get("grants", []) or []: for g in spec.get("grants", []) or []:
print(f"{name}: {g['table']} caps=[{','.join(g.get('caps', []))}] columns={g.get('columns')}") print(f"{name}: {g['table']} caps=[{','.join(g.get('caps', []))}] columns={g.get('columns')}")
if not spec.get("superuser") and not spec.get("grants"): for sg in spec.get("schemas", []) or []:
print(f"{name}: {sg['schema']}.* caps=[{','.join(sg.get('caps', []))}] columns={sg.get('columns')} (seluruh schema)")
if not spec.get("superuser") and not spec.get("grants") and not spec.get("schemas"):
print(f"{name}: (tanpa grant)") print(f"{name}: (tanpa grant)")
elif what == "user": elif what == "user":
role = (model.get("users", {}) or {}).get(name) role = (model.get("users", {}) or {}).get(name)
@@ -392,12 +493,19 @@ def save_model(model_path: Path, model: dict) -> None:
def cmd_grant(args, model_path: Path) -> int: def cmd_grant(args, model_path: Path) -> int:
model = load_model(model_path) model = load_model(model_path)
is_schema = args.table.endswith(".*")
target = args.table[:-2] if is_schema else args.table
caps = _split_csv(args.caps)
columns = _columns_arg(args.columns)
if args.action == "add": if args.action == "add":
msg = set_grant(model, args.role, args.table, _split_csv(args.caps), _columns_arg(args.columns), False) fn = set_schema_grant if is_schema else set_grant
msg = fn(model, args.role, target, caps, columns, False)
elif args.action == "set": elif args.action == "set":
msg = set_grant(model, args.role, args.table, _split_csv(args.caps), _columns_arg(args.columns), True) fn = set_schema_grant if is_schema else set_grant
msg = fn(model, args.role, target, caps, columns, True)
else: else:
msg = remove_grant(model, args.role, args.table, _split_csv(getattr(args, "caps", None)) or None) fn = remove_schema_grant if is_schema else remove_grant
msg = fn(model, args.role, target, caps or None)
_say(msg) _say(msg)
save_model(model_path, model) save_model(model_path, model)
return 0 return 0
+1
View File
@@ -29,6 +29,7 @@ MODEL OPA (authz.yml -> data.json):
matrix [--schema <s>] matriks role x tabel (s=select w=write d=ddl) matrix [--schema <s>] matriks role x tabel (s=select w=write d=ddl)
show role|user|table <nama> detail satu objek show role|user|table <nama> detail satu objek
grant add|set|remove <role> <schema.table> [--caps a,b] [--columns x,y|*] grant add|set|remove <role> <schema.table> [--caps a,b] [--columns x,y|*]
grant add <role> '<schema>.*' [--caps a,b] # seluruh tabel di schema (mis. gold.*)
role add|remove <nama> [--superuser] [--force] role add|remove <nama> [--superuser] [--force]
user assign|unassign <svc> <role> service account -> role (model) user assign|unassign <svc> <role> service account -> role (model)
catalog list|add|rename <baru> kelola catalog (rename ikut berkas Trino) catalog list|add|rename <baru> kelola catalog (rename ikut berkas Trino)
+47 -3
View File
@@ -55,10 +55,10 @@ then ok "data.json identik dengan hasil render"; else bad "data.json berbeda dar
# ─── B. Policy data-driven ─────────────────────────────────────────────────── # ─── B. Policy data-driven ───────────────────────────────────────────────────
echo; echo -e "${BLD}B. Policy Rego data-driven${NC}" 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 if grep -q 'data.authz.catalogs' "${REGO}" && grep -q 'schema_grant' "${REGO}"; then
ok "Rego membaca katalog/schema dari data.authz.*" ok "Rego data-driven (data.authz.catalogs + schema_grant)"
else else
bad "Rego belum data-driven (data.authz.* tidak ditemukan)" bad "Rego belum data-driven (data.authz.* / schema_grant tidak ditemukan)"
fi fi
if grep -qE '"iceberg"' "${REGO}"; then if grep -qE '"iceberg"' "${REGO}"; then
bad "Rego masih meng-hardcode literal \"iceberg\"" bad "Rego masih meng-hardcode literal \"iceberg\""
@@ -86,6 +86,19 @@ case "${out}" in *"caps=[select,write]"*) ok "grant set menimpa caps" ;; *) bad
out="$(cli show role data-scientist)" out="$(cli show role data-scientist)"
case "${out}" in *"caps=[select]"*) ok "grant remove --caps menyisakan select" ;; *) bad "grant remove gagal: ${out}" ;; esac case "${out}" in *"caps=[select]"*) ok "grant remove --caps menyisakan select" ;; *) bad "grant remove gagal: ${out}" ;; esac
"${CLI}" --model "${M}" grant add data-scientist 'gold.*' --caps select --columns "*" >/dev/null 2>&1
out="$(cli show role data-scientist)"
case "${out}" in *"gold.*"*) ok "grant tingkat-schema tersimpan (gold.*)" ;; *) bad "grant schema tak tersimpan: ${out}" ;; esac
python3 "${DEPLOY_DIR}/lib/authz.py" --model "${M}" --out "${OUT}/d2.json" render >/dev/null 2>&1
if python3 -c "import json;d=json.load(open('${OUT}/d2.json'));assert d['roles']['data-scientist']['schema_grants']['gold']['select'] is True"; then
ok "render menampilkan schema_grants"
else
bad "render tak memuat schema_grants"
fi
"${CLI}" --model "${M}" grant remove data-scientist 'gold.*' >/dev/null 2>&1
out="$(cli show role data-scientist)"
case "${out}" in *"gold.*"*) bad "grant schema tak terhapus" ;; *) ok "grant schema terhapus" ;; esac
"${CLI}" --model "${M}" user assign svc_ml data-scientist >/dev/null 2>&1 "${CLI}" --model "${M}" user assign svc_ml data-scientist >/dev/null 2>&1
out="$(cli show user svc_ml)" out="$(cli show user svc_ml)"
case "${out}" in *"svc_ml: data-scientist"*) ok "user assign bekerja" ;; *) bad "user assign gagal: ${out}" ;; esac case "${out}" in *"svc_ml: data-scientist"*) ok "user assign bekerja" ;; *) bad "user assign gagal: ${out}" ;; esac
@@ -181,12 +194,43 @@ for i, (_, want) in enumerate(base_cases):
print(f" keputusan[{i}] -> {got or '(undefined)'} (harap {want})") print(f" keputusan[{i}] -> {got or '(undefined)'} (harap {want})")
if got != want: if got != want:
fails += 1 fails += 1
# Blok luas data-driven: op di LUAR op_caps (simulasi level-kolom UPDATE/MERGE)
extra = [
(tin("UpdateColumns", "iceberg", "bronze", "orders", ["id"], "u", ["data-engineer"]), "true"),
(tin("UpdateColumns", "iceberg", "gold", "orders_gold", ["id"], "u", ["data-analyst"]), "false"),
(tin("UpdateColumns", "iceberg", "other", "x", ["id"], "u", ["data-engineer"]), "false"),
]
te = build(extra)
for i, (_, want) in enumerate(extra):
got = ev(te, i)
print(f" luas[{i}] -> {got or '(undefined)'} (harap {want})")
if got != want:
fails += 1
tr = build(ren_cases, {"authz": {"catalogs": ["lakehouse"], "write_schemas": ["bronze", "silver", "gold"]}}) tr = build(ren_cases, {"authz": {"catalogs": ["lakehouse"], "write_schemas": ["bronze", "silver", "gold"]}})
for i, (_, want) in enumerate(ren_cases): for i, (_, want) in enumerate(ren_cases):
got = ev(tr, i) got = ev(tr, i)
print(f" rename[{i}] -> {got or '(undefined)'} (harap {want})") print(f" rename[{i}] -> {got or '(undefined)'} (harap {want})")
if got != want: if got != want:
fails += 1 fails += 1
# Grant tingkat-schema: data-analyst dapat 'gold.*' (select)
roles = json.loads((opa / "data.json").read_text())["roles"]
roles["data-analyst"]["schema_grants"] = {"gold": {"select": True, "columns": "*"}}
sch_cases = [
(tin("SELECTFROMCOLUMNS", "iceberg", "gold", "orders_gold", ["id"], "u", ["data-analyst"]), "true"),
(tin("SELECTFROMCOLUMNS", "iceberg", "gold", "brand_new_table", ["id"], "u", ["data-analyst"]), "true"),
(tin("SELECTFROMCOLUMNS", "iceberg", "bronze", "orders", ["id"], "u", ["data-analyst"]), "false"),
(tin("SELECTFROMCOLUMNS", "iceberg", "silver", "orders_silver", ["order_date"], "u", ["data-analyst"]), "false"),
(tin("FILTERTABLES", "iceberg", "gold", "whatever", [], "u", ["data-analyst"]), "true"),
]
ts = build(sch_cases, {"roles": roles})
for i, (_, want) in enumerate(sch_cases):
got = ev(ts, i)
print(f" schema[{i}] -> {got or '(undefined)'} (harap {want})")
if got != want:
fails += 1
sys.exit(1 if fails else 0) sys.exit(1 if fails else 0)
PY PY
then ok "policy OPA: perilaku dasar + ganti catalog lewat data (via docker)" then ok "policy OPA: perilaku dasar + ganti catalog lewat data (via docker)"