Row-Level Security
Row-level security (RLS) controls which rows a user can see in query results. After a query passes through RBAC, ABAC, and query-level security, RLS applies a final filter to every row in the result set. Rows that fall outside the user's permitted scope are handled according to the configured filter type.
How RLS Works
RLS rules are defined on a per-dataset or per-field basis. Each rule specifies:
- A condition that determines which rows the user is authorized to see.
- A filter type that determines how unauthorized rows are handled.
Filter Types
Datafi supports four RLS filter types. Each serves a different use case depending on how much information you want to reveal about the existence or shape of unauthorized data.
| Filter Type | Behavior | Output Example | Use Case |
|---|---|---|---|
| Skip | Omits the row entirely from results | Row not present | The user should not know the row exists |
| Blank | Returns the row with empty string values for restricted fields | "" | The user can see the row exists but not its content |
| Mask | Returns the row with asterisk-masked values | "***" | The user should know data exists but cannot read it |
| Random | Returns the row with random character values | "xK9mQ2" | Preserve data shape for testing without revealing real values |
Skip
The Skip filter removes unauthorized rows from the result set entirely. The user has no indication that the row exists.
{
"filter_type": "Skip",
"condition": {
"field": "region",
"operator": "eq",
"value": "{{user.region}}"
}
}
Example result:
| id | name | region |
|---|---|---|
| 1 | Alice | us-west |
| 3 | Carol | us-west |
Rows for other regions are silently omitted.
Use Skip when the existence of a row is itself sensitive information, such as records related to ongoing investigations or embargoed data.
Blank
The Blank filter returns all rows but replaces restricted field values with empty strings.
{
"filter_type": "Blank",
"condition": {
"field": "department",
"operator": "eq",
"value": "{{user.department}}"
},
"apply_to": ["salary", "bonus"]
}
Example result for a user in the engineering department:
| id | name | department | salary | bonus |
|---|---|---|---|---|
| 1 | Alice | engineering | 120000 | 15000 |
| 2 | Bob | finance | ||
| 3 | Carol | engineering | 115000 | 12000 |
Mask
The Mask filter replaces restricted field values with a fixed mask string, typically ***.
{
"filter_type": "Mask",
"condition": {
"field": "clearance",
"operator": "gte",
"value": "{{user.clearance}}"
},
"apply_to": ["ssn", "dob"],
"mask_char": "*",
"mask_length": 3
}
Example result:
| id | name | ssn | dob |
|---|---|---|---|
| 1 | Alice | *** | *** |
| 2 | Bob | 123-45-6789 | 1990-01-15 |
Mask is the most commonly used filter type for PII fields. It clearly communicates that data exists but is protected.
Random
The Random filter replaces restricted values with randomly generated characters that match the original data's length and type.
{
"filter_type": "Random",
"condition": {
"field": "project",
"operator": "in",
"value": "{{user.projects}}"
},
"apply_to": ["customer_name", "account_id"]
}
Example result:
| id | customer_name | account_id |
|---|---|---|
| 1 | xK9mQpLw | 7829461 |
| 2 | Acme Corp | 1234567 |
Random values are regenerated on each query -- the same row will produce different random values across requests.
RLS Conditions
RLS conditions use dynamic variables resolved at query time. You can reference user attributes using the {{user.*}} syntax.
| Variable | Description | Example |
|---|---|---|
{{user.id}} | The authenticated user's ID | usr_abc123 |
{{user.region}} | The user's assigned region | us-west |
{{user.department}} | The user's department | engineering |
{{user.clearance}} | The user's clearance level | 3 |
{{user.projects}} | List of projects the user belongs to | ["alpha", "beta"] |
{{user.tenant_id}} | The user's tenant identifier | tenant_001 |
Condition Operators
| Operator | Description |
|---|---|
eq | Equals |
neq | Not equals |
gt / gte | Greater than / greater than or equal |
lt / lte | Less than / less than or equal |
in | Value is in a list |
contains | Field contains substring |
Combining RLS with Other Layers
RLS operates as the final governance layer. It does not replace RBAC, ABAC, or query-level security -- it complements them.
Best Practices
- Choose the right filter type for the sensitivity level. Use Skip for highly sensitive rows, Mask for PII fields, and Blank or Random when the data shape matters more than the content.
- Avoid overly complex conditions. Keep RLS conditions simple and testable. Complex nested conditions are harder to audit.
- Test with multiple user profiles. Verify that each user role sees the expected subset of rows.
- Document your RLS rules. Maintain a record of which datasets have RLS applied and what conditions are in effect.