← All articles
RLSDAX

RLS Patterns That Scale Past One Team

Row-level security that works for one team's regional sales report often falls apart the moment a second business unit, a partner org, or a hierarchy of managers needs access to the same model. The fix isn't more roles — it's a security table designed for scale from the start.

Skip the hardcoded role, use a security table

Instead of a DAX filter that hardcodes USERPRINCIPALNAME() = "someone@co.com", model security as data:

[UserAccess] :=
'Security'[UserEmail] = USERPRINCIPALNAME ()

Where Security is a table mapping UserEmail to the dimension keys they can see — region, business unit, whatever the grain is. Adding a user, or an entire team, becomes a data change, not a model redeploy.

Handling hierarchies without exploding row counts

For manager-sees-direct-reports scenarios, don't try to enumerate every manager's full downline as rows in the security table — it grows combinatorially. Instead, store a path (like a materialized path or a ParentPath column) and filter with a path-prefix match:

[UserAccessByHierarchy] :=
VAR CurrentUserPath =
    LOOKUPVALUE ( 'Security'[Path], 'Security'[UserEmail], USERPRINCIPALNAME () )
RETURN
    PATHCONTAINS ( 'Employee'[Path], CurrentUserPath )

The one thing to actually test

Test RLS as the user, not as yourself with a role applied in Desktop. "View as role" with a hardcoded filter hides bugs that only show up under USERPRINCIPALNAME() — particularly casing mismatches between Entra ID and your security table. Normalize both sides to lowercase before comparing, or you'll debug a "phantom" access issue that's really just Someone@Co.com not equaling someone@co.com.