Skip to main content

Winner of the Embedded Analytics Solution of the Year at the Data Breakthrough Awards 2026

Back to blog list

Power BI Row-Level Security: How to Set It Up, and Where It Stops

Power BI row-level security filters which rows each viewer sees: define roles and filter rules in Desktop, assign members in the Service, and test. When viewers are your customers, your application supplies the identity in the embed token, so test that path with real tokens.

Summarize with:

What Is Row-Level Security in Power BI?

Filtering rows is the easy half. The hard half is who vouches for the viewer: Microsoft Entra ID (formerly Azure Active Directory), or your own code.

Power BI row-level security (RLS) is a mechanism that restricts which table rows a viewer can see, using roles with data analysis expressions (DAX) filter rules defined in the semantic model and applied at query time. Roles are created in Power BI Desktop (or, for imported models, the Power BI Service), published with the model, and enforced for users with Viewer permissions. Role membership, assigned to users or Microsoft Entra ID security groups in the Service, decides which rows each viewer gets.

Static RLS hard-codes a filter value per role, while dynamic RLS uses USERPRINCIPALNAME() against a user-to-segment mapping table so one role serves everyone. In app-owns-data embedding, the viewer's identity comes from the EffectiveIdentity passed in the embed token instead.

It's a staple of business intelligence (BI), and Microsoft's row-level security documentation covers it thoroughly, embedded cases included. Yet the canonical worked examples split sales rows by region for staff. They all answer one question: which rows may this signed-in user see? And that user always signs in to Power BI, as an employee or a B2B guest. We'll start there. The role mechanics carry over to customers; the identity behind them doesn't.

How to Define Roles and DAX Rules in Power BI Desktop — And Test Them Before You Publish

You define the role and its filter in Power BI Desktop, and you check it there before anything is published. Microsoft's workflow runs: define roles and rules in Desktop using data analysis expressions (DAX), publish, add members in the Service, then validate. The Desktop half looks like this:

  1. Open the role dialog. Per Microsoft Learn, "From the Modeling tab, select Manage Roles." Create a role and name it for what it restricts, such as West Sales.
  2. Pick the table the rule belongs on. Choose the dimension table that holds the segment (usually Region or Customer), not the fact table. The filter reaches the facts through your model's relationships.
  3. Type the DAX filter rule. For a static role: [Region] = "West". Rows where the expression is true stay visible. Every other row is filtered out of every query that role runs.
  4. Validate with View as role. On the Modeling tab, select View as role and tick the role. Then check every visual, including totals, and confirm that only West rows come back.

That check proves the DAX is right. It doesn't prove anything about who the viewer is. Microsoft says it plainly on the Fabric RLS page: "Test as role simulates role membership but doesn't fully replicate the authentication context of another user, especially for B2B guests or embedded scenarios."

So our rule is simple. Use View/Test as role to validate the model logic, then test each real path on its own. B2B guests sign in through Microsoft Entra ID, and Learn's advice for them is "Always validate with the real external guest account." App-owns-data customers arrive through an embed token instead, and Learn notes that Test as role "does not simulate embedded authentication flows." Test them with real embed tokens that carry an EffectiveIdentity.

The rule isn't Desktop-only, either. According to Learn, "You can configure RLS for imported semantic models in Power BI Desktop or the Power BI service." Every edit also ships with the model, because "When you publish to Power BI, you also publish the role definitions."

Engineering leads usually ask us whose job this is. There are three owners:

  • The rule: whoever owns the semantic model.
  • Membership: anyone with Contributor or higher on the workspace. Learn states that such users "see the Security option and can assign users to a role." It adds: "You can't assign users to a role within Power BI Desktop." Membership also covers only colleagues who sign in to Power BI.
  • The identity in an embed token: your backend developers, in ordinary server code.

Membership can't be scripted through Fabric's API either. Learn's Power BI Desktop projects page notes that "It's not possible to get and set Row-level security role members using Fabric REST API."

That's three owners for one boundary, and no single pull request shows all three.

How to Assign Users and Entra ID Groups in the Power BI Service

Role members are assigned in the Power BI Service, never in Desktop. Publishing the semantic model is what turns enforcement on.

  1. Publish the semantic model and report to the workspace.
  2. On the semantic model, open Security (visible to Contributor or higher).
  3. Add users or Microsoft Entra ID (formerly Azure Active Directory) security groups to each role.
  4. Re-check each role with Test as role.

Keep these facts separate:

  • Enforcement starts at publish. A Viewer in no role typically sees empty results because, in Microsoft's words, "RLS is enforced but no matching role is applied" (Microsoft Learn).
  • Membership decides which rows. It maps each viewer to a role's filter.
  • Workspace Admin, Member and Contributor roles aren't subject to RLS. Give report consumers Viewer (Microsoft Learn).
  • Membership doesn't affect an app-owns-data embed token. The token states its own identity (covered below).

We'd assign groups rather than individuals almost every time. People join, change teams and leave, and each change is a Service-side edit that no pull request shows. A group moves that churn into Entra ID, where your identity team already manages it. It also keeps the role list short enough to re-test properly.

So role definition and role membership live in two different products, and both halves get verified in the Service.

Static vs Dynamic RLS: Which Should You Use?

A sales team adds a new region most weeks. Under static row-level security (RLS), each one means someone opens the semantic model, creates a role such as Region_Nordics with the rule [Region] = "Nordics", republishes, and then someone with Contributor access assigns members in the Service. One week, nobody does. On Monday the Nordics team opens the report to empty visuals. Nothing errors, and the first sign of trouble is a confused email.

That's the static trade-off. One hard-coded role per segment is easy to read and easy to audit when you have four regions that haven't changed in years. It gets fragile once segments or people churn, because every change becomes a model edit plus a membership change, often made by two different people. We think the tipping point is less about how many roles you have and more about how fast they change. If segments or membership move weekly, hand-maintained roles will fall behind.

Dynamic RLS replaces the pile of roles with one role and a user-to-segment mapping table:

UserEmailRegion
ana@contoso.comNordics
raj@contoso.comDACH

Put the data analysis expressions (DAX) rule on the Region dimension so it flows to the fact table through the existing relationship:

[Region] IN
    CALCULATETABLE (
        VALUES ( UserRegion[Region] ),
        UserRegion[UserEmail] = USERPRINCIPALNAME ()
    )

Onboarding a region or a person is now a row in a table, not a republish.

The pattern rests on one assumption: that USERPRINCIPALNAME() returns the viewer. For colleagues it does. Microsoft Learn states that in the Power BI service both USERNAME() and USERPRINCIPALNAME() return the signed-in user's User Principal Name (UPN).

That assumption breaks in app-owns-data embedding. When your application authenticates with a service principal, Microsoft documents that the same functions return the service principal's application ID or an empty string, not your end user (Microsoft Learn). The mapping table then has nothing correct to match.

So here's the conclusion. Use static roles for a handful of stable segments, and switch to a mapping table once segments or people change often. But a mapping table alone doesn't isolate an embedded customer. Under a service principal, the same dynamic role filters per customer only when your application passes an EffectiveIdentity in the embed token.

What Power BI RLS Does Not Cover

Row-level security (RLS) has a documented boundary. The mistakes that cause trouble tend to sit just outside it.

Start with workspace roles. Microsoft's RLS documentation states that RLS applies to users with Viewer permissions. Anyone holding Admin, Member or Contributor in the workspace sees every row, whatever role they're mapped to. So a well-meant "give the account team Contributor so they can fix a visual" quietly removes their filter. We treat workspace access as part of the security design, not an admin chore: keep report consumers on Viewer, or share through an app.

Relationships are the second edge. A security filter follows each relationship's filter direction. On a bi-directional relationship, it only crosses back if you select Apply security filter in both directions. Microsoft's RLS documentation warns that this setting can slow queries, so test it before you rely on it. Miss it, and a table you expected to be filtered can come back whole.

Third, RLS filters rows. It won't hide a salary column or a whole table. That's object-level security (OLS), a separate feature with its own rules. Both live in the semantic model rather than in report visuals, so they also govern paths like Analyze in Excel (Microsoft Learn). That only helps if the model itself is right.

Our view is blunt. A model that ignores workspace roles, cross-filter direction or the row-versus-column distinction will leak data while every role still looks correct in testing.

Nothing errors. The rows simply arrive.

How to Verify Role Coverage as Roles Multiply

An over-permissive role doesn't throw an error. It just returns more rows than it should, and every visual renders normally.

That's why we treat coverage as a routine rather than a build step. Re-run it whenever a role is added, a table joins the model, or a relationship changes, because each of those can widen what an existing role sees. In our experience the leak rarely sits in the DAX filter rule someone reviewed; it sits in the table nobody wrote a rule for. The checks are cheap. Skipping them isn't.

  • Every table, every role. For each role, list the tables and confirm that each one is either filtered by its own rule or reached by a filter propagating from a filtered table. A table with neither comes back unfiltered.
  • Every relationship the filter must cross. When the security filter has to travel against a relationship's single cross-filter direction (typically through a bridge or many-to-many table), it stops there. It continues only if the relationship cross-filters in both directions and has Apply security filter in both directions enabled. Otherwise the fact table returns every row.
  • Model logic, in Desktop and the Service. View as role and Test as role re-check the rules. They don't re-check the viewer's identity: Microsoft notes that Test as role "uses your own identity when evaluating dynamic RLS expressions".
  • Real identities, separately. Re-check B2B guests by signing in with the real guest account, and service-principal embeds with real embed tokens that carry an EffectiveIdentity.

A role that returns too many rows fails silently. Re-prove coverage every time the model or a relationship changes. A check at build time doesn't cover later changes.

What Changes When the Viewer Is Your Customer, Not Your Colleague

Put the mapping-table role behind an app that embeds with a service principal, and Microsoft says USERPRINCIPALNAME() and USERNAME() now "return the service principal's application ID or an empty string—not an end user's identity" (Microsoft Learn). Microsoft's instruction for this case is direct: "Pass the EffectiveIdentity object with the appropriate username and roles when generating an embed token."

Your customer signs in to your product, not to Power BI. Microsoft Entra ID has never heard of her.

Here's how identity reaches the semantic model in the "embed for your customers" (app-owns-data) setup. Your backend checks the customer's session, looks up which tenant she belongs to, and requests an embed token carrying an effective identity. That identity contains a username, one or more roles and the dataset, and it can optionally carry a customData string that your rules read with CUSTOMDATA().

Microsoft's embedded RLS documentation spells out what happens next: "If the roles are dynamic, the username string is used as the filter." Its worked example defines a role as [CountryRegionCode] = username(), passes France as the username, and notes that "The region name is used as the effective identity."

We'd underline that last point. The username doesn't have to be a person. It's a string your application chooses, so a tenant ID works as well as an email address, and the dynamic rule filters on it exactly as it would filter on a signed-in user's name.

Your application now owns three jobs. It looks up the right identity for each request. It mints the token server-side, never in the browser. It keeps token lifetimes short.

So ordinary dynamic RLS doesn't carry over unchanged to customers. Under a service principal, per-customer filtering depends on the EffectiveIdentity your application passes, and that username is the string the filter runs on. RLS isn't replaced. The job of vouching for the viewer moves from Microsoft Entra ID to your code.

What Changes When Your Application Vouches for the Viewer: The Failure Modes of Embed-Token RLS

Embed-token RLS is a documented, supported way to carry a tenant boundary in Power BI. The rule still holds. What changes is who answers the question of who's looking.

For a colleague, Microsoft Entra ID answers it, and none of your code sits in that path. For a customer, your backend answers it, and Power BI accepts the identity it's handed. So the failures we worry about aren't in the DAX. They're in the few lines that build the token.

A common failure mode looks like this. The semantic model is shared across tenants, the rule filters on the username, and the backend reads the tenant ID from a query parameter instead of the server-side session. Someone edits the URL, or a token cache keyed on the wrong value serves one session's token to another, or a lookup simply returns the wrong tenant. Every role still tests perfectly, and one customer is looking at another customer's rows. That isn't a Power BI defect; it's what follows from Power BI trusting the identity your application supplies.

A missing identity behaves differently depending on how you authenticate. Microsoft is explicit: if you omit the username and role, "For a service principal, token generation fails." For a master user, "token generation succeeds but the data isn't filtered (all the data is returned)." One fails closed; the other fails wide open.

Service-side membership won't catch it either: "Assigning users to roles within the Power BI service doesn't affect RLS or OLS when using an embed token (App owns data scenario only)." The token alone decides which rows come back.

Our view is that the identity path must be narrow, server-side, tested per tenant and auditable, with tokens kept short-lived and scoped. View as role in Desktop never exercises it; Microsoft's instruction is to "always test with actual embed tokens that include EffectiveIdentity to verify that RLS filters are applied correctly." For your most sensitive tenants, workspace isolation limits the blast radius of the bug you haven't found yet.

Choosing Your RLS Design: Static, Dynamic, Embed-Token Identity, or Workspace Isolation

There are four designs, and one question sits under all of them: who vouches for the viewer? The scale column below draws on Microsoft's RLS guidance, query caching page and multitenancy documentation. The fourth row is Microsoft's own "workspace based isolation with service principal profiles" (security overview).

DesignWhere the rule livesWho owns it / where editedWho supplies identityNew tenant addedWhat gives way at scaleWrong identity exposes
Static RLSSemantic model, one role per segmentModel owner (Desktop or Service); Contributor or higher assigns members in the ServiceMicrosoft Entra ID sign-inNew role, republish, assign membersFilters apply to "every DAX query"; where query caching runs (Premium or Embedded, Import models only), "Cached query results are specific to user and semantic model context"That role's segment
Dynamic RLSSemantic model plus mapping tableModel owner; mapping table's data ownerEntra ID, via USERPRINCIPALNAME()Add mapping rowsSame per-query filters and per-user cacheWhatever the bad mapping row grants
Embed-token RLSSemantic model role filtering on username()Model owner for the rule; your backend developers for the tokenYour application, via EffectiveIdentityAdd the tenant to your lookup; no republishSame query and cache costs; one shared model bounded by capacity SKU memoryAnother customer's rows
Workspace isolationA semantic model per customer; RLS only within a customer, if at allYour provisioning code, via service principal profilesYour applicationProvision workspace, model, reportModel size and parallel refreshes set by the SKU (10 GB+ needs Large semantic models); a service principal can have up to 100,000 profiles, and each profile can access or create up to 1,000 workspaces (multitenancy documentation)An entire other customer's workspace

So when does a tenant deserve its own workspace and model? We'd say when its data volume pushes against what a shared model can hold within your capacity SKU, when it needs its own refresh schedule, or when it needs a customised model the others don't share. Contractual isolation requirements belong on that list too. Below those thresholds, a shared model with embed-token RLS is simpler to run. Above them, per-tenant workspaces trade query contention for provisioning work.

Our verdict: pick static for a few stable internal segments, dynamic for many or fast-changing ones, embed-token RLS when the viewer is a customer, and per-customer workspaces when tenants are big enough to deserve their own model.

Power BI RLS is the right tool for Entra-identified internal reporting, and it's documented for customers too. Whichever row you choose, the identity path your application owns is the part to test hardest.

For the architecture side, see embedded analytics for multi-tenant database architectures, how Embeddable and Power BI differ for SaaS teams, and row-level security in Embeddable's data model.

Frequently asked questions

Does row-level security in Power BI apply to workspace members?

No. RLS only restricts users with Viewer permissions. Workspace Admins, Members and Contributors see every row, so give report consumers the Viewer role (Microsoft Learn).

Can you assign users to RLS roles in Power BI Desktop?

No. You define roles in Desktop, but you assign users and Microsoft Entra ID groups to them in the Power BI Service (Microsoft Learn).

Why does USERPRINCIPALNAME() not filter per user in Power BI Embedded?

Under a service principal it returns the app's ID or an empty string, not your customer. Pass an EffectiveIdentity in the embed token so the filter runs on the username you supply.

What is the difference between RLS and object-level security (OLS) in Power BI?

RLS filters which rows a viewer sees. Object-level security (OLS) hides whole tables or columns.

Does View as role prove that RLS works for embedded customers?

No. It checks the model logic only. Test embedded scenarios with real embed tokens that carry an EffectiveIdentity, because Test as role doesn't simulate embedded authentication.