sync a58faaa refactor(authz)
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)"
|
||||||
|
|||||||
Reference in New Issue
Block a user