2026-10-09 02:35:14 +07:00
|
|
|
|
# =============================================================================
|
|
|
|
|
|
# OPA — policy Trino access-control (package trino) — OPA-ONLY
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
|
|
|
|
# Trino (access-control.name=opa) mengirim satu request per operasi:
|
|
|
|
|
|
# POST http://opa:8181/v1/data/trino/allow
|
|
|
|
|
|
# input.context.identity = { "user": "...", "groups": [...] }
|
|
|
|
|
|
# input.action.operation = "SELECT" | "INSERT" | ...
|
|
|
|
|
|
# input.action.resource = { catalogName, schemaName, tableName, columns }
|
|
|
|
|
|
# OPA mengembalikan { "result": { "allow": true|false } }.
|
|
|
|
|
|
#
|
|
|
|
|
|
# Grup user berasal dari LDAP group provider Trino (OpenLDAP, cn = data-*).
|
|
|
|
|
|
# Izin didefinisikan sebagai data di data.json → data.roles.<grup>.tables.<schema_table>.
|
2026-10-13 14:44:55 +07:00
|
|
|
|
# Nama catalog/schema TIDAK di-hardcode di policy: dibaca dari data.authz.catalogs /
|
|
|
|
|
|
# data.authz.write_schemas (hasil render config/opa/authz.yml) → rename catalog/schema
|
|
|
|
|
|
# cukup mengubah data, bukan Rego. Lihat scripts/sicabe-authz.
|
2026-10-09 02:35:14 +07:00
|
|
|
|
# grant = { select: bool, write: bool, ddl: bool, columns: "*" | [..] }
|
|
|
|
|
|
# - data-admin : superuser (semua operasi)
|
|
|
|
|
|
# - select : SELECT/SHOW COLUMNS/DESCRIBE
|
|
|
|
|
|
# - write : INSERT/UPDATE/DELETE/MERGE
|
|
|
|
|
|
# - ddl : CREATE/DROP/ALTER/... TABLE
|
|
|
|
|
|
# Kolom: bila columns != "*", SELECT hanya boleh menyebut kolom tsb.
|
|
|
|
|
|
# deny-by-default.
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
package trino
|
|
|
|
|
|
|
|
|
|
|
|
import rego.v1
|
|
|
|
|
|
|
|
|
|
|
|
default allow := false
|
|
|
|
|
|
|
|
|
|
|
|
# Normalisasi operasi: uppercase, buang spasi & underscore.
|
|
|
|
|
|
op := upper(replace(replace(trim_space(input.action.operation), " ", ""), "_", ""))
|
|
|
|
|
|
|
|
|
|
|
|
identity_groups := input.context.identity.groups
|
|
|
|
|
|
identity_user := input.context.identity.user
|
|
|
|
|
|
|
2026-10-13 14:44:55 +07:00
|
|
|
|
# ── Nama catalog/schema dari DATA (authz.yml), JANGAN di-hardcode di sini ────
|
|
|
|
|
|
# `default` → aman (deny-by-default) bila data.authz belum ada. Rujuk path
|
|
|
|
|
|
# SPESIFIK (data.authz.*) — JANGAN meneruskan `data` utuh ke builtin, sebab itu
|
|
|
|
|
|
# membuat compiler menganggapnya bergantung pada `allow` (rego_recursion_error).
|
|
|
|
|
|
default data_catalogs := []
|
|
|
|
|
|
data_catalogs := data.authz.catalogs
|
|
|
|
|
|
|
|
|
|
|
|
# Catalog yang boleh di-listing = catalog terdaftar + 'system' (bawaan Trino).
|
|
|
|
|
|
allowed_catalogs contains c if { some c in data_catalogs }
|
|
|
|
|
|
allowed_catalogs contains "system"
|
|
|
|
|
|
|
2026-10-09 02:35:14 +07:00
|
|
|
|
# Role efektif = grup (dari LDAP) ∪ role user (data.users) — untuk service account
|
|
|
|
|
|
# yang tidak terdaftar di LDAP (mis. svc_dbt, svc_om dari password file Basic).
|
|
|
|
|
|
effective_roles contains r if {
|
|
|
|
|
|
some g in identity_groups
|
|
|
|
|
|
r := g
|
|
|
|
|
|
}
|
|
|
|
|
|
effective_roles contains r if {
|
|
|
|
|
|
r := data.users[identity_user]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# Grup 'data-admin' = superuser (bypass semua pembatasan data/sistem).
|
|
|
|
|
|
is_super if {
|
|
|
|
|
|
some r in effective_roles
|
|
|
|
|
|
r == "data-admin"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# ── CREATE/DROP SCHEMA (DDL level-schema) ───────────────────────────────────
|
|
|
|
|
|
# Trino mengirim operasi ini tanpa resource tabel → tak bisa memakai grant
|
|
|
|
|
|
# per-tabel di op_caps. Izinkan untuk peran engineering (data-admin/engineer).
|
|
|
|
|
|
allow if {
|
|
|
|
|
|
op in {"CREATESCHEMA", "DROPSCHEMA"}
|
|
|
|
|
|
some r in effective_roles
|
|
|
|
|
|
r in {"data-admin", "data-engineer"}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# ── Pemetaan operasi -> kemampuan yang dibutuhkan ───────────────────────────
|
|
|
|
|
|
# SELECTFROMCOLUMNS TIDAK ada di sini (ditangani khusus + cek kolom).
|
|
|
|
|
|
op_caps := {
|
|
|
|
|
|
"SHOWCOLUMNS": "select",
|
|
|
|
|
|
"DESCRIBE": "select",
|
|
|
|
|
|
"INSERTINTOTABLE": "write",
|
|
|
|
|
|
"UPDATE": "write",
|
|
|
|
|
|
"DELETE": "write",
|
|
|
|
|
|
"MERGE": "write",
|
|
|
|
|
|
"CREATETABLE": "ddl",
|
|
|
|
|
|
"DROPTABLE": "ddl",
|
|
|
|
|
|
"TRUNCATETABLE": "ddl",
|
|
|
|
|
|
"ALTERTABLE": "ddl",
|
|
|
|
|
|
"RENAMETABLE": "ddl",
|
|
|
|
|
|
"ADDCOLUMN": "ddl",
|
|
|
|
|
|
"DROPCOLUMN": "ddl",
|
|
|
|
|
|
"RENAMECOLUMN": "ddl",
|
|
|
|
|
|
"COMMENTTABLE": "ddl",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# ── Operasi metadata yang diizinkan utk user terautentikasi ────────────────
|
|
|
|
|
|
meta_op(opx) if startswith(opx, "SHOW")
|
|
|
|
|
|
meta_op(opx) if opx in {"USE", "SETSESSION", "RESETSESSION", "SETROLE", "SHOWROLES", "SHOWCURRENTROLES"}
|
|
|
|
|
|
|
|
|
|
|
|
# ── Eksekusi query (level sistem) ───────────────────────────────────────────
|
|
|
|
|
|
allow if op == "EXECUTEQUERY"
|
|
|
|
|
|
|
|
|
|
|
|
# ── Operasi metadata SHOW/USE/... utk user terautentikasi ──────────────────
|
|
|
|
|
|
allow if meta_op(op)
|
|
|
|
|
|
|
|
|
|
|
|
# ── Superuser ───────────────────────────────────────────────────────────────
|
|
|
|
|
|
allow if is_super
|
|
|
|
|
|
|
|
|
|
|
|
# ── Schema sistem/metadata (listing/autocomplete; isi data tetap terbatas) ─
|
|
|
|
|
|
allow if {
|
|
|
|
|
|
op == "SELECTFROMCOLUMNS"
|
|
|
|
|
|
res := input.action.resource
|
|
|
|
|
|
res.table.catalogName == "system"
|
|
|
|
|
|
res.table.schemaName in {"jdbc", "metadata", "information_schema"}
|
|
|
|
|
|
}
|
|
|
|
|
|
allow if {
|
|
|
|
|
|
op == "SELECTFROMCOLUMNS"
|
|
|
|
|
|
res := input.action.resource
|
|
|
|
|
|
res.table.schemaName == "information_schema"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# ── Akses & listing catalog / schema ────────────────────────────────────────
|
|
|
|
|
|
allow if {
|
|
|
|
|
|
op in {"ACCESSCATALOG", "FILTERCATALOGS"}
|
|
|
|
|
|
res := input.action.resource
|
2026-10-13 14:44:55 +07:00
|
|
|
|
res.catalog.name in allowed_catalogs
|
2026-10-09 02:35:14 +07:00
|
|
|
|
}
|
|
|
|
|
|
allow if {
|
|
|
|
|
|
op == "FILTERSCHEMAS"
|
|
|
|
|
|
res := input.action.resource
|
2026-10-13 14:44:55 +07:00
|
|
|
|
res.schema.catalogName in allowed_catalogs
|
2026-10-09 02:35:14 +07:00
|
|
|
|
}
|
|
|
|
|
|
allow if {
|
|
|
|
|
|
op == "FILTERTABLES"
|
|
|
|
|
|
res := input.action.resource
|
|
|
|
|
|
res.table.catalogName == "system"
|
|
|
|
|
|
res.table.schemaName in {"jdbc", "metadata", "information_schema"}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# ── Operasi data/metadata per tabel (non-SELECT) ────────────────────────────
|
|
|
|
|
|
# dbt membuat tabel sementara <name>__dbt_tmp (dan backup <name>__dbt_backup):
|
|
|
|
|
|
# cocokkan ke grant tabel dasarnya.
|
|
|
|
|
|
dbt_suffixes := {"__dbt_tmp", "__dbt_backup"}
|
|
|
|
|
|
|
|
|
|
|
|
has_dbt_suffix(name) if {
|
|
|
|
|
|
some s in dbt_suffixes
|
|
|
|
|
|
endswith(name, s)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
base_table_name(name) := n if {
|
|
|
|
|
|
some s in dbt_suffixes
|
|
|
|
|
|
endswith(name, s)
|
|
|
|
|
|
n := substring(name, 0, count(name) - count(s))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
base_table_name(name) := name if {
|
|
|
|
|
|
not has_dbt_suffix(name)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
table_id(schema, table) := sprintf("%s_%s", [schema, base_table_name(table)])
|
|
|
|
|
|
|
2026-10-13 18:28:09 +07:00
|
|
|
|
# ── 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]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-09 02:35:14 +07:00
|
|
|
|
allow if {
|
|
|
|
|
|
cap := op_caps[op]
|
|
|
|
|
|
res := input.action.resource
|
|
|
|
|
|
tid := table_id(res.table.schemaName, res.table.tableName)
|
|
|
|
|
|
some r in effective_roles
|
|
|
|
|
|
grant := data.roles[r].tables[tid]
|
|
|
|
|
|
grant[cap] == true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# ── SELECT ... (per-kolom) ──────────────────────────────────────────────────
|
|
|
|
|
|
# Semua kolom yang diminta harus diizinkan oleh grant.columns.
|
|
|
|
|
|
cols_all_ok(columns, cols) if columns == "*"
|
|
|
|
|
|
cols_all_ok(columns, cols) if {
|
|
|
|
|
|
columns != "*"
|
|
|
|
|
|
every c in cols {
|
|
|
|
|
|
c in columns
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
allow if {
|
|
|
|
|
|
op == "SELECTFROMCOLUMNS"
|
|
|
|
|
|
res := input.action.resource
|
2026-10-13 14:44:55 +07:00
|
|
|
|
res.table.catalogName in data_catalogs
|
2026-10-09 02:35:14 +07:00
|
|
|
|
tid := table_id(res.table.schemaName, res.table.tableName)
|
|
|
|
|
|
cols := res.table.columns
|
|
|
|
|
|
count(cols) > 0
|
|
|
|
|
|
some r in effective_roles
|
|
|
|
|
|
grant := data.roles[r].tables[tid]
|
|
|
|
|
|
grant.select == true
|
|
|
|
|
|
cols_all_ok(grant.columns, cols)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# ── SELECT tanpa kolom eksplisit (mis. count(*)) ────────────────────────────
|
|
|
|
|
|
# Trino mengirim columns: [] untuk count(*)/agregasi tanpa kolom. Izinkan bila
|
|
|
|
|
|
# user punya 'select' pada tabel tsb (tidak membocorkan isi kolom). Tanpa rule
|
|
|
|
|
|
# ini, count(*) selalu ditolak karena rule SELECT di atas butuh count(cols) > 0.
|
|
|
|
|
|
allow if {
|
|
|
|
|
|
op == "SELECTFROMCOLUMNS"
|
|
|
|
|
|
res := input.action.resource
|
2026-10-13 14:44:55 +07:00
|
|
|
|
res.table.catalogName in data_catalogs
|
2026-10-09 02:35:14 +07:00
|
|
|
|
tid := table_id(res.table.schemaName, res.table.tableName)
|
|
|
|
|
|
count(res.table.columns) == 0
|
|
|
|
|
|
some r in effective_roles
|
|
|
|
|
|
grant := data.roles[r].tables[tid]
|
|
|
|
|
|
grant.select == true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# ── FilterColumns — daftar kolom utk metadata (information_schema/SHOW COLUMNS)
|
|
|
|
|
|
# Trino memakai operasi ini untuk memfilter kolom yang boleh DILIHAT. Tanpa rule
|
|
|
|
|
|
# ini (deny-by-default) SEMUA kolom tersembunyi dari information_schema,
|
|
|
|
|
|
# `SHOW COLUMNS`, dan `DESCRIBE` — padahal SELECT-nya sendiri diizinkan. Akibat
|
|
|
|
|
|
# nyata: katalog metadata (mis. ingestion OpenMetadata) melihat tabel TANPA kolom.
|
|
|
|
|
|
# Batas per-kolom tetap dihormati lewat cols_all_ok (least privilege).
|
|
|
|
|
|
allow if {
|
|
|
|
|
|
op == "FILTERCOLUMNS"
|
|
|
|
|
|
res := input.action.resource
|
|
|
|
|
|
res.table.schemaName == "information_schema"
|
|
|
|
|
|
}
|
|
|
|
|
|
allow if {
|
|
|
|
|
|
op == "FILTERCOLUMNS"
|
|
|
|
|
|
res := input.action.resource
|
|
|
|
|
|
res.table.catalogName == "system"
|
|
|
|
|
|
}
|
|
|
|
|
|
allow if {
|
|
|
|
|
|
op == "FILTERCOLUMNS"
|
|
|
|
|
|
res := input.action.resource
|
|
|
|
|
|
tid := table_id(res.table.schemaName, res.table.tableName)
|
|
|
|
|
|
some r in effective_roles
|
|
|
|
|
|
grant := data.roles[r].tables[tid]
|
|
|
|
|
|
grant.select == true
|
|
|
|
|
|
grant.columns == "*"
|
|
|
|
|
|
}
|
|
|
|
|
|
allow if {
|
|
|
|
|
|
op == "FILTERCOLUMNS"
|
|
|
|
|
|
res := input.action.resource
|
|
|
|
|
|
tid := table_id(res.table.schemaName, res.table.tableName)
|
|
|
|
|
|
cols := res.table.columns
|
|
|
|
|
|
count(cols) > 0
|
|
|
|
|
|
some r in effective_roles
|
|
|
|
|
|
grant := data.roles[r].tables[tid]
|
|
|
|
|
|
grant.select == true
|
|
|
|
|
|
cols_all_ok(grant.columns, cols)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# ── Listing tabel (FilterTables): tabel terlihat bila ada grant apapun ─────
|
|
|
|
|
|
any_cap(grant) if {
|
|
|
|
|
|
some cap in ["select", "write", "ddl"]
|
|
|
|
|
|
grant[cap] == true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
allow if {
|
|
|
|
|
|
op == "FILTERTABLES"
|
|
|
|
|
|
res := input.action.resource
|
2026-10-13 14:44:55 +07:00
|
|
|
|
res.table.catalogName in data_catalogs
|
2026-10-09 02:35:14 +07:00
|
|
|
|
tid := table_id(res.table.schemaName, res.table.tableName)
|
|
|
|
|
|
some r in effective_roles
|
|
|
|
|
|
grant := data.roles[r].tables[tid]
|
|
|
|
|
|
any_cap(grant)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-13 18:28:09 +07:00
|
|
|
|
# ── 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)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-09 02:35:14 +07:00
|
|
|
|
allow if {
|
|
|
|
|
|
op == "SetSystemSessionProperty"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-13 18:28:09 +07:00
|
|
|
|
# ── 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.
|
2026-10-09 02:35:14 +07:00
|
|
|
|
#
|
2026-10-13 18:28:09 +07:00
|
|
|
|
# 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).
|
2026-10-09 02:35:14 +07:00
|
|
|
|
allow if {
|
|
|
|
|
|
res := input.action.resource
|
|
|
|
|
|
some r in effective_roles
|
2026-10-13 18:28:09 +07:00
|
|
|
|
sg := schema_grant(r, res.table.schemaName)
|
|
|
|
|
|
sg.write == true
|
2026-10-09 02:35:14 +07:00
|
|
|
|
}
|