Wicked Smart Data
LearnArticlesAbout
Sign InSign Up
LearnArticlesAboutContact
Sign InSign Up
Wicked Smart Data

The go-to platform for professionals who want to master data, automation, and AI — from Excel fundamentals to cutting-edge machine learning.

Platform

  • Learning Paths
  • Articles
  • About
  • Contact

Connect

  • Contact Us
  • RSS Feed

© 2026 Wicked Smart Data. All rights reserved.

Privacy PolicyTerms of Service
All Articles
Implementing Row-Level Security in Power BI: Dynamic Rules, Role Testing, and Enterprise Deployment

Implementing Row-Level Security in Power BI: Dynamic Rules, Role Testing, and Enterprise Deployment

Power BI🔥 Expert31 min readJul 5, 2026Updated Jul 5, 2026
Table of Contents
  • Introduction
  • Prerequisites
  • How Power BI Enforces Row-Level Security
  • The Filter Propagation Question
  • Static Roles: The Foundation
  • Why Static Roles Break Down at Scale
  • Dynamic RLS: The Production-Grade Pattern
  • Building the Security Mapping Table
  • The Dynamic DAX Expression
  • Handling the Relationship Between SecurityMapping and Region
  • Handling Organizational Hierarchies
  • The Explicit Enumeration Approach
  • Recursive Hierarchy in DAX
  • Multi-Role Scenarios and Executive Access
  • Testing RLS: View As Role and Beyond
  • View As Role in Power BI Desktop
  • Testing the Zero-Access Case
  • Using the Power BI Service to Test
  • Programmatic Testing with the Power BI REST API
  • RLS and DAX Measures: What Works, What Doesn't
  • Context Transition and RLS
  • LOOKUPVALUE vs RELATED in Security Expressions
  • Calculated Columns and RLS
  • Enterprise Deployment: Roles, Groups, and the Service
  • Publishing Datasets with RLS
  • Dynamic RLS + AAD Groups: The Hybrid Pattern
  • Incremental Security Table Refresh
  • Power BI Embedded and RLS
  • Hands-On Exercise
  • Common Mistakes & Troubleshooting
  • "All users see all data" after publishing
  • Users see no data when they should see some
  • Performance issues with large SecurityMapping tables
  • Roles not respected in DirectQuery mode
  • Report editors and workspace members bypass RLS
  • Summary & Next Steps
  • Implementing Row-Level Security in Power BI: Dynamic Rules, Role Testing, and Enterprise Deployment

    Introduction

    Picture this: your organization has just finished building a beautiful sales dashboard in Power BI. Regional managers love it. Executives are using it daily. Then someone in finance notices that the Southeast regional manager can see the Northeast region's numbers — and those numbers include some sensitive commission data that was supposed to stay regional. Suddenly you have a security incident, a panicked call to IT, and a report that can't be trusted until it's fixed. What went wrong? Row-Level Security (RLS) was either never implemented, or it was implemented incorrectly.

    RLS is one of the most critical and most misunderstood features in Power BI. It sits at the intersection of data modeling, DAX, and enterprise identity management, which means getting it right requires competence across all three. Done poorly, RLS gives you false confidence — users feel restricted, but a determined person can often find the gaps. Done well, it's nearly invisible: users see exactly the data they're authorized to see, the rules maintain themselves as the organization changes, and your security posture is genuinely airtight.

    By the end of this lesson, you will be able to design and implement both static and dynamic RLS in Power BI, write DAX security expressions that handle real-world complexity, test your rules rigorously before deployment, and roll out RLS in enterprise environments where identity comes from Azure Active Directory groups and where dataset refresh, organizational hierarchy, and multi-role scenarios all need to work together.

    What you'll learn:

    • How Power BI enforces row-level security at the query engine level, and why that matters for your design choices
    • How to build dynamic RLS using USERPRINCIPALNAME() and a security mapping table — the pattern used in most production deployments
    • How to write and test DAX filter expressions that handle hierarchies, multi-role membership, and edge cases like users who appear in no security group
    • How to use Power BI Desktop's "View As Role" feature and the REST API to validate security rules before users ever see the report
    • Enterprise deployment patterns: publishing with RLS to Power BI Service, managing roles via Azure AD groups, and monitoring for security drift

    Prerequisites

    You should be comfortable with:

    • DAX fundamentals — at minimum, CALCULATE, FILTER, VALUES, and how filter context works
    • Power BI data modeling — star schema design, relationships, and how filter propagation flows through your model
    • Basic Power BI Service navigation — workspaces, datasets, and the difference between the Desktop and the Service
    • A working understanding of Azure Active Directory — what a UPN is, what a security group is, and how members are assigned

    If you're fuzzy on filter context in DAX, pause here and shore that up first. RLS is filter context — you cannot implement it correctly without understanding it.


    How Power BI Enforces Row-Level Security

    Before writing a single DAX expression, you need to understand what's actually happening when Power BI enforces security. This isn't academic — it directly determines how you structure your data model and where you place your security filters.

    When a user opens a report, Power BI Service authenticates them via Azure AD and determines their identity. That identity (their User Principal Name, or UPN — typically their work email address) gets passed to the Analysis Services engine that powers the Power BI dataset. The engine then applies whatever RLS rules are defined for the roles that user belongs to. Only after those filters are applied does the engine execute the DAX queries that drive your visuals.

    This means the security filter runs before your report queries. Your report DAX can't escape it. A measure that uses ALL() to ignore filters? It still can't see rows that were filtered out by RLS. A visual that uses ALLSELECTED()? Same story. This is good news — it means RLS is genuinely enforced at the engine level, not bolted on top at the visualization layer. But it also means you need to be thoughtful about where in your model you place the security filter, because filter propagation through relationships determines what gets restricted.

    The Filter Propagation Question

    Here's the practical consequence: your RLS filter applies to a specific table. Power BI then propagates that filter through your model relationships to restrict related tables. The direction of that propagation depends on your relationship configuration.

    In a classic star schema, you have a fact table (like Sales) and dimension tables (like Region, Product, Date). If you apply an RLS filter to the Region dimension table, the filter propagates to Sales through the one-to-many relationship, restricting which sales rows a user can see. This is the correct pattern for most scenarios.

    What you want to avoid is applying your security filter directly to the fact table. It works, but it creates a maintenance nightmare — every new fact table needs its own filter. Filter from the dimension; let relationships do the work.

    Region (dimension) ──[1:*]──> Sales (fact)
       ^
       │
    RLS filter applied here
       │
    Filter propagates to Sales automatically
    

    Bidirectional relationships deserve special caution. If your Region to Sales relationship is set to filter in both directions, then filtering Sales would also filter Region — which is usually not what you want for RLS. Stick to single-direction filtering in your security-related relationships unless you have a very specific reason otherwise and you've tested it thoroughly.


    Static Roles: The Foundation

    Static roles are where everyone starts, and they're the right choice for organizations with a small number of stable security groupings. Let's implement one properly so you understand the mechanics before we move to dynamic rules.

    Imagine you're building a sales dashboard for a retail company with four regions: Northeast, Southeast, Midwest, and West. Each regional manager should only see their region's data.

    In Power BI Desktop, open the Modeling ribbon and select "Manage Roles." Click "Create" and name your first role Northeast_Manager. In the table pane, find your Region dimension table and click the ellipsis next to it. Enter this DAX expression:

    [RegionName] = "Northeast"
    

    Create three more roles — Southeast_Manager, Midwest_Manager, and West_Manager — with corresponding filter expressions. Save and close.

    Now, here's what this actually does: Power BI wraps this expression in a FILTER() applied to the Region table at query time. When someone assigned to the Northeast_Manager role runs any query, the engine behaves as though the Region table only contains the row where RegionName = "Northeast". That restriction then flows through your relationships to limit the Sales rows, the Budget rows, and any other fact table connected to Region.

    Why Static Roles Break Down at Scale

    Static roles work fine for four regions. They start to become a problem when you have 40 regions, or when regions change, or when the same person manages two regions, or when a user is promoted and their access needs to change. Every structural change requires opening Power BI Desktop, modifying roles, and republishing the dataset. In a live production environment, that's an operational burden that compounds over time.

    More fundamentally, static roles encode your organization's structure into your data model. When the organization changes, the model must change. The enterprise-grade answer is dynamic RLS.


    Dynamic RLS: The Production-Grade Pattern

    Dynamic RLS decouples your security configuration from your data model definition. Instead of hardcoding filter values into role expressions, you store the mapping between users and their data access in a table — a security mapping table — and write a DAX expression that looks up the current user's access at query time.

    This is the pattern used in virtually every serious Power BI deployment. Here's how to build it.

    Building the Security Mapping Table

    Your security mapping table stores the relationship between user identities and the dimension values they're allowed to see. For our retail example:

    UserEmail RegionName
    sarah.chen@company.com Northeast
    sarah.chen@company.com Midwest
    james.okafor@company.com Southeast
    priya.sharma@company.com West
    david.kim@company.com Northeast
    david.kim@company.com Southeast
    david.kim@company.com Midwest
    david.kim@company.com West

    Notice that Sarah Chen has access to two regions — she's probably a divisional manager. David Kim has access to all four — he's probably an executive or a national sales director. This is something static roles cannot handle gracefully; dynamic RLS handles it naturally.

    This table typically comes from one of three places:

    1. A dedicated security table in your source database maintained by IT or HR systems
    2. A SharePoint list managed by business owners who control access
    3. An Azure AD-derived table built during data refresh by querying the Microsoft Graph API

    Option 3 is the most scalable and the most secure, because it derives access from your authoritative identity system automatically. We'll return to this in the enterprise deployment section.

    The Dynamic DAX Expression

    In your Region table, you create a single role — call it Regional_Access — and apply this DAX filter expression:

    [RegionName] IN 
        CALCULATETABLE(
            VALUES( SecurityMapping[RegionName] ),
            SecurityMapping[UserEmail] = USERPRINCIPALNAME()
        )
    

    Let's walk through what this does. USERPRINCIPALNAME() returns the UPN of the currently authenticated user — their email address, in most Azure AD configurations. The CALCULATETABLE expression then filters the SecurityMapping table down to only rows where that user's email appears, and from those rows extracts the distinct RegionName values. Finally, the IN operator checks whether the current Region row's RegionName is in that list.

    The result: each user sees exactly the regions mapped to them in the SecurityMapping table. Change the table, and access changes immediately on the next query — no model republishing required.

    Important: USERPRINCIPALNAME() returns the authenticated user's identity when the report is accessed through Power BI Service. In Power BI Desktop, it returns your own Windows identity or email, which is why "View As Role" testing is essential before deployment. We'll cover testing thoroughly in a dedicated section.

    Handling the Relationship Between SecurityMapping and Region

    The SecurityMapping table needs a relationship to your Region table so that filters can propagate correctly. Create a many-to-one relationship from SecurityMapping[RegionName] to Region[RegionName]. This relationship should filter in both directions — this is one of the cases where bidirectionality is actually appropriate, because the security mapping table exists solely to drive security filtering and doesn't participate in your analytical queries.

    Actually, let's be precise about an alternative approach many practitioners prefer: don't rely on the relationship at all. Keep the DAX expression self-contained using CALCULATETABLE as shown above. This approach is more explicit about what's happening and less vulnerable to unexpected relationship interactions. The expression directly queries the SecurityMapping table and returns the valid RegionName values, then checks membership in that set.

    SecurityMapping
    ┌────────────────────┬────────────────┐
    │ UserEmail          │ RegionName     │
    ├────────────────────┼────────────────┤
    │ sarah.chen@...     │ Northeast      │
    │ sarah.chen@...     │ Midwest        │
    │ james.okafor@...   │ Southeast      │
    └────────────────────┴────────────────┘
    
    Region
    ┌────────────────┐
    │ RegionName     │      Filter: [RegionName] IN {user's allowed regions}
    ├────────────────┤      ↓
    │ Northeast      │ ←── Visible to sarah.chen
    │ Southeast      │ ←── NOT visible to sarah.chen
    │ Midwest        │ ←── Visible to sarah.chen
    │ West           │ ←── NOT visible to sarah.chen
    └────────────────┘
    

    Handling Organizational Hierarchies

    Real organizations aren't flat. You have individual contributors, regional managers, divisional managers, and executives — all with different scopes of data access that relate to each other through the organizational hierarchy. If you encode each person's access as individual rows in the security mapping table, you end up with an enormous table that's painful to maintain whenever the hierarchy changes.

    The better approach is to think about hierarchy-based access in two ways: explicit enumeration for smaller organizations, and recursive hierarchy traversal for larger ones.

    The Explicit Enumeration Approach

    For most mid-market organizations, the security mapping table approach with role-based rather than user-based entries is the cleanest solution. Instead of mapping individual users to regions, you map Azure AD security groups to regions:

    GroupEmail RegionName
    northeast-managers@company.com Northeast
    divisional-east@company.com Northeast
    divisional-east@company.com Southeast
    national-sales@company.com Northeast
    national-sales@company.com Southeast
    national-sales@company.com Midwest
    national-sales@company.com West

    But wait — Power BI's USERPRINCIPALNAME() returns the individual user's UPN, not their group memberships. So you can't directly compare UPN to group emails in the filter expression.

    You have two options. First, you can expand the group memberships into individual user rows during your data refresh process by querying Azure AD (via Microsoft Graph API or a Power Query connector). The mapping table then contains individual user emails, and the DAX expression works as previously shown.

    Second, you can use Power BI's built-in support for mapping Azure AD security groups to roles at the Service level (which we'll cover in deployment). In this case, you define roles in the model, and Azure AD group membership determines which role a user gets — the actual role assignment happens outside DAX.

    Recursive Hierarchy in DAX

    If your organization has a true reporting hierarchy stored in your data — like an employee table with a ManagerID column — you can implement manager-level access using DAX path functions. Here's a scenario: a sales manager should see all transactions belonging to everyone in their reporting chain.

    Start with an Employee table:

    EmployeeID EmployeeName ManagerID Email
    1 Maria Torres NULL m.torres@company.com
    2 James Liu 1 j.liu@company.com
    3 Amy Park 2 a.park@company.com
    4 Chris Webb 2 c.webb@company.com

    Use DAX's PATH function to precompute the hierarchy path for each employee:

    HierarchyPath = 
    PATH( Employee[EmployeeID], Employee[ManagerID] )
    

    This creates a string like "1|2|3" for Amy Park, encoding the full chain from the root to her. Now, your RLS expression on the Employee table becomes:

    PATHCONTAINS(
        Employee[HierarchyPath],
        LOOKUPVALUE(
            Employee[EmployeeID],
            Employee[Email],
            USERPRINCIPALNAME()
        )
    )
    

    This expression says: for each employee row, check whether the current user's EmployeeID appears anywhere in that employee's hierarchy path. If Maria Torres (EmployeeID 1) is logged in, she sees everyone, because 1 appears in every path. If James Liu (EmployeeID 2) is logged in, he sees himself, Amy, and Chris, but not Maria's other direct reports if she had any. Amy Park sees only herself.

    This is elegant and self-maintaining — as long as your employee data is current, the security logic is correct without any manual intervention.

    Performance note: The PATH and PATHCONTAINS functions work by computing and traversing string representations of hierarchies. On very large employee tables (50,000+ rows), this can introduce noticeable query latency. Profile your query performance in such cases and consider precomputing and materializing the hierarchy mapping in your ETL process rather than computing it in DAX.


    Multi-Role Scenarios and Executive Access

    What happens when a user belongs to multiple roles? In Power BI, multiple role assignments are additive — the user sees the union of all data accessible through each role they're assigned to. This is an OR relationship, not AND.

    This is important and often misunderstood. If you assign a user to both Northeast_Manager and Southeast_Manager, they see both regions. You cannot use roles to implement an AND restriction (show only data that's in Region A AND Category B). For that kind of compound restriction, you need to build it into a single role's DAX expression.

    For executives who need to see all data, you have two patterns:

    Pattern 1: Executive bypass in the DAX expression

    USERPRINCIPALNAME() IN VALUES( ExecutiveList[Email] )
        || [RegionName] IN 
            CALCULATETABLE(
                VALUES( SecurityMapping[RegionName] ),
                SecurityMapping[UserEmail] = USERPRINCIPALNAME()
            )
    

    This first checks if the user is in an executive list and, if so, returns TRUE for all rows (bypassing the filter entirely). Otherwise, it falls back to the standard access check.

    Pattern 2: Comprehensive mapping table

    Add executives to your SecurityMapping table with rows for every region. This is simpler from a DAX perspective but requires maintenance whenever you add new dimension values. The executive bypass pattern in DAX is more resilient to schema changes.

    Warning: If you're using Power BI Report Server (on-premises) rather than Power BI Service, USERPRINCIPALNAME() returns the Windows domain account (DOMAIN\username format, not email). This breaks email-based lookups. On-premises deployments should use USERNAME() and store domain accounts in the mapping table instead.


    Testing RLS: View As Role and Beyond

    Inadequate testing is the most common reason RLS deployments fail in production. You need to verify that each role shows the right data, that no role shows data it shouldn't, and that users who belong to no role don't see any data they shouldn't.

    View As Role in Power BI Desktop

    The "View As Role" feature in Power BI Desktop is your primary testing tool during development. On the Modeling ribbon, find "View As" (it may be labeled "View as roles" depending on your Desktop version). Select a role, and optionally specify a specific UPN to simulate. Your report immediately re-renders showing only the data that user/role combination would see.

    Critical things to check for each role:

    • Do all visuals filter correctly? Look for charts that show "Other" categories or unexpected totals.
    • Do measure totals respect the filter? A regional sales total should sum only the visible regions.
    • Are there any cards or KPIs that show unfiltered numbers? This sometimes happens when measures use ALL() incorrectly.
    • Does cross-filtering between visuals still work? Clicking a region bar chart should further filter, not conflict with RLS.

    When specifying a UPN to test dynamic RLS, use a real email from your security mapping table. Enter sarah.chen@company.com (one of your mapped users) and verify she sees Northeast and Midwest data. Then enter a UPN that doesn't exist in your mapping table and verify that she sees no data at all — this tests your "zero access" edge case.

    Testing the Zero-Access Case

    This is the edge case most developers forget. What happens when a user's UPN doesn't appear in your SecurityMapping table? Let's trace through the DAX:

    [RegionName] IN 
        CALCULATETABLE(
            VALUES( SecurityMapping[RegionName] ),
            SecurityMapping[UserEmail] = USERPRINCIPALNAME()
        )
    

    If USERPRINCIPALNAME() returns a UPN that has no rows in SecurityMapping, then the CALCULATETABLE expression returns an empty table. VALUES() of an empty table is an empty list. And [RegionName] IN {} (empty set) evaluates to FALSE for every row. The user sees zero data.

    This is correct behavior — unknown users should see nothing. But make sure it's actually happening. Test with a UPN that isn't in your table. If your user accidentally sees all data instead of no data, you have a bug in your DAX, and you have a serious security hole.

    Using the Power BI Service to Test

    After publishing, you can test role assignments in the Service by going to your dataset's Security settings (find the dataset in your workspace, click the ellipsis, select "Security"). Here you can add individual users or Azure AD groups to roles and use the "Test As Role" option to preview what a role member would see.

    The Service-level testing is essential because it confirms that the identity resolution (UPN lookup from Azure AD) is working correctly. A common failure mode is that a user's UPN in Azure AD differs from what you have stored in your security mapping table — perhaps due to a domain change, an alias, or a recently hired employee whose email was added to the report data but who used their old email format during login.

    Programmatic Testing with the Power BI REST API

    For enterprise deployments with dozens of roles and hundreds of users, manual testing doesn't scale. The Power BI REST API provides endpoints that allow you to validate effective permissions programmatically.

    The POST /datasets/{datasetId}/generateTokenInGroup endpoint allows you to generate an embed token for a specific identity, which you can then use to validate what that identity sees. More directly relevant for testing is using the Analysis Services client libraries to execute DAX queries against your published dataset while impersonating specific users — this lets you write automated tests that verify expected row counts for each user/role combination.

    For a production deployment, consider building a test harness that:

    1. Pulls a list of expected user/region combinations from your security mapping table
    2. For each user, executes a query that returns the distinct regions visible to them
    3. Compares the result to the expected list
    4. Alerts on any discrepancy

    This kind of automated security validation, run after each dataset refresh and after each dataset publish, gives you genuine confidence that your RLS is functioning correctly rather than security theater.


    RLS and DAX Measures: What Works, What Doesn't

    RLS interacts with DAX measures in ways that aren't always obvious. Understanding these interactions prevents bugs that are difficult to diagnose.

    Context Transition and RLS

    RLS operates at the filter context level, above individual measure calculations. When a measure uses CALCULATE with ALL() to remove filters:

    All Regions Sales = CALCULATE( SUM( Sales[Amount] ), ALL( Region ) )
    

    This removes user-applied slicers and cross-filters from the Region table — but it does not remove the RLS filter. The RLS filter is a different kind of context, applied at a lower level that ALL() cannot override. So All Regions Sales for a Northeast-only user will still only return Northeast sales, even though you said ALL(Region). This is the intended behavior and a security feature, but it can confuse users who expect to see comparison metrics against the full dataset.

    If you need to show a user a metric for the full company (for context) while still restricting their detailed data view, you're in a genuinely complex situation. The only clean solution is to pre-aggregate those "total company" numbers in a separate table that isn't subject to RLS (a summary table at a non-RLS-restricted grain), or to use a separate dataset without RLS for the benchmark metrics and combine them in a composite model.

    LOOKUPVALUE vs RELATED in Security Expressions

    A common mistake is using RELATED() in RLS expressions when the required related table isn't in the right filter context. RELATED() only works within row context (like in calculated columns), not in filter context. Since RLS DAX expressions operate as filter expressions, use LOOKUPVALUE() when you need to fetch a value from another table:

    -- Wrong: RELATED doesn't work in filter expressions
    [SalesTerritory] = RELATED( Employee[Territory] )
    
    -- Right: LOOKUPVALUE works in filter expressions  
    [SalesTerritory] = LOOKUPVALUE(
        Employee[Territory],
        Employee[Email],
        USERPRINCIPALNAME()
    )
    

    Calculated Columns and RLS

    An important gotcha: RLS does not apply to calculated columns. Calculated columns are computed during the data refresh, not at query time, so the RLS filter context isn't active when they're computed. If your calculated column contains data that should be protected by RLS, a user who accesses the column values through DAX could potentially see information outside their allowed scope. This is one reason to keep sensitive derivations in measures rather than calculated columns when you're in an RLS-restricted model.


    Enterprise Deployment: Roles, Groups, and the Service

    Moving from development to enterprise deployment introduces a set of considerations that don't exist in Desktop.

    Publishing Datasets with RLS

    When you publish a dataset from Power BI Desktop to the Service, the role definitions come with it. Go to your workspace, find the dataset, click the ellipsis, and select "Security." Here you'll see the roles you defined in Desktop and fields to add members.

    You can add:

    • Individual user email addresses
    • Azure AD security groups
    • Mail-enabled security groups
    • Microsoft 365 Groups

    For any organization beyond a handful of users, always use Azure AD security groups rather than individual user accounts. Here's why: when a new sales rep joins the Northeast team, IT adds them to the Northeast-Sales-Team AAD group. Power BI automatically includes them in the Northeast_Manager role because the group is assigned to that role. No Power BI admin needs to touch the dataset. When someone leaves, IT removes them from the group — again, Power BI access is automatically revoked. The security follows your authoritative identity system without any manual reconciliation.

    Dynamic RLS + AAD Groups: The Hybrid Pattern

    Here's a pattern that gives you the best of both worlds. You define one role in your model — Standard_User — with a dynamic DAX expression. In the Service, you assign your broad All-Power-BI-Users Azure AD group to that role. The dynamic DAX expression then handles the fine-grained filtering based on the SecurityMapping table.

    This means you manage two things separately:

    • Who can see the report at all: controlled by AAD group membership in the Service
    • What data each user sees within the report: controlled by the SecurityMapping table and the DAX expression

    Separating these concerns makes your security architecture cleaner. IT manages authentication and basic access (AAD groups). Data stewards manage the SecurityMapping table. The two systems don't need to be synchronized manually.

    Incremental Security Table Refresh

    Your SecurityMapping table needs to be kept current. If you're sourcing it from a database, this is straightforward — include it in your regular dataset refresh. But if it's large or if you want access changes to take effect quickly, consider setting up an incremental refresh policy that only refreshes the security table (or isolates it in a composite model's DirectQuery partition).

    For the fastest possible access updates, the most sophisticated pattern is to store the SecurityMapping table in a DirectQuery-connected source (like Azure SQL Database or Azure Synapse) rather than in Import mode. Changes to the database table take effect on the next query without requiring any dataset refresh. The trade-off is a small query performance cost — the security lookup happens in real time against the database rather than in-memory.

    Power BI Embedded and RLS

    If you're embedding Power BI reports in an application using Power BI Embedded (the Azure-based embedding service), RLS works differently. Instead of authenticating via Azure AD, your embedding application generates a token that specifies the user's effective identity and role. This is called "Effective Identity."

    In this pattern, your application code — not the Power BI Service — is responsible for determining what role to assign and what username to pass. The token generation call looks like:

    {
      "accessLevel": "View",
      "identities": [
        {
          "username": "sarah.chen@company.com",
          "roles": ["Regional_Access"],
          "datasets": ["your-dataset-id"]
        }
      ]
    }
    

    The Power BI engine then treats the embedded session as if that user with that role is authenticated. Your dynamic DAX expression fires with USERPRINCIPALNAME() returning sarah.chen@company.com, and the security mapping lookup runs exactly as it would in the Service.

    The critical security implication: your embedding application's token generation code is now the security boundary. If a bug allows a user to request a token with someone else's username or a higher-privilege role, your RLS is completely bypassed. Application-level security in embedded scenarios requires the same rigor — arguably more — as the RLS configuration itself.


    Hands-On Exercise

    In this exercise, you'll build a complete dynamic RLS implementation for a sales dataset from scratch. Here's the scenario: you're building a Regional Sales Performance dashboard for a national retail chain. Regional managers should only see their regions; divisional VPs see their division's regions; the national director sees everything.

    Step 1: Prepare your data model

    Create (or import) the following tables:

    A Region dimension table with columns: RegionID, RegionName, Division

    RegionID | RegionName  | Division
    ---------|-------------|----------
    1        | Northeast   | East
    2        | Southeast   | East
    3        | Midwest     | Central
    4        | Southwest   | Central
    5        | West        | West
    6        | Northwest   | West
    

    A Sales fact table with columns: SaleID, RegionID, ProductID, SaleDate, Amount

    A SecurityMapping table with columns: UserEmail, RegionName

    Populate SecurityMapping with data reflecting this org structure:

    • Regional managers: one region each
    • East Division VP (dvp.east@retailco.com): Northeast and Southeast
    • Central Division VP (dvp.central@retailco.com): Midwest and Southwest
    • West Division VP (dvp.west@retailco.com): West and Northwest
    • National Director (nd@retailco.com): all six regions
    • A test user with no mapping (ghost@retailco.com): no rows

    Step 2: Set up relationships

    Create a many-to-one relationship from Sales[RegionID] to Region[RegionID] with single-direction filtering (Region → Sales).

    Create a many-to-one relationship from SecurityMapping[RegionName] to Region[RegionName]. Set this to single-direction filtering as well — we'll use the explicit DAX approach rather than relationship propagation.

    Step 3: Create the dynamic role

    In the Modeling ribbon, open Manage Roles and create a role called Regional_Access. Apply this filter to the Region table:

    [RegionName] IN 
        CALCULATETABLE(
            VALUES( SecurityMapping[RegionName] ),
            SecurityMapping[UserEmail] = USERPRINCIPALNAME()
        )
    

    Step 4: Create a total sales measure

    In your Sales table, create a measure:

    Total Sales = SUM( Sales[Amount] )
    

    Create a second measure to help with testing — this one should return the count of visible regions:

    Visible Region Count = COUNTROWS( Region )
    

    Step 5: Build a simple test report

    Create a table visual with Region[RegionName] and [Total Sales]. Create a card visual with [Visible Region Count]. These will be your test visuals.

    Step 6: Test thoroughly

    Use "View As" in the Modeling ribbon to test the following scenarios:

    • Test as role Regional_Access with UPN dvp.east@retailco.com — you should see Northeast and Southeast, Visible Region Count = 2
    • Test as role Regional_Access with UPN nd@retailco.com — you should see all six regions, Visible Region Count = 6
    • Test as role Regional_Access with UPN ghost@retailco.com — you should see zero rows, Visible Region Count = 0
    • For one of your regional managers (add an entry in SecurityMapping), confirm they see exactly one region

    Step 7: Add an executive bypass

    Modify your role filter expression to add an executive override check that lets a user see all data if their UPN starts with a specific executive domain prefix (or better: is in a separate ExecutiveList table you create). Practice modifying the DAX without breaking the existing access logic:

    VAR CurrentUser = USERPRINCIPALNAME()
    VAR UserRegions = 
        CALCULATETABLE(
            VALUES( SecurityMapping[RegionName] ),
            SecurityMapping[UserEmail] = CurrentUser
        )
    RETURN
        [RegionName] IN UserRegions
    

    Notice we've refactored to use variables — this is a best practice for readability and also allows you to compute CurrentUser once and reference it twice. Extend this with your executive check using the COUNTROWS of an ExecutiveList lookup.

    Bonus challenge: Modify the SecurityMapping table to use Division-level access entries. A VP with Division = "East" should see all regions in that division without individually listing each region. Write the DAX expression that handles this by joining through the Region table's Division column rather than looking up RegionName directly.


    Common Mistakes & Troubleshooting

    "All users see all data" after publishing

    This almost always means you've published the dataset but haven't assigned any users to the role in the Service. Check your dataset's Security page. An unassigned role affects no one — users who aren't assigned to any role see all data by default (for the workspace members who have the right dataset access). Assign your Azure AD group to the role immediately after publishing.

    Users see no data when they should see some

    First, check UPN format. USERPRINCIPALNAME() returns the exact UPN from Azure AD — typically firstname.lastname@company.com. If your SecurityMapping table has emails in a different format (different domain, different convention, or lowercase vs. mixed case), the lookup fails. DAX string comparisons are case-insensitive in most contexts, but domain differences are real.

    Second, check whether the user is actually assigned to the role in the Service. If a user is assigned to a role with a DAX expression that filters out all their rows, they'll see nothing. But if they're not assigned to any role, they should see everything — so seeing nothing means they're assigned to a role whose filter returns empty for them.

    Third, test the USERPRINCIPALNAME() return value by temporarily creating a measure:

    CurrentUPN = USERPRINCIPALNAME()
    

    Put this in a card visual and view it as the affected user (using "Test As" in the Service). This tells you exactly what UPN the engine is seeing.

    Performance issues with large SecurityMapping tables

    If your SecurityMapping table has hundreds of thousands of rows (common in large enterprises with fine-grained access), the CALCULATETABLE lookup can become expensive. Optimizations to consider:

    1. Ensure the SecurityMapping table is in Import mode, not DirectQuery, when using it for lookups in RLS expressions. Import mode keeps it in memory and makes lookups fast.
    2. Add a calculated column to SecurityMapping that normalizes UPN format during data refresh, rather than doing string manipulation in the DAX filter expression.
    3. Index your source database table on UserEmail so that the ETL refresh is fast.
    4. Consider whether you actually need that many rows, or whether group-based assignment at the Service level could dramatically reduce the table size.

    Roles not respected in DirectQuery mode

    If your dataset uses DirectQuery to a source that supports its own row-level security (like Azure SQL Database with RLS, or SQL Server with row-level security), you may encounter a situation where Power BI's RLS and the database's own RLS interact. Be explicit about which layer is doing the filtering to avoid double-filtering (which causes empty results) or no filtering (which causes security gaps).

    The safest approach: if Power BI RLS is your primary control, disable or set database-level RLS to pass-through. If database-level RLS is your primary control (because of compliance requirements that mandate database-layer security), you can configure Power BI to pass the user's identity through to the database — this is called Fixed Identity vs. Per-User credentials in the dataset settings.

    Report editors and workspace members bypass RLS

    Power BI workspace members with the Admin, Member, or Contributor role can see all data in datasets within that workspace — RLS does not apply to them. Only users with the Viewer role (or those who access the report through an app) are subject to RLS. This is by design, on the assumption that people with edit rights need to see all data to build reports properly. If you need to restrict workspace members too, you need to separate the dataset and the report into different workspaces, publish the report to a workspace where the restricted users have Viewer access.


    Summary & Next Steps

    You've now covered the complete arc of RLS implementation — from understanding how the Analysis Services engine enforces security filters, through static and dynamic role design, into hierarchical access patterns, DAX measure interactions, and enterprise deployment with Azure AD.

    The key mental model to carry forward: RLS is filter context applied at the identity layer, before any of your report's DAX executes. Design your security filters to operate on dimension tables and let relationship propagation do the work. Use dynamic RLS with a security mapping table for any organization that changes over time (which is every organization). Test obsessively — the happy path, the edge cases, and especially the zero-access case.

    For next steps, here's where to deepen your expertise:

    Object-Level Security (OLS): While RLS restricts which rows a user can see, OLS restricts which columns and tables are visible at all. It's Power BI's answer to column-level security and is complementary to RLS. Implement OLS when you have columns (like salary, commission rate, or PII fields) that should be entirely invisible to certain users rather than just filtered.

    Composite Models and RLS: When you combine Import and DirectQuery tables in a composite model, RLS rules become more complex — each storage mode has its own security enforcement path. If you're building composite models in production, the RLS interactions are a critical area to study.

    Power BI Activity Log and Security Auditing: Once your RLS is deployed, you need to know if it's working. The Power BI Activity Log (accessible via REST API or Microsoft 365 Compliance Center) records every report view event, including which user accessed which report. Combine this with programmatic RLS testing to build a continuous security monitoring pipeline.

    Sensitivity Labels and Information Protection: Microsoft Purview sensitivity labels integrate with Power BI to add an additional layer of access governance. Understanding how sensitivity labels and RLS work together — and where one supplements the other — is important for organizations with strict data classification requirements.

    Mastering RLS is one of the highest-leverage investments you can make as a Power BI practitioner. Security done right is invisible to users and invulnerable to attackers. Now go audit the RLS on your existing dashboards — you may be surprised what you find.

    Learning Path: Getting Started with Power BI

    Previous

    Mastering Power Query in Power BI: Transforming, Cleaning, and Shaping Data Before It Hits Your Model

    Related Articles

    Power BI⚡ Practitioner

    Implementing Power BI Tenant Settings and Sensitivity Labels for Enterprise Data Protection and Compliance

    28 min
    Power BI⚡ Practitioner

    Mastering DAX Calculation Groups: Reduce Measure Proliferation and Build Reusable Metric Frameworks

    20 min
    Power BI⚡ Practitioner

    Mastering Power Query in Power BI: Transforming, Cleaning, and Shaping Data Before It Hits Your Model

    22 min

    On this page

    • Introduction
    • Prerequisites
    • How Power BI Enforces Row-Level Security
    • The Filter Propagation Question
    • Static Roles: The Foundation
    • Why Static Roles Break Down at Scale
    • Dynamic RLS: The Production-Grade Pattern
    • Building the Security Mapping Table
    • The Dynamic DAX Expression
    • Handling the Relationship Between SecurityMapping and Region
    • Handling Organizational Hierarchies
    • The Explicit Enumeration Approach
    • Recursive Hierarchy in DAX
    • Multi-Role Scenarios and Executive Access
    • Testing RLS: View As Role and Beyond
    • View As Role in Power BI Desktop
    • Testing the Zero-Access Case
    • Using the Power BI Service to Test
    • Programmatic Testing with the Power BI REST API
    • RLS and DAX Measures: What Works, What Doesn't
    • Context Transition and RLS
    • LOOKUPVALUE vs RELATED in Security Expressions
    • Calculated Columns and RLS
    • Enterprise Deployment: Roles, Groups, and the Service
    • Publishing Datasets with RLS
    • Dynamic RLS + AAD Groups: The Hybrid Pattern
    • Incremental Security Table Refresh
    • Power BI Embedded and RLS
    • Hands-On Exercise
    • Common Mistakes & Troubleshooting
    • "All users see all data" after publishing
    • Users see no data when they should see some
    • Performance issues with large SecurityMapping tables
    • Roles not respected in DirectQuery mode
    • Report editors and workspace members bypass RLS
    • Summary & Next Steps