# Row-level security and masking

Row-level security limits which rows of a table a user can see; column masking changes what they see in sensitive columns, for example showing only the last four digits of a card number.

> [!NOTE]
>
> **Before reading**
>
> [RBAC and ABAC](<https://wiki.4tm0.net/data/rbac-vs-abac.html>)

## Why it matters

Table-level access is often too coarse. A regional manager needs the customers table, but only their region. A support agent needs to find a customer, but not see full payment details. Without row and column controls, the platform either withholds useful data or copies it into many restricted versions, which then drift apart and multiply the places sensitive data lives.

## How it works

A row filter is a condition added to every query on a table, based on who is asking. If a manager's region is EU, the filter region = 'EU' is applied, and rows from other regions are invisible, as if they did not exist. Aggregates, joins and counts all see only the permitted rows.

A column mask is an expression applied to a column's values, based on who is asking. Common masks replace the value with null or a fixed string, show only part of it, hash it so it can still be joined or counted without being read, or reduce precision, such as a birth date shown as the year only. Users without the right attributes still query the column; they see the masked form.

Both are defined as policies and should be written against attributes and tags, as described for ABAC, so that one policy covers many tables. A policy that masks every column tagged pii covers new columns as soon as they are tagged.

The most important decision is where enforcement happens. There are several candidates:

- The query engine, such as Trino with a policy engine, rewrites each query with the filters and masks. Everything that queries through the engine is covered; anything that reads the files another way is not.
- The catalog can return, with a table's metadata, the filters and masks the engine must apply. In 2026 the Iceberg REST catalog specification added an optional read-restrictions object (a required row filter and required column projections) to the load-table response; as of October 2026 engines are only beginning to enforce it.
- The semantic layer or a data API can apply rules for the metrics and endpoints it serves.
- Views or separate tables per audience are the oldest approach: simple, but each copy is another place to maintain.

```
flowchart TB
  M[Manager, region EU] --> E[Query engine]
  S[Support agent] --> E
  E -->|asks| P[Policy engine:<br/>filters and masks<br/>for this user]
  E --> T[(clean_customers)]
  E -->|rows where region = EU,<br/>email masked, card shown as last 4| M2[Manager sees]
  E -->|"all regions, email visible,<br/>card shown as last 4"| S2[Agent sees]
```

Whatever is chosen, policies should be enforced in one place per path to the data, with every other path closed. If the engine applies masks but analysts can also read the Parquet files directly with storage credentials, the masks protect nothing. Credential vending from the catalog, and denying direct bucket access, close that path.

Masking in queries is different from removing data. The full values are still stored, and anyone with privileged access, backups or a misconfigured path can read them. For data that most uses never need in full, such as card numbers, it is better not to store the full value at all, or to tokenise it at ingestion.

## Key terms

- Row filter A condition that limits which rows a user sees.
- Column mask An expression that hides or reduces a column's values for a user.
- Dynamic masking Masking applied at query time, depending on the user.
- Enforcement point The component that applies filters and masks.
- Tokenisation Replacing a sensitive value with a token that can be reversed only by a separate, protected service.

## Example

In the shop, managers see only their region's customers, email is visible only to support, privacy and the CRM sync service, and only the payments team sees more than the last four digits of card\_token, the payment provider token copied from payments.

The query the engine effectively runs for a manager with region EU:

```
SELECT customer_id,
       name,
       CASE WHEN contains(current_groups(), 'support')
              OR contains(current_groups(), 'privacy')
              OR contains(current_groups(), 'crm-sync')
            THEN email
            ELSE NULL END AS email,
       region,
       CASE WHEN contains(current_groups(), 'payments')
            THEN card_token
            ELSE concat('**** ', substr(card_token, -4)) END AS card_token
FROM shop.clean_customers
WHERE region = 'EU';
```

The manager writes SELECT \* FROM shop.clean\_customers and gets EU rows with email masked and card\_token shown as its last four; the policy engine supplied the filter and both masks for this user. A support agent running the same query gets all regions, with email visible and card\_token still masked.

## Pitfalls

- Several enforcement points with different rules. Users see different data through different tools.
- Open side doors. Direct file access, exports and copies bypass engine policies.
- Masks that leak through other columns. Masking email does not help if a free-text notes column contains it.
- Hashing without a secret; see [PII and data classification](<https://wiki.4tm0.net/data/pii-and-classification.html>).
- Policies tied to column names. Write them against tags or attributes so new columns are covered automatically.
- Relying on masking for data you should not keep. Tokenise or drop it at ingestion.

## Learn more

- [Trino, Open Policy Agent access control (row filters and column masks)](<https://trino.io/docs/current/security/opa-access-control.html>)
- [Google Open Source Blog, Standardizing fine-grained access control in Apache Iceberg's REST catalog (October 2026)](<https://opensource.googleblog.com/2026/10/standardizing-fine-grained-access-control-in-apache-icebergs-rest-catalog.html>)

## Related

[Multi-tenancy](<https://wiki.4tm0.net/data/multi-tenancy.html>) [PII and data classification](<https://wiki.4tm0.net/data/pii-and-classification.html>) [Semantic layer](<https://wiki.4tm0.net/data/semantic-layer.html>) [Governance as code](<https://wiki.4tm0.net/data/governance-as-code.html>)
