Files
sicabe-core/deploy/config/opa/policies/trino.rego
T

353 lines
13 KiB
Rego
Raw Normal View History

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
}