A naive static RLS setup will not scale or secure a multi-tenant Power BI workspace: if you try to assign roles or permissions per user, you’re signing up for unmanageable maintenance and inevitable leakage. By the end of this article, you’ll be able to implement a dynamic RLS pattern using USERPRINCIPALNAME() that enforces tenant isolation based on the signed-in user, with full awareness of the edge cases and hidden pitfalls that cause most RLS models to fail in production.
The Hidden Trap: Static RLS Roles Leak in Multi-Tenant Scenarios
It’s tempting to reach for static roles—one per customer or department—when you first implement RLS for a multi-tenant workspace. This breaks down fast: Power BI roles are defined at publish time, not per workspace user, and you can’t delegate role assignment to tenant admins. Worse, static roles make tenant onboarding a deployment task, not a data-driven one: if you have 100 customers, you have 100 roles to manage. This doesn’t just scale poorly—it exposes you to accidental cross-tenant access if a role is misassigned or if a user is added to multiple roles.
The fix is dynamic RLS: a single DAX filter that looks up the signed-in user at runtime and filters rows accordingly. This is where USERPRINCIPALNAME() comes in—but only if you wire your model, filters, and data structures for it.
Dynamic RLS with USERPRINCIPALNAME: The Core Pattern
The working pattern is to build a mapping table that links users (or better, tenants) to the tenant keys in your fact tables, and filter all user-facing data through this mapping at query time using DAX. The naive error is to use USERNAME() instead of USERPRINCIPALNAME(), or to model the relationships in a way that allows users to bypass filtering via report-level slicers or bidirectional relationships.
Why USERPRINCIPALNAME()—and Not USERNAME()—Wins in Power BI Service
USERPRINCIPALNAME() returns the UPN (usually the email address) of the signed-in user in the Power BI Service, matching what your identity provider actually manages. USERNAME()—a relic from on-premises Analysis Services—returns a different format in Power BI Service and can misalign with your user mapping table. Unless you’re targeting Analysis Services over Active Directory, always use USERPRINCIPALNAME() for dynamic RLS in the Service. This is not a preference; it’s a requirement for correct filtering.
Naive Filtering: What Goes Wrong
Suppose you try to implement dynamic RLS with a filter like this:
[UserTenantFilter] =
'TenantUsers'[UserEmail] = USERPRINCIPALNAME()
Applied as a filter on the TenantUsers table in the RLS role, this seems like it should restrict data to the current user. But if your TenantUsers table is not connected directly (via a one-to-many, single-direction relationship) to your fact tables, or if your model has ambiguous or bidirectional relationships, users may still see data outside their tenant—especially if slicers on the report allow it.
The Table Structure That Actually Enforces Tenant Isolation
To truly isolate tenants, you need a data model where every fact table is directly or indirectly filtered by a single, unambiguous path from a Tenant or User mapping table. Here’s what that looks like:
- Dim_Tenant: One row per tenant, with TenantKey as PK.
- TenantUsers: Many rows per tenant, with columns TenantKey and UserEmail (UPN format).
- Fact_Sales: Business data with TenantKey as FK.
Set up relationships:
- Dim_Tenant[PK:TenantKey] → TenantUsers[FK:TenantKey] (one-to-many, single direction)
- Dim_Tenant[PK:TenantKey] → Fact_Sales[FK:TenantKey] (one-to-many, single direction)
Do not create a relationship directly between TenantUsers and Fact_Sales. The TenantKey bridge via Dim_Tenant is essential if you want to centralize filtering and avoid ambiguous paths. All relationships should be single-direction from dimension to fact—bidirectional filtering introduces risk of data leakage, especially with RLS.
The Only Reliable RLS Filter Expression
The filter on the TenantUsers table should be set to:
[Tenant RLS Filter] =
TenantUsers[UserEmail] = USERPRINCIPALNAME()
This restricts the rows in TenantUsers to just those for the signed-in user. Because TenantUsers is linked to Dim_Tenant via TenantKey, and Dim_Tenant in turn filters Fact_Sales, the filter context propagates through the model, ensuring the user can only see data associated with their tenant.
If the model is built correctly, there is no need for complicated DAX in the measures. The model’s filter propagation does the work; the DAX expression is only for the RLS role definition.
Worked Example: Why Slicers and Bidirectional Relationships Break Security
Take a model with the structure above, but imagine you instead create a bidirectional relationship between TenantUsers and Fact_Sales (TenantUsers[UserEmail] to Fact_Sales[UserEmail], both as text), and you add a slicer for UserEmail on the report. The intent is to allow a user to see only their data, but in practice, here’s what can go wrong:
- Bidirectional filtering + slicers: If the relationship is bidirectional and a slicer is present, the user could select another user (even from another tenant) in the slicer. Because bidirectional filtering propagates both ways, and because the RLS filter only restricts TenantUsers, this can result in the fact table being filtered by the slicer’s selection—even if it’s outside the authenticated user’s own tenant. This is a classic RLS bypass.
- Ambiguous relationships: If TenantUsers and Dim_Tenant both relate to Fact_Sales, but with different keys (UserEmail and TenantKey), and at least one is bidirectional, Power BI will resolve filter context in a way that may not match your RLS intent. This ambiguity is undetectable until a user lands on the wrong data.
To see this in action, set up a role with the naive filter:
TenantUsers[UserEmail] = USERPRINCIPALNAME()
With the bidirectional relationship and UserEmail slicer, the user can select any UserEmail present in Fact_Sales—even those not mapped to their UPN in TenantUsers. The RLS filter only restricts TenantUsers, but the slicer and relationship allow Fact_Sales to be filtered directly, which is not secured by RLS. This is not a theoretical exploit—it’s a live security hole. The only defense is a single-direction relationship from Tenant dimension to fact, and slicers that do not expose cross-tenant fields.
Edge Cases: Workspace Members, DirectQuery, and Power BI Desktop
There are three ways dynamic RLS can fail quietly, and most documentation doesn’t surface these until a breach occurs:
- Workspace Admins and Members: In the Power BI Service, workspace admins and members can bypass RLS if they have build permissions or if they export data. RLS is not an OLS substitute; it only filters rows, not columns or metadata. Sensitive data should never be present in the model if workspace admins should not see it.
- DirectQuery Models: RLS in DirectQuery mode will push the filter predicate (the USERPRINCIPALNAME-based restriction) down to the source as a SQL WHERE clause. This is usually what you want, but if your source is not secured or if you use custom connectors, verify the predicate is actually pushed (using SQL Profiler or Query Diagnostics). Otherwise, RLS can be bypassed or silently ignored.
- Testing in Power BI Desktop: Dynamic RLS does not apply to the author in Desktop. You must use the “View as Role” feature with a specific UPN, but this is only a simulation: the actual UPN seen by USERPRINCIPALNAME() in Desktop is typically your local account or a test value. Always verify RLS in the deployed Service with a non-admin, non-owner test user.
Checklist: Bulletproofing Your Dynamic RLS Implementation
- Every fact table is filtered (directly or via dimensions) by a single, single-direction relationship path from a mapping table that includes USERPRINCIPALNAME().
- No bidirectional relationships exist between user/tenant mapping tables and fact tables.
- No slicers or visuals expose cross-tenant values (UserEmail, TenantKey) unless you have a business case and additional security controls.
- The RLS filter uses USERPRINCIPALNAME(), not USERNAME().
- Test RLS in the Service using accounts with the least privilege, not just as an admin or owner.
- For DirectQuery, verify the filter is pushed to the source using trace tools.
If any item above fails, cross-tenant data exposure is not just possible—it’s likely under user-driven report exploration.
What to Do Next
Dynamic RLS with USERPRINCIPALNAME() is the only way to scale secure, maintainable row-level security in a multi-tenant Power BI workspace. But for it to work, the model must strictly enforce single-directional relationships from a user/tenant mapping, and you must validate that slicers and workspace permissions can’t bypass filtering. If you inherit a model or a report, audit the relationship diagram and DAX role filters before assuming RLS is safe; most leaks are architectural, not DAX typos. The next step is to build a test tenant with representative users and verify, in the Service, that no cross-tenant access is possible—then automate your mapping table population as part of your tenant onboarding process.
