Classification & masking
Per-column data classification, mask strategies, row filters, and the Rego-backed policy engine that enforces them.
Every managed table carries a data classification at the table level and at the column level. Combined with mask strategies and row filters, this gives you fine-grained control over which user — or which workflow — sees which data.
Three layers of governance
| Layer | What it does | Granularity |
|---|---|---|
| Table classification | Marks the entire table at a sensitivity level — public, internal, confidential, restricted. | Per table |
| Column classification + mask strategy | Marks individual columns and decides whether they're shown, redacted, hashed, or denied per role. | Per column per role |
| Row filter | A Rego predicate that restricts which rows a caller can see (e.g. "only rows where org_unit matches the caller's department"). | Per row per caller |
All three are evaluated together at read time. There is no path to the data that bypasses them.
Table classification
Choose at table creation; can be changed by a workspace admin. The classifications map to who can use the table at all:
| Classification | Who can read the table |
|---|---|
| public | Any workspace member |
| internal | Workspace members with the data:read-internal grant (default for members) |
| confidential | Workspace admins and above |
| restricted | Workspace owners and org owners only |
Higher classifications also affect default mask strategies on the columns — see below.
Column classification & mask strategies
Each column can declare a classification of its own. When set, the column's effective sensitivity is the max of its own classification and the table's. A column-level restricted column inside an internal table is still treated as restricted.
For each (column, role) pair, you can declare a mask strategy:
| Strategy | What the role sees |
|---|---|
clear | The actual value |
redact | A fixed mask string ([REDACTED]) |
hash | A deterministic hash — useful for joining masked data across queries |
null | A NULL value as if the column didn't exist for them |
deny | The query fails entirely with a permission error |
Common patterns:
| Column type | Workspace member | Workspace admin | Workspace owner |
|---|---|---|---|
| PII (name, email) | redact | clear | clear |
| Salary / financials | null | null | clear |
| Internal IDs | clear | clear | clear |
| External secrets | deny | deny | clear |
Row filters
A row filter is a Rego predicate that decides whether a row is visible to a caller. Filters are written per role and per table. Example concepts:
- Tenant scope —
row.org_unit == caller.org_unit - Region scope —
row.region in caller.allowed_regions - Project scope —
row.project_id in caller.project_grants
The filter runs as part of the policy decision at read time. Rows that fail the filter are excluded from the result set; the caller has no signal that excluded rows existed at all.
The policy decision point
Classifications, masks, and filters are compiled into a per-tenant Rego bundle by the platform. Every read against a managed table evaluates that bundle. The bundle is rebuilt when:
- A classification is changed.
- A mask strategy is added or modified.
- A row filter is added or modified.
- A role definition changes.
Rebuilds are fast and apply on the next read; there is no waiting period.
Every read is bound to the compiled bundle. The platform sends the hash of the environment's active bundle with each read, and the decision point permits only when the replica that evaluated it holds exactly that bundle. A replica that has not received the latest compile — after a restart, for example — answers 503 AUTHZ_UNAVAILABLE for that environment's reads until the platform republishes to it (within seconds), rather than evaluating the environment as if it had no policies. An environment that has never had a column policy is evaluated as allow-all, and that is the only case in which it is.
OPA is the policy decision point. It is a hard dependency: when it cannot be reached, or when it does not hold the environment's compiled bundle, reads fail closed rather than falling back to a weaker engine.
Audit
Every read against a managed table emits an RESOURCE_ACCESS event in the audit log with:
- The actor and their resolved role.
- The table ID and the columns requested.
- The row count returned (not the rows themselves).
- The mask strategies and row filters that applied.
For compliance audits, this answers "who saw what column when" without exposing the data itself.
Related
- Managed tables — table creation and lifecycle.
- Security → Authorization — the broader authorisation model these policies live inside.
- Audit logging — what gets recorded on every read.
- Compliance → GDPR — how column masking maps to data-minimisation requirements.