first commit
This commit is contained in:
@@ -0,0 +1,270 @@
|
||||
# =============================================================================
|
||||
# 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>.
|
||||
# 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
|
||||
|
||||
# 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
|
||||
res.catalog.name in {"iceberg", "system"}
|
||||
}
|
||||
allow if {
|
||||
op == "FILTERSCHEMAS"
|
||||
res := input.action.resource
|
||||
res.schema.catalogName in {"iceberg", "system"}
|
||||
}
|
||||
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)])
|
||||
|
||||
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
|
||||
res.table.catalogName == "iceberg"
|
||||
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
|
||||
res.table.catalogName == "iceberg"
|
||||
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
|
||||
res.table.catalogName == "iceberg"
|
||||
tid := table_id(res.table.schemaName, res.table.tableName)
|
||||
some r in effective_roles
|
||||
grant := data.roles[r].tables[tid]
|
||||
any_cap(grant)
|
||||
}
|
||||
|
||||
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.
|
||||
#
|
||||
# ⚠️ 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.
|
||||
allow if {
|
||||
res := input.action.resource
|
||||
res.table.schemaName in {"bronze", "silver", "gold"}
|
||||
some r in effective_roles
|
||||
r == "data-engineer"
|
||||
}
|
||||
Reference in New Issue
Block a user