Row filter rules
Limit which rows users can see by injecting WHERE clauses into queries based on user properties like tenant, department, or role.
Row filter rules are the most powerful mechanism in Kaelio's SQL security policies. They inject WHERE clauses into queries so that users only see the rows they're authorized to access. This enables multi-tenant isolation, department-scoped data, and regional access control — all without users needing to write any filtering logic themselves.
Rules are defined in the row_filter_rules section of your
policy YAML.
Basic usage
Each rule specifies a table_name and a filter_sql clause.
The filter_sql is a WHERE condition that gets injected into
every query that touches the matching table.
version: "1.0"
default_allow_tables: true
row_filter_rules:
- table_name: orders
filter_sql: "tenant_id = '{tenant_id}'"Template variables like {tenant_id} are replaced with the
actual value from the user's security properties at query
time.
Before and after
Given the policy above and a user whose tenant_id property
is set to acme:
SELECT * FROM ordersSELECT * FROM orders WHERE tenant_id = 'acme'The user sees only their own tenant's orders without writing any filtering logic.
Template variables
Template variables are placeholders in your filter_sql that
get replaced with user-specific values. Wrap them in curly
braces: {variable_name}.
Built-in variables
These variables are always available for every user:
| Variable | Description |
|---|---|
{user_id} | The unique identifier of the current user |
{tenant_id} | The tenant the user belongs to |
{role} | The user's assigned role |
Custom variables
Any key defined in a user's security properties is available
as a template variable. For example, if a user has the
property department: "engineering", you can use
{department} in your filter:
row_filter_rules:
- table_name: projects
filter_sql: "department = '{department}'"See User Security Properties for how to configure properties on user profiles.
Filtering with existing WHERE clauses
When a query already contains a WHERE clause, the row filter is combined with it using AND. Both conditions must be true for a row to appear in the results.
SELECT * FROM orders WHERE status = 'shipped'SELECT * FROM orders WHERE status = 'shipped' AND tenant_id = 'acme'The user sees only shipped orders belonging to their tenant.
Missing template variables
If a filter_sql references a template variable that
doesn't exist in the user's security properties, the query
is denied entirely. Kaelio injects a 1 = 0 condition
so that no rows are returned. This fail-closed behavior
prevents accidental full-table access when a user profile is
misconfigured or incomplete.
For example, if a row filter uses {region} but the user
has no region property set, any query touching that table
returns zero rows. This is intentional — it's safer to deny
access than to show all rows because a filter can't be
applied.
Make sure all users who need access to a filtered table have the required properties configured in their profile. See User Security Properties for setup instructions.
Filtering across JOINs
Row filters are applied to each table individually, even when tables are joined. Kaelio qualifies the filter with the correct table alias so that each table gets its own independent filter.
row_filter_rules:
- table_name: orders
filter_sql: "tenant_id = '{tenant_id}'"
- table_name: customers
filter_sql: "tenant_id = '{tenant_id}'"SELECT o.id, c.name
FROM orders o
JOIN customers c ON o.customer_id = c.idSELECT o.id, c.name
FROM orders o
JOIN customers c ON o.customer_id = c.id
WHERE o.tenant_id = 'acme' AND c.tenant_id = 'acme'Both tables are independently filtered. A user can't bypass their tenant filter by joining to an unfiltered table.
Rule matching priority
When multiple row filter rules could match a table, the first matching rule wins. The matching order follows the same precedence as table rules:
- Exact name —
ordersmatches only theorderstable - Specific glob —
sales_*matches tables starting withsales_ - Wildcard —
*matches all tables
Only the first matching rule applies to a given table. If you need different filters for different user groups on the same table, use conditional rules (see below).
Conditional row filters
Add a condition block to apply a row filter only when the
user's security properties match. This lets you define
different filters for different roles or departments.
row_filter_rules:
- table_name: support_tickets
filter_sql: "assigned_to = '{user_id}'"
condition:
role: "agent"
- table_name: support_tickets
filter_sql: "department = '{department}'"
condition:
role: "manager"In this example:
- Agents see only tickets assigned to them
- Managers see all tickets in their department
- Users with other roles have no row filter rule match for
support_tickets, so they see all rows (unless a wildcard rule covers it)
When a condition has multiple keys, all keys must match (AND logic). If the user doesn't have a required property, the condition doesn't match and the rule is skipped.
Using glob patterns
Use glob patterns in table_name to apply a row filter
across many tables at once. The * wildcard is especially
useful for enforcing tenant isolation across your entire
database.
row_filter_rules:
- table_name: "*"
filter_sql: "tenant_id = '{tenant_id}'"This adds a tenant_id filter to every table in every query.
If some tables don't have a tenant_id column, you can
override the wildcard with more specific rules:
row_filter_rules:
- table_name: public_settings
filter_sql: "1 = 1"
- table_name: "*"
filter_sql: "tenant_id = '{tenant_id}'"Because exact names take priority over wildcards,
public_settings gets the passthrough filter 1 = 1 (no
restriction), while all other tables are filtered by tenant.
Common patterns
Multi-tenant isolation
Ensure every user only sees data belonging to their organization:
row_filter_rules:
- table_name: "*"
filter_sql: "tenant_id = '{tenant_id}'"Department scoping
Restrict tables to show only rows relevant to a user's department:
row_filter_rules:
- table_name: expenses
filter_sql: "department = '{department}'"
- table_name: headcount
filter_sql: "department = '{department}'"
- table_name: budget_allocations
filter_sql: "department = '{department}'"Regional access control
Limit data visibility based on the user's geographic region:
row_filter_rules:
- table_name: sales
filter_sql: "region = '{region}'"
- table_name: warehouses
filter_sql: "region = '{region}'"
- table_name: logistics
filter_sql: "region = '{region}'"Related
- Policy YAML Reference — full schema for glob patterns, conditions, and all policy fields
- User Security Properties — configure the user attributes used in template variables
- Table Access Rules — control which tables users can query
- Column Filtering Rules — hide sensitive columns from query results
Docs