Published: 2025-10-04
Adding groups instead of individual users to a workspace must be considered a best practice. Of course, if only two or fewer people are accessing the content of the workspace in question, this does not make any sense π
However, there may be multiple Admins, Members, or Contributors managing and developing the content of the workspace. If this is the case, using Entra Id groups will significantly reduce the effort required to control access to the workspace. Additionally, it is possible to "productionize" the membership in one of the Entra Id groups. Of course, this also depends on how membership in groups is managed at your organization.
As this article is about expanding Entra Id groups to users (being more precise, the UserPrincipalName), and might become the first article in a series about monitoring the Microsoft data estate, this article is not about workspace roles; this article is about granting access to users who are interested in one or more workspaces and the consumption of Compute Units (s).
# A little disclaimer
When it comes to Fabric monitoring, I'm a proponent of FUAM (Fabric Unified Admin Monitoring).
This article is about expanding FUAM (to some extent); this is not due to a shortcoming of the solution, but rather because of a special requirement. FUAM provides the data necessary to build a solution that meets the specific requirements.
However, expanding groups, even nested groups, to the UserPrincipalName is a challenge that needs to be tackled more often, and not just for the use case outlined in this article. So even if you are not using FUAM, this article hopefully offers enough Python to get you interested π
# The solution I want to build
In short, I want to build a solution that allows users to see the consumption of Compute Unit (s). But (as always), users are only allowed to see the workspaces they can access. For this reason, I lean on the FUAM table 'workspace_scanned_users'. Looking at the table, we see this (filtered for a specific workspace):
![[Monitoring Fabric - Expanding groups to users - workspaces_scanned_users.png]]
Looking at the column 'PrincipalType', we see that there are two groups granted access to the workspace: one is assigned to the workspace role 'Viewer', and the other to the role 'Member'.
When I want to grant access to users of the Power BI app, but only to their workspaces, I need to expand these groups to the UserPrincipalName (the email address), because otherwise Row Level Security (RLS) cannot be implemented, at least not in an elegant way.
# The problem I need to tackle
Looking at the workspace from inside the Fabric service, I see the groups granted access to the workspace and the workspace roles.
The picture below shows who has access to the workspace:
![[Monitoring Fabric - Expanding groups to users - Workspace access.png]]
There is an individual user, an Office 365 group (Analyzing MVP DL emails), and an Entra Id security group (aad_theoutergroup).
If RLS is required to restrict access to only a subset of the available workspaces, some approaches come to mind. If the number of workspaces is vast, the most elegant solution is to expand the groups to include users (from a more technical perspective, the UserPrincipalName).
And, of course, it's not just about expanding the groups; it's also about considering nested groups, as this is a very often-used pattern when examining larger organizations.
After executing the notebook 'workspaces_scanned_users_groupsexpanded', there is a new delta table in the FUAM_Lakehouse: workspaces_scanned_users_expandedgroups
When I execute the query from above, there are now more rows (groups got expanded), but also more columns:
![[Monitoring Fabric - Expanding groups to users - workspaces_scanned_users_expandedGroups.png]]
These new columns (in contrast to the original FUAM table) are:
+ graphId_Start
The Entra Id of the object that has access to the workspace, which is either a group or a user.
+ graphId_Path
The complete path of Entra Id objects (groups) is not required to configure RLS, but as I love paths, this may become handy sometime in the future.
+ graphId_Parent
The immediate ancestor (the parent) of the UserPrincipalName (the email)
+ graphId_UserPrincipalName
The UserPrincipalName is the thing that I need to configure RLS.
+ UserPrincipalName_expanded
This is the final column, which contains the original UserPrincipalName in cases where the PrincipalType equals 'User' or the value of the column graphId_UserPrincipalName.
# Some musings about the notebook, or the Python I use
There is nothing particularly special about the Python code I wrote. Nevertheless, I would like to highlight certain aspects in the following chapters. The following chapter headers are the same as the markdown headers in the notebook.
## Read secret from Azure Key Vault
I use a dedicated Service Principal when accessing the GraphAPI. This means in my FUAM implementations, there are two. One is accessing all the Fabric APIs, and the 2nd one is reading from the GraphAPI. This principle is called "Separation of Duties", which avoids a single Service Principal becoming too powerful. This might also be a requirement in your organization.
## Create MSAL client application
This is required to access the Graph API.
Please be aware that the Service Principal requires the following API permissions:
+ Group.Read.All
+ GroupMember.Read.All
+ User.Read.All
Additionally, the Service Principal must be assigned to the role of Directory Readers. The following picture shows what I'm talking about:
![[Monitoring Fabric - Expanding groups to users - Directory reader role.png]]
## Get the groups from the 'workspace_scanned_users' table
Even though this is one of the smallest cells, according to the number of rows. It's the cell with the biggest attitude π
Whenever I need to iterate across a subset of values derived from a table column, I tend to transform the values into a list. Then, within the iteration, I call the user-defined function that expands a group to UserPrincipalNames. The function then adds a dictionary to a list.
The list is transformed into a dataframe that is then joined (left outer) with the original FUAM table.
I'm doing this for performance reasons, as from my experience using user defined functions inside dataframe manipulations with ```.withcolumn``` is more expensive (CU consumption) and takes longer.
## Joining 'workspaces_scanned_users' and 'df_expandedGroups'
Defining a schema for the "list" is not overkill, as it also allows joining an empty "list" with the original dataframe. Sure, it is very unlikely that the "list" will be empty, but who knows?
# A final word
I have added the notebook as an extra step to the "Load_FUAM_Data_E2E" pipeline. I'm aware that this is not an ideal solution, but perhaps a better solution will be available in one of the future releases of FUAM.
# Resources
+ The Fabric notebook: https://github.com/tomatminceddata/TomIsMonitoringFabric/tree/main/ExpandingGroupsToUsers
+ FUAM at the fabric toolbox: https://github.com/microsoft/fabric-toolbox/tree/main/monitoring/fabric-unified-admin-monitoring