Decision and enforcement are separate
The design follows the standard split, which is what keeps the rule out of the client:
- The decision point answers, for a subject and a set of resources: denied, these row filters, these column masks.
- The enforcement point sits between compiling a query and executing it. It asks for the decision, and injects it into the plan.
Nothing downstream can opt out, because by the time a query executes the restriction is part of it.
How a policy is expressed
A policy binds an attribute - a logical name mapped to a physical column - to a value set, for a set of subjects.
Value sets are reusable allow-lists, and an entry can be:
- a static literal,
- a claim from the caller's token, resolved at decision time,
- or a per-subject value loaded in advance.
The middle one is what makes a single policy serve an organisation: "the region on the caller's token" is one policy, not one policy per region.
How policies combine
| Situation | Result |
|---|---|
| Several policies on the same attribute | Their values are OR-unioned |
| Policies on different attributes | AND-combined, as nested filters |
| An empty allowed set, with nulls not permitted | No rows, explicitly and not by accident |
Where the filter lands in the plan
Strictly below any aggregation. That is the detail that decides whether row security is real: filtering after a sum gives a correct-looking total computed over rows the caller may not see. Here the restriction applies before the aggregate, so a total is the total of what this person may see.
Fail-closed
If the decision cannot be reached, the request errors out. It does not log and continue, and it does not fall back to unrestricted.
Calls arriving without a subject - a service token, a trusted internal path - skip row security by construction, and the platform records that as a bypass event instead of leaving it indistinguishable from a normal query.
Freshness
Decisions are cached to keep repeated queries cheap, and every write to a policy bumps a version and invalidates the cached decisions that depended on it. A permission change does not wait for a cache to expire.
Auditability
Every decision, and every bypass, is written to a decision log, queryable and administrator-gated - so "why did this person see this row" and "what ran without row security" are both answerable after the fact. The platform-wide record is separate - see Audit.
Related
- Column masking - the second half of the decision: which values inside the rows a caller may read.
- Roles and grants - whether the object is visible at all, decided before any of this.
- Data engine - where the plan these filters are injected into is compiled and executed.
- Tenant isolation - the layer underneath, answering which organisation a query is even looking at.