Row-level security
Row filters restrict which rows a user sees in a table. They live in a
YAML file passed with --row-filters, which reloads on SIGHUP.
tables: lake.analytics.customers: # catalog.schema.table rules: - roles: [analyst_north] where: "region = 'north'" - users: ["ana@example.com", "*@corp.example.com"] where: "region IN (SELECT region FROM ctl.main.acl WHERE usr = getvariable('curral_user'))"curral serve ... --row-filters rls.yamlHow rules match
Section titled “How rules match”- A rule applies to whoever matches it, by role or by user (exact name or
*@domain). - A user who matches several rules sees the union: the
whereclauses are combined withOR. - A user who matches no rule is not filtered. Whether they may read the table at all is the policy’s decision.
Filters driven by data
Section titled “Filters driven by data”getvariable('curral_user') and getvariable('curral_roles') expose the
caller inside a where. With an ACL table, access changes by editing data
instead of configuration:
where: "region IN (SELECT region FROM ctl.main.acl WHERE usr = getvariable('curral_user'))"Validation
Section titled “Validation”Each where is validated against the real table at startup, in
curral check and on reload. An error aborts startup; on reload, the
previous file is kept.
How it is enforced
Section titled “How it is enforced”Each reference to the table in the user’s query becomes, in DuckDB’s own
syntax tree, a subquery that filters first. The user’s query runs on top
of it, so WHERE, joins and aggregations never see hidden rows.
Queries that cannot be protected safely are denied with
decided_by: protection. The rules are shared with column masking; see
What gets denied.
/v1/schema flags filtered tables with "row_filtered": true, and the dry
run and audit log list the filters applied (row_filters).