Skip to content

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.

rls.yaml
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'))"
Terminal window
curral serve ... --row-filters rls.yaml
  • 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 where clauses are combined with OR.
  • A user who matches no rule is not filtered. Whether they may read the table at all is the policy’s decision.

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'))"

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.

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).