A warehouse enforces GRANT itself because it owns the storage. A lakehouse table is a set of files that any engine holding the bucket's key can read, so access control must hold at three layers: object storage (which keys reach which prefixes, Object Storage IAM), the catalog (who may load, create or drop a table: Polaris roles, Lakekeeper with OpenFGA, Unity Catalog grants) and the query engine, the only layer that understands rows and columns. Trino 483 403,499 offers allow-all, file-based, Open Policy Agent and Apache Ranger access control; BookNest's file-based rules give analyst the sales tables and a masked customers, and the Malaysian support team support_my only Malaysian customers:
{
"tables": [
{"user": "admin", "privileges": ["SELECT", "INSERT", "DELETE", "UPDATE", "OWNERSHIP"]},
{"user": "support_my", "schema": "booknest", "table": "customers",
"privileges": ["SELECT"], "filter": "country = 'MY'"},
{"user": "analyst", "schema": "booknest", "table": "customers", "privileges": ["SELECT"],
"columns": [
{"name": "email", "mask": "regexp_replace(email, '^(.)[^@]*', '$1***')"},
{"name": "national_id", "mask": "CAST(NULL AS varchar)"},
{"name": "phone", "allow": false}]},
{"user": "analyst", "schema": "booknest", "table": "(orders|order_items|daily_sales)",
"privileges": ["SELECT"]}
]
}The first matching rule wins, and a table no rule matches is denied. access-control.name=file and security.config-file=/etc/trino/rules.json in access-control.properties switch it on (privacy/trino_acl_up.sh mounts both into l1-trino). As admin, customer 12 shows gus.evans12@example.com and IC 011223-12-1792; privacy/acl.sh runs queries as the other users:
-- analyst: SELECT customer_id, email, national_id FROM customers WHERE customer_id = 12
customer_id | email | national_id
-------------+------------------+-------------
12 | g***@example.com | NULL
-- analyst: SELECT phone FROM customers WHERE customer_id = 12
Access Denied: Cannot select from table iceberg.booknest.customers
-- support_my: SELECT country, count(*) AS customers FROM customers GROUP BY country
country | customers
---------+-----------
MY | 291
-- analyst: DELETE FROM orders WHERE order_id = 1
Access Denied: Cannot delete from table iceberg.booknest.ordersMasks and filters are SQL expressions Trino adds to the plan, so they hold inside joins, views and CREATE TABLE AS. Two caveats: without authentication --user analyst is a claim anyone can make (Securing Catalog and Engines adds passwords), and the rules bind only Trino, while a Spark 129 job holding the MinIO 30,943 key reads raw files. Close that gap with credential vending: Polaris, Lakekeeper and Unity Catalog check the caller's grants, then hand the engine short-lived storage credentials scoped to one table's prefix, so no engine holds the bucket key.