sync a58faaa refactor(authz)
This commit is contained in:
@@ -6,9 +6,10 @@
|
||||
# ./scripts/sicabe-authz render # authz.yml -> data.json
|
||||
# ./scripts/sicabe-authz apply # render + reload OPA
|
||||
# ./scripts/sicabe-authz fmt # rapikan format berkas ini
|
||||
# Policy Rego membaca data.authz.catalogs / data.authz.write_schemas — nama
|
||||
# catalog/schema TIDAK di-hardcode di Rego. Ganti nama catalog = ubah `catalogs:`
|
||||
# di sini -> render -> reload. Dokumentasi: docs/authorization-cli.md
|
||||
# Policy Rego membaca data.authz.catalogs + grant schema (data.roles.<r>.schema_grants)
|
||||
# — nama catalog/schema & cakupan GRANT TIDAK di-hardcode di Rego. Ganti nama
|
||||
# catalog = ubah `catalogs:` di sini -> render -> reload. Grant tingkat-schema:
|
||||
# `roles.<r>.schemas: [{schema, caps, columns}]`. Dokumentasi: docs/authorization-cli.md
|
||||
# =============================================================================
|
||||
version: 1
|
||||
catalogs: [iceberg]
|
||||
@@ -29,6 +30,16 @@ roles:
|
||||
- table: gold.orders_gold
|
||||
caps: [select, write, ddl]
|
||||
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:
|
||||
grants:
|
||||
- table: bronze.orders
|
||||
|
||||
@@ -39,6 +39,26 @@
|
||||
"ddl": true,
|
||||
"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": {
|
||||
@@ -90,6 +110,6 @@
|
||||
},
|
||||
"_generated": {
|
||||
"from": "authz.yml",
|
||||
"sha256": "8c7de053a5683e00c27f8d92157463c65ff6d45d5802e1e03b48bb9a2a933cb5"
|
||||
"sha256": "36620fa9962357759a498f40d22a3a729d8fa7a8fa69d9941dbd270806968aad"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,9 +38,7 @@ identity_user := input.context.identity.user
|
||||
# SPESIFIK (data.authz.*) — JANGAN meneruskan `data` utuh ke builtin, sebab itu
|
||||
# membuat compiler menganggapnya bergantung pada `allow` (rego_recursion_error).
|
||||
default data_catalogs := []
|
||||
default write_schemas := []
|
||||
data_catalogs := data.authz.catalogs
|
||||
write_schemas := data.authz.write_schemas
|
||||
|
||||
# Catalog yang boleh di-listing = catalog terdaftar + 'system' (bawaan Trino).
|
||||
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)])
|
||||
|
||||
# ── 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 {
|
||||
cap := op_caps[op]
|
||||
res := input.action.resource
|
||||
@@ -257,30 +264,89 @@ allow if {
|
||||
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 {
|
||||
op == "SetSystemSessionProperty"
|
||||
}
|
||||
|
||||
# ── DEV/EVAL: data-engineer bebas DDL + tulis di schema bronze ───────────────
|
||||
# Diminta eksplisit (2026-09-22) untuk mempermudah ingestion (Sling) dan
|
||||
# transform (dbt) di layer bronze.
|
||||
# ── Blok luas: role dengan schema-grant 'write' = OPERASI APA PUN di schema ──
|
||||
# Dulu HARDCODE (role data-engineer + schema bronze/silver/gold via write_schemas).
|
||||
# Kini DATA-DRIVEN dari authz.yml `roles.<role>.schemas` — eksplisit & terport.
|
||||
#
|
||||
# ⚠️ Cakupan blok ini SENGAJA LEBIH LUAS dari least-privilege per-tabel:
|
||||
# 1. Berlaku untuk OPERASI APA PUN yang menargetkan tabel di schema bronze —
|
||||
# termasuk operasi level-kolom yang dikirim Trino untuk UPDATE/MERGE, yang
|
||||
# tidak punya entri di op_caps sehingga sebelumnya jatuh ke deny-by-default.
|
||||
# Tanpa ini, `MERGE` (upsert) selalu gagal: "Access Denied: Cannot update
|
||||
# columns [...]".
|
||||
# 2. Berlaku untuk tabel apa pun, termasuk tabel sementara `<nama>_tmp` milik
|
||||
# Sling. Tabel itu tidak akan cocok dengan grant biasa karena policy hanya
|
||||
# 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.
|
||||
# Cakupan sengaja LEBIH LUAS dari least-privilege per-tabel, karena:
|
||||
# 1. Menangkap OPERASI APA PUN di schema itu — termasuk operasi level-kolom yang
|
||||
# dikirim Trino untuk UPDATE/MERGE dan TIDAK punya entri di op_caps. Tanpa ini
|
||||
# `MERGE` (upsert) gagal: "Access Denied: Cannot update columns [...]".
|
||||
# 2. Berlaku untuk tabel apa pun di schema itu, termasuk tabel sementara
|
||||
# `<nama>_tmp` (Sling) dan tabel baru → tak perlu grant per-tabel.
|
||||
# Hanya bila schema-grant punya 'write' — role 'select'-only (mis. data-analyst)
|
||||
# TIDAK terpengaruh (tetap least-privilege).
|
||||
allow if {
|
||||
res := input.action.resource
|
||||
res.table.schemaName in write_schemas
|
||||
some r in effective_roles
|
||||
r == "data-engineer"
|
||||
sg := schema_grant(r, res.table.schemaName)
|
||||
sg.write == true
|
||||
}
|
||||
|
||||
+121
-13
@@ -43,9 +43,10 @@ HEADER = """\
|
||||
# ./scripts/sicabe-authz render # authz.yml -> data.json
|
||||
# ./scripts/sicabe-authz apply # render + reload OPA
|
||||
# ./scripts/sicabe-authz fmt # rapikan format berkas ini
|
||||
# Policy Rego membaca data.authz.catalogs / data.authz.write_schemas — nama
|
||||
# catalog/schema TIDAK di-hardcode di Rego. Ganti nama catalog = ubah `catalogs:`
|
||||
# di sini -> render -> reload. Dokumentasi: docs/authorization-cli.md
|
||||
# Policy Rego membaca data.authz.catalogs + grant schema (data.roles.<r>.schema_grants)
|
||||
# — nama catalog/schema & cakupan GRANT TIDAK di-hardcode di Rego. Ganti nama
|
||||
# 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}
|
||||
|
||||
|
||||
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:
|
||||
if not _as_list(model.get("catalogs", []), "catalogs"):
|
||||
raise ModelError("catalogs tidak boleh kosong")
|
||||
@@ -104,6 +121,8 @@ def validate(model: dict) -> None:
|
||||
raise ModelError(f"role {name!r} harus objek")
|
||||
for grant in (spec or {}).get("grants", []):
|
||||
_grant(grant)
|
||||
for sg in (spec or {}).get("schemas", []) or []:
|
||||
_schema_grant(sg)
|
||||
users = model.get("users", {})
|
||||
if not isinstance(users, dict):
|
||||
raise ModelError("users harus berupa objek")
|
||||
@@ -118,17 +137,29 @@ def _render_role(spec: dict) -> dict:
|
||||
spec = spec or {}
|
||||
if spec.get("superuser"):
|
||||
return {"superuser": True}
|
||||
out: dict = {}
|
||||
tables: dict = {}
|
||||
for grant in spec.get("grants", []):
|
||||
key = _table_key(grant["table"])
|
||||
out: dict = {}
|
||||
g: dict = {}
|
||||
caps = grant.get("caps", [])
|
||||
for cap in CAPS:
|
||||
if cap in caps:
|
||||
out[cap] = True
|
||||
out["columns"] = grant.get("columns", "*")
|
||||
tables[key] = out
|
||||
return {"tables": tables} if tables else {}
|
||||
g[cap] = True
|
||||
g["columns"] = grant.get("columns", "*")
|
||||
tables[_table_key(grant["table"])] = g
|
||||
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:
|
||||
@@ -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}"
|
||||
|
||||
|
||||
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:
|
||||
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 " .")
|
||||
print(f" {r:<{w}} " + " ".join(cells))
|
||||
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
|
||||
|
||||
|
||||
@@ -350,7 +449,9 @@ def cmd_show(model_path: Path, what: str, name: str) -> int:
|
||||
print(f"{name}: SUPERUSER (semua operasi)")
|
||||
for g in spec.get("grants", []) or []:
|
||||
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)")
|
||||
elif what == "user":
|
||||
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:
|
||||
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":
|
||||
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":
|
||||
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:
|
||||
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)
|
||||
save_model(model_path, model)
|
||||
return 0
|
||||
|
||||
@@ -29,6 +29,7 @@ MODEL OPA (authz.yml -> data.json):
|
||||
matrix [--schema <s>] matriks role x tabel (s=select w=write d=ddl)
|
||||
show role|user|table <nama> detail satu objek
|
||||
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]
|
||||
user assign|unassign <svc> <role> service account -> role (model)
|
||||
catalog list|add|rename <baru> kelola catalog (rename ikut berkas Trino)
|
||||
|
||||
@@ -55,10 +55,10 @@ then ok "data.json identik dengan hasil render"; else bad "data.json berbeda dar
|
||||
|
||||
# ─── 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.*"
|
||||
if grep -q 'data.authz.catalogs' "${REGO}" && grep -q 'schema_grant' "${REGO}"; then
|
||||
ok "Rego data-driven (data.authz.catalogs + schema_grant)"
|
||||
else
|
||||
bad "Rego belum data-driven (data.authz.* tidak ditemukan)"
|
||||
bad "Rego belum data-driven (data.authz.* / schema_grant tidak ditemukan)"
|
||||
fi
|
||||
if grep -qE '"iceberg"' "${REGO}"; then
|
||||
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)"
|
||||
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
|
||||
out="$(cli show user svc_ml)"
|
||||
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})")
|
||||
if got != want:
|
||||
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"]}})
|
||||
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
|
||||
|
||||
# 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)
|
||||
PY
|
||||
then ok "policy OPA: perilaku dasar + ganti catalog lewat data (via docker)"
|
||||
|
||||
Reference in New Issue
Block a user