Row-level security is one of those features that sounds like an afterthought and is actually load-bearing. The idea is simple: instead of building a separate report for every region, team, or client, you build one report, and Power BI shows each viewer only the rows they're allowed to see. The German sales manager opens it and sees Germany; the French one opens the same report and sees France; the global head sees everything. One report, many truths, each correctly scoped. It's elegant, it's powerful, and it's also precisely the feature where a small, quiet mistake means someone sees numbers they absolutely should not — a salary, a rival region's figures, a client's data shown to another client. So it's worth doing properly, and worth checking you did, because the failure mode here isn't a broken report. It's a silent leak.

How it actually works

Row-level security in Power BI has two parts that people conflate and shouldn't. First, roles, which are filters: you define a role and give it a DAX rule that restricts a table to certain rows. A "Germany" role might carry the filter [Country] = "Germany" on your sales table. Second, membership: you assign actual people (or, better, groups) to those roles in the Power BI service. The role decides what the filter is; the membership decides who gets it. Both halves have to be right, and they fail in different ways.

The static approach — one role per region, each with a hardcoded filter — is easy to understand and fine when the groupings are few and stable. It gets unwieldy fast, though: fifty regions means fifty roles to maintain, and every new one is a manual change.

Dynamic RLS: the pattern that scales

The approach that actually holds up at scale is dynamic row-level security, where a single role filters based on who the current user is. The key is the USERPRINCIPALNAME() function, which returns the logged-in user's identity. You build a small security table mapping each user to what they're allowed to see — user to region, say — relate it into your model, and write one role whose filter reads roughly:

[Country] =
LOOKUPVALUE(
    UserSecurity[Country],
    UserSecurity[Email], USERPRINCIPALNAME()
)

Now there's one role, not fifty, and access is managed as data in a table rather than as a pile of hardcoded DAX rules. Add a person to the security table and they get access; remove them and they don't. This is the version I reach for on anything real, because it turns a maintenance nightmare into a row in a table.

Where it quietly goes wrong

Here are the mistakes that cause the silent leak, in rough order of how often I've seen them bite:

  • The filter doesn't propagate. RLS filters flow along relationships, in their direction. If you filter a dimension but the relationship to the fact table doesn't carry the filter through — wrong direction, inactive relationship — the facts stay unfiltered and people see everything. Always test that the restriction actually reaches the numbers, not just the lookup table.
  • Bidirectional relationships punching holes. Bidirectional cross-filtering can route around your security filter in ways that are genuinely hard to reason about. Be extremely wary of them in a model that relies on RLS; they're a common cause of leaks that pass a casual glance.
  • Gaps in the security table. With dynamic RLS, a user missing from the mapping table typically sees nothing — which is the safe failure, but generates "the report is empty" tickets. The dangerous version is a default or a wildcard that accidentally grants more than intended. Fail closed, never open.
  • Forgetting that admins and creators bypass it. The person building the report often has permissions that skip RLS entirely, so it looks fine to them while being broken for everyone else. Testing as yourself proves nothing.

Always test with 'View as'

This is the discipline that saves you: Power BI has a "View as role" feature that lets you see the report exactly as a member of a given role — or as a specific user — would. Use it, every time, before you publish anything with RLS. Impersonate the German manager and confirm they see only Germany. Impersonate someone who should see nothing and confirm they do. Never trust that the filter works because the DAX looks right; verify that the numbers on the screen are correctly restricted for a real role. The gap between "the rule looks correct" and "the data is actually restricted" is exactly where leaks live, and "View as" is how you close it.

The mindset

Row-level security is where data modelling meets governance, and it deserves the seriousness of both. A dashboard with a wrong colour is an embarrassment; a dashboard that shows one client another client's revenue is an incident. So treat RLS not as a filter you add at the end but as a security control you design in and verify deliberately — prefer the dynamic, data-driven pattern so access is manageable, be paranoid about filter propagation, and always, always view the report as the people it's meant to restrict before you let it out. Done properly, it's one report doing the work of fifty, safely. Done carelessly, it's a leak you won't discover until someone sees what they shouldn't — and by then it's not a bug, it's a breach.