Power BI RLS DAX Governance May 2025

Role-Based Page Navigation in Power BI — without native support

A client wanted different users to see different report pages based on their role. Power BI has no built-in feature for this. Here's how I built it anyway — and what you need to know before you try.

⚠ Critical Security Notice

UI-level RLS — greyed-out buttons and hidden page tabs — is a navigation control only. It is not a security measure. A user who knows the page URL can bypass it entirely.

This pattern must always be combined with data-level RLS on your fact tables and strict workspace access controls. Both are covered in this article.

The Brief

A client running a multi-department Power BI environment came to me with a straightforward request: Finance should see Finance pages. Operations should see Operations pages. HR should see HR pages. Nobody should see pages that aren't theirs.

Simple in concept. The problem: Power BI has no native page-level security feature. You cannot assign visibility of a page to an RLS role the way you assign data filters. Pages are visible to everyone who has access to the report — full stop.

What Power BI does have is enough building blocks to construct a solid workaround. Here's how I built it.

Walkthrough: role-based navigation in action — Finance, Operations, and HR roles each seeing different active buttons

The Architecture

The solution uses five components working together:

The result: active buttons navigate correctly. Inactive buttons appear greyed-out and do nothing when clicked. All destination pages are hidden from the tab bar — the Home page is the only entry point.

Power BI has a built-in Page Navigator button (Insert → Buttons → Page Navigation) that auto-generates a full button set for your report. It looks like exactly what you need.

It isn't. The Page Navigator widget is pre-packaged with very limited customisation. You cannot drive individual button destinations or fill colours via DAX measures — which is the entire mechanism this solution relies on. Use Blank buttons instead, one per page.

The DAX

PageAccess Table

DAX — Calculated Table
PageAccess =
DATATABLE(
    "RoleName", STRING,
    "PageKey",  STRING,
    "HasAccess", INTEGER,
    {
        { "Finance",    "finance_detail",    1 },
        { "Finance",    "executive_summary", 1 },
        { "Operations", "ops_detail",         1 },
        { "HR",         "hr_detail",          1 }
    }
)

Navigate Measures

These return the exact page name when the role has access, or BLANK() when it doesn't. The button destination is set to BLANK() which disables the action entirely.

DAX — Navigate Finance
Navigate Finance =
IF(
    COUNTROWS(
        FILTER(PageAccess, PageAccess[PageKey] = "finance_detail")
    ) > 0,
    "Finance Detail",
    BLANK()
)

Repeat for Navigate Executive Summary, Navigate Operations, and Navigate HR, swapping the PageKey and return value.

Colour Measures

Two sets: fill colour and text colour. Active state is brand blue with white text. Inactive state is light grey with faded grey text.

DAX — Colour Finance (fill)
Colour Finance =
IF(
    COUNTROWS(
        FILTER(PageAccess, PageAccess[PageKey] = "finance_detail")
    ) > 0,
    "#2A5F8F", -- active: brand blue
    "#E0E0E0"  -- inactive: light grey
)
DAX — Text Colour Finance (font)
Text Colour Finance =
IF(
    COUNTROWS(
        FILTER(PageAccess, PageAccess[PageKey] = "finance_detail")
    ) > 0,
    "#FFFFFF", -- active: white
    "#BBBBBB"  -- inactive: faded grey
)

Wiring Up the Buttons

For each button on the Home page:

1

Insert → Buttons → Blank

Do not use the Page Navigation widget. Insert individual blank buttons and set the label text manually in the Format pane.

2

Toggle Action on → Page navigation → fx on Destination

Click the fx icon next to Destination, set Format style to Field value, and select the corresponding Navigate measure.

3

Button style → Fill → fx

Select the corresponding Colour measure. This controls the button background colour based on the active RLS role.

4

Button style → Font colour → fx

Select the corresponding Text Colour measure so the label text also reflects the active/inactive state.

5

Hide all destination pages from the tab bar

Right-click each destination page tab → Hide page. Leave Home visible. Users who bypass buttons via the tab bar would undermine the entire pattern.

Enterprise Deployment — USERPRINCIPALNAME()

The static role approach above works well for small teams. For production environments, the better pattern is dynamic RLS with a single role driven by a UserMapping table fed from your identity infrastructure.

Instead of assigning users to multiple roles in the Power BI Service, you create one DynamicAccess role with a single filter:

DAX — DynamicAccess role filter on UserMapping table
[UserEmail] = USERPRINCIPALNAME()

The Navigate measures are updated to look up the user's role dynamically:

DAX — Navigate Finance (dynamic)
Navigate Finance =
VAR CurrentRole =
    CALCULATE(
        SELECTEDVALUE(UserMapping[RoleName]),
        FILTER(UserMapping, UserMapping[UserEmail] = USERPRINCIPALNAME())
    )
VAR HasAccess =
    COUNTROWS(
        FILTER(
            PageAccess,
            PageAccess[RoleName] = CurrentRole &&
            PageAccess[PageKey] = "finance_detail"
        )
    )
RETURN IF(HasAccess > 0, "Finance Detail", BLANK())

Where the UserMapping Table Comes From

In practice the UserMapping table is never hardcoded in Power BI. It comes from systems that already own this data:

SourceHow it reaches Power BIBest for
Azure AD / Entra IDMicrosoft Graph API sync to warehouse, or Azure AD connector in Power QueryMicrosoft-first enterprises
HR System (Workday, SAP)ETL into data warehouse as dim_employeeDepartment-based access
Dedicated security table in warehouseStandard connector — same as any other tableBest audit trail
IAM platform (Okta, SailPoint)Pushed to data warehouse, read by Power BIEnterprise-grade governance

The governance principle: whoever owns the UserMapping table owns access to the report. That should be IT or the data team — not the report author.

Limitations

⚠️
Buttons are greyed out, not hidden. Users can see all buttons and know other pages exist. If complete concealment is required, separate reports per audience is the right answer — not this pattern.
⚠️
This is UX, not security. A user who knows the page URL can still navigate directly. Data-level RLS on your fact tables is mandatory alongside this pattern.
⚠️
Page name strings must match exactly. If you rename a page, update the corresponding Navigate measure. A single character difference will silently break navigation.
⚠️
Test in your embedding context. This pattern works in Power BI Desktop and the Service. Behaviour in embedded scenarios varies — always verify before deployment.

Sample File & Full Guide

The full step-by-step guide — covering static RLS for this exercise and dynamic RLS with USERPRINCIPALNAME() for enterprise deployment — is available on GitHub along with the sample PBIX file used in the walkthrough above.

github.com/bachovak/PBI_role-based_Page_Visibility