Articles · Business intelligence · · 7 min read

Power BI Row-Level Security That Does Not Leak

Row-level security fails at the edges — filter direction, bridge tables, aggregations and composite models. The patterns that hold, and how to test them.

By Matthieu de La Mettrie · Founder, Skopura

Row-level security filters a model’s data by who is asking. That sentence is the whole promise, and it is also where the trouble starts: RLS is implemented as a filter on one table, and everything else about whether it holds depends on how filters move through the rest of your model. If you can draw the propagation path from the security table to every fact table in the model, you can reason about the boundary. If you cannot, you are shipping a guess.

What actually leaks when RLS fails?

Rarely a full table dump. The failure modes are quieter than that.

The first is existence disclosure. A slicer bound to a dimension that sits outside the filtered path lists every customer, every project code, every cost centre — including the ones whose rows are correctly hidden. No fact rows leak. The names do. For a model covering acquisitions, tenders or headcount, the names are the sensitive part.

The second is aggregate bleed. A measure resolves against a table the security filter never reaches — an aggregation table, a summary fact loaded separately, a snapshot built for performance. Detail rows are filtered, the total is not, and the gap between them is itself information.

The third is blank-looks-like-broken. A user whose mapping row is missing sees empty visuals and raises a ticket. Someone with model edit rights reproduces it, sees data because RLS does not constrain them, and closes it as not reproducible. Now nobody trusts the report and nobody knows why.

All three are design faults, not bugs. They are visible on the model diagram before anyone opens a report.

Where does the filter stop propagating?

RLS applies a DAX predicate to one or more tables. From there it travels along relationships in the direction those relationships allow. A star schema with single-direction relationships from dimension to fact propagates cleanly in one direction and not the other — which is exactly what you want when the filter is applied at the dimension, and useless when it is applied at the fact.

filters

propagates

propagates

no relationship

not reachable

USERPRINCIPALNAME()

Security mapping table

Dimension: Customer

Fact: Sales

Fact: Forecast

Dimension: Product owner

Aggregation table

Unsecured surface

Filter path from identity to fact. Every table not reachable from the security table is unsecured by construction.Concept diagram — logical propagation only

The dotted edges are the audit. Any table that cannot be reached from the security table by following relationships in their permitted direction is, by construction, unsecured. Write that list down. For each entry, decide one of three things: relate it to the spine, secure it with its own predicate, or remove it from the model. “It only has reference data in it” is a decision too, but make it explicitly and record why.

Static roles or dynamic mapping?

Static roles hard-code the predicate — a role per region, per business unit, per legal entity. Dynamic RLS evaluates the current user against a mapping table at query time. They fail differently.

Static rolesDynamic mapping
Where the rule livesIn the model definitionIn data, refreshed with the model
Adding an audienceModel change and republishInsert a mapping row
Failure when membership is staleUser sees another group's rowsUser sees nothing
AuditabilityRead the role predicatesQuery the mapping table
Blast radius of an errorScoped to one rolePotentially every user
Coupling to identity sourceWorkspace role assignmentUpstream directory or HR feed
How the two approaches behave under change and under audit.Qualitative comparison — no measurements

Dynamic mapping fails closed when a row is missing, which is the right default, and fails open when the mapping pipeline writes the wrong keys, which is the wrong default and is much harder to notice. Treat the mapping table as production data with its own tests, not as a lookup someone maintains in a spreadsheet.

What does a deny-by-default predicate look like?

The predicate should be written so that an unmatched identity resolves to an empty set rather than an unfiltered one. Beware of expressions where a missing mapping collapses to a condition that is trivially true.

Dynamic RLS predicate on the customer dimensiondax
1234567891011121314
// Applied to: Dim Customer
VAR CurrentUser = USERPRINCIPALNAME()
VAR Allowed =
  CALCULATETABLE(
      VALUES( 'Security Mapping'[CustomerKey] ),
      'Security Mapping'[UserPrincipalName] = CurrentUser,
      'Security Mapping'[IsActive] = TRUE()
  )
RETURN
  'Dim Customer'[CustomerKey] IN Allowed

// A hierarchy variant, where the mapping stores a node
// and access flows down the path:
// PATHCONTAINS( 'Dim Org'[OrgPath], LOOKUPVALUE( ... ) )

Two details matter here. Calculated columns are evaluated at refresh, outside any user context, so a column that encodes access decisions is computed once for everyone — put the logic in the predicate or in a measure, not in a calculated column. And security filters are not removable by ALL or REMOVEFILTERS inside a measure, which is a genuine safety property: a report author cannot write their way around the boundary. The boundary is broken by model topology, not by DAX.

How do you test a boundary you cannot see?

“View as role” is a smoke test, not a test. It checks the predicate compiles and returns something; it does not check that the something is correct, and it does not exercise the propagation path through every table.

Build a test fixture instead. Take a handful of representative identities — one with a single mapping, one with several, one with a hierarchy node, one with no mapping at all — and define, outside the model, the set of keys each should be able to see. Then evaluate the model as each identity and compare returned key sets against expected key sets, per fact table, per dimension that backs a slicer. Key sets, not totals: two different sets can sum to the same number.

Run this on every model change, not only when the RLS logic changes. The cases that break RLS are usually unrelated edits — a new relationship set to both-directions to make a slicer behave, an aggregation table added for a slow page, a composite model extended to a source with fixed credentials. None of those arrive labelled as a security change.

The no-mapping identity is the most valuable fixture you have. It is the only one that distinguishes “correctly sees nothing” from “model is broken”, and it is the one that catches a predicate that quietly fails open.

When is RLS the wrong boundary entirely?

RLS hides rows. It does not hide the shape of the model: measure names, column names, table structure and the fact that a measure exists at all remain visible. If the existence of a measure called Redundancy Provision is itself the disclosure, row filtering is not the control you need — object-level security or a separate model is.

Where should the audience boundary live?

No boundary needed — one model, one audience

Splitting models has a real cost: metric definitions drift the moment they are maintained twice. If you split, keep the definitions upstream — in the transformation layer, in dbt models or in a shared Fabric or Databricks layer — so the split happens at the presentation boundary and not at the definition boundary. A semantic layer centralises metric definitions so separate reports agree; that property is worth preserving even when the models themselves must diverge.

What belongs in the review?

Treat the security topology as part of the model contract, reviewed on the same cadence as the measures. Three artefacts make that practical. First, the unreachable-table list described above, regenerated whenever relationships change. Second, the identity fixtures and their expected key sets, held in version control next to the model definition. Third, a short record of which tables are deliberately unsecured and why.

None of this is exotic. It is the same discipline you would apply to any other contract between a system and its consumers — except that when this one fails, nobody files a bug, because the report looks exactly as it should.

power-bi · row-level-security · semantic-model · dax

Share on LinkedIn ↗

Work with Skopura

Facing this bottleneck in your own estate?

Skopura builds production-grade lakehouse pipelines, evaluation harnesses, and provides fractional data leadership. Thirty-minute call, no deck.

Book a 30-min call →Email directly

Further reading

Business intelligence · 7 min

Dashboard Decay and the Review Habit

Reports rot quietly: definitions drift, audiences move on, copies multiply. A concrete review habit that keeps a reporting estate honest without a rebuild.

Business intelligence · 9 min

Star-Schema Decisions That Age Badly

A star schema rarely breaks. It drifts — grain, conformance, keys, history and partition boundaries all encode assumptions that quietly expire.