advanced 3 min answer

Row-level security on the orders table restricts each regional analyst to their own region. An audit shows a Berlin analyst's dashboard returning French rows. The policy is present and correct on the table, the analyst's attributes are right, and the query in the dashboard selects from `orders_summary`. What happened?

rlsmaterialised-viewspolicy-bypassauditenforcement-point
Show the full answer Hide the answer

The first three things to check

  1. What object the query actually reads. orders_summary is not orders. Ask what created it and under whose identity it refreshes.
  2. The definer's rights. A view or materialised view built by a privileged owner typically evaluates with the owner's permissions, so the policy that filters the base table was applied to the owner, who sees everything. A single such object can expose all 27 regions at once.
  3. Every other copy. Extracts into a BI tool's own storage, a reverse-ETL push into a CRM, a scheduled CSV to a shared drive. Each is a path where the filter was evaluated once, by someone else, and then materialised.

The diagnosis

The policy was enforced at refresh time under the owner's identity, and the result was stored. Row-level security filters rows when the protected object is read. A materialised view reads it once, as its owner, and from then on serves pre-filtered — which means pre-unfiltered — rows to whoever queries the view.

This has been a documented property of definer's-rights views in relational engines since the 1990s, and it is why every mature engine now offers an invoker's-rights alternative. The correct answers are: propagate the policy to derived objects, or make the derived object carry the region column and apply its own policy, or refresh with invoker's rights where the engine supports it.

The misleading signal is that the policy tests pass. Query orders as the Berlin analyst and you get Berlin rows, every time. The control is working exactly as designed on the object it was designed for, which is why a control test that only exercises the base table reports green through the entire incident.

The fix, in order

  1. Revoke access to the derived objects until they are brought under policy. It is disruptive and it stops the exposure now.
  2. Enumerate the derived objects. Lineage metadata gives the list; without it, a search of view definitions and scheduled extracts does.
  3. Re-create the derived objects with the policy-bearing columns present and policies attached, or replace them with views that evaluate as the caller.
  4. Add a control test that queries each published object as a restricted test user, not just the base table. This is the test that would have caught it.

When this is the wrong model entirely

Enforcement close to the data costs a query-time policy evaluation on every read and is worth it where consumers query base tables directly. Choose differently when they do not: if most consumption is through extracts and pre-aggregated marts, row-level security on base tables is the wrong enforcement point. Enforce at the layer everybody actually reads — the semantic layer or the serving store — or accept that policy correctness depends on a per-object review that nobody will keep up. Choosing an enforcement point that matches the consumption pattern is a design decision, not a configuration detail.

Common weak answers

  • "Educate the team not to create views." Views are the product; the enforcement design has to survive them.
  • "Turn on column masking too." Additive controls on the same object do not address a path that bypasses the object.