Reporting on user’s Group membership in Azure AD

In another article of the “give me a list of all objects a given user is a member of” series, let’s see how we can leverage the Graph API to enumerate all groups a given user is direct, or indirect member of. Similarly to the ExO-based script we discussed a while back, we will use the shortcut “memberOf” query, which in the Graph API is performed by querying the /users/{id}/memberOf endpoint. Unlike the ExO scenario however, here we can also use a “transitive” query, one that accounts for nested groups, i.e. when our user is member of groupX, which in turn is a member of a groupY, both groups will be returned in the output. Said query is facilitated via the /users/{id}/transitiveMemberOf endpoint.

Another difference with the Exchange scenario is the set of groups we can cover. This time, (plain) Azure AD Security groups are included out of the box, but we loose the opportunity to report on some Exchange-specific objects, such as Dynamic DGs. The full list of group objects currently supported by the Graph API queries includes: Azure AD Security groups, Mail-enabled Security groups, Distribution groups, Microsoft 365 groups (which can also be security-enabled). In addition, Azure AD supports dynamic membership for some of these group types, so we can also differentiate them by the membership type. A big plus of the Graph API method is that it automatically returns membership of dynamic groups as well, whereas for the Exchange, additional queries are required. Lastly, some Azure AD groups can be used for delegating access to admin roles, which is another criteria we want highlighted. Overall, using the Graph API methods gives us better chance to properly answer the question “where does user X have access”… but not always.

The script itself is very similar to the one we used to enumerate membership of Administrative units. In fact, the only thing changed in the Graph API query is the OData cast – this time we’re using the value of /microsoft.graph.group. Additionally, we want to be a bit more selective with the set of properties returned for each group object, as they can amount to a lot of additional traffic. Lastly, we are introducing a new parameter, namely -TransitiveMembership, using which will replace the /memberOf query with the /transitiveMemberOf one. Below are examples of the actual Graph API queries used in either scenario:

GET https://graph.microsoft.com/v1.0/users/user@domain.com/memberOf/microsoft.graph.group?$select=id,displayName,mailEnabled,securityEnabled,membershipRule,mail,isAssignableToRole,groupTypes

GET https://graph.microsoft.com/v1.0/users/user@domain.com/transitivememberOf/microsoft.graph.group?$select=id,displayName,mailEnabled,securityEnabled,membershipRule,mail,isAssignableToRole,groupTypes

As always, the connectivity bits of the script will need some attention, before you are able to run it. Make sure to replace lines 14-16 with the corresponding values, keeping in mind that the application used must have Directory.Read.All or equivalent permissions granted. Better yet, replace the entire connectivity block (lines 10-40) with your preferred method to obtain an access token.

Then, all you need to do is decide whether you want to run the script against all users in the tenant (default behavior when you specify no parameter), or against a subset of the users, in which case you need to provide a list of users for the –UserList parameter. The later accepts a list of UPNs or GUIDs, or you can even import a CSV file:

#provide a list of users via their identifier
.\AAD_Groups_MemberOf_inventory.ps1 -UserList "vasil","user@domain.com" ,"b0a68760-1234-1234-1234-8d1ae78f15dc"

#import the list of users from a CSV file
.\AAD_Groups_MemberOf_inventory.ps1 -UserList (Import-Csv .\testtttt.csv).UserPrincipalName

In the example above, the first value (“vasil”) will fail to be resolved against a matching user object and thus will be skipped. Make sure to provide a set of valid UPNs or GUIDs. If importing from a CSV file, make sure to pass an array of string values, not the full imported object. Optionally, you can include the –TransitiveMembership parameter, if needed:

#obtain a report of (transitive) group membership for all users
.\AAD_Groups_MemberOf_inventory.ps1 -TransitiveMembership

The last thing worth mentioning is the output. Again I’ve chosen a “one line per group entry” format, as it makes it that much easier to filter on each individual column in the resulting CSV file. Speaking of columns, apart from the user’s GUID and UserPrincipalName, you can expect to get the group’s GUID, display name, email address (if present), group type, membership type and rule, and the RoleAssignable property, indicating whether this group can be used to delegate membership to Azure AD admin roles. An example of the output is shown on the screenshot below:

sample output of the group membership reportThe “group type” property is a mix between the possible permutations of the groupTypes, securityEnabled, mailEnabled properties, as returned from the Graph. The snippet below illustrates the logic used:

if ($Group.groupTypes -eq "Unified" -and $Group.securityEnabled) { "Microsoft 365 (security-enabled)" } #test with Unified,DynamicMembership - ManagerDynamic
elseif ($Group.groupTypes -eq "Unified" -and !$Group.securityEnabled) { "Microsoft 365" }
elseif (!($Group.groupTypes -eq "Unified") -and $Group.securityEnabled -and $Group.mailEnabled) { "Mail-enabled Security" } #verify
elseif (!($Group.groupTypes -eq "Unified") -and $Group.securityEnabled) { "Azure AD Security" }
elseif (!($Group.groupTypes -eq "Unified") -and $Group.mailEnabled) { "Distribution" }
else { "N/A" }

And with that, here’s the link to the script over at my GitHub page. Usual warnings apply – treat this as a proof of concept and not a production-ready sample.

A version of the script that uses the Microsoft Graph SDK for PowerShell cmdlets instead of direct Graph API queries can be found here.

1 thought on “Reporting on user’s Group membership in Azure AD

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.