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 / 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:



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","" ,"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:

The “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.

Posted in Azure AD, Graph API, Microsoft 365, Office 365, PowerShell | Leave a comment

Reporting on user’s Administrative unit membership in Azure AD

As Microsoft is slowly ramping up the Azure AD RBAC story, I expect usage of Administrative units to gain more traction, which in turn means organizations will need an easy way to report on AU usage. Sure, you can readily get the list of AUs to which a given user has been added to from the Azure AD blade, or similarly, get a list of all members of a particular AU. However, doing this for a thousand users via the UI is a no go, so we need a programmatic solution.

In fact, we have few such solutions. Let’s start with good old Exchange Online PowerShell – you can use the user object’s AdministrativeUnits property to obtain the list of AUs, i.e.:

Get-User vasil | select -ExpandProperty AdministrativeUnits

From here’s it’s fairly easy to automate this across all users:

Get-User -ResultSize Unlimited | select UserPrincipalName,AdministrativeUnits

where you can expose additional properties as needed and you can even use the Get-AdministrativeUnit cmdlet to replace the GUIDs with human-readable values. However, the cmdlet offers only a limited set of properties on the AU object, most importantly it doesn’t give us information about membership type.

Another set of cmdlets we can use come from the MSOnline module, but they also return insufficient details. The set of AU cmdlets within the AzureAD/AzureADPreview module goes a step further, but still has some limitations. Finally, we end up with the Graph API, and it’s bastard child, the Microsoft Graph API SDK for PowerShell, both of which offer the most extensive support of endpoints/methods for the scenario at hand. As with most things Graph, you will need to have an app with the required permissions (Directory.Read.All would do) before you start poking around.

Instead of going over each endpoint we can use to gather information on AUs, let’s just focus on the fastest/easiest method to do the task at hand. We need a list of users, and for each user we need a list of AUs he’s a member of. So, we need a query against the /users endpoint to give us the basic user information, and some way to fetch AU membership. This is where the /memberOf endpoint comes in, providing us with a quick way to list all directory objects for which a given user is considered a “member”. These include all group types recognizable by Azure AD, admin roles, and most importantly for our scenario, Administrative units.

Here’s where we can take another shortcut – instead of getting the full list of objects returned from the /memberOf endpoint, we can use the so-called OData cast to limit the output to just AU objects. To do so, we simply need to append the microsoft.graph.administrativeUnit string to our request. In other words, the query we will run looks something like this:


From here on, it’s just a matter of gathering the relevant details and preparing the output. The script will fetch a list of all user objects within the organization, then loop over each of them and run the query above. In an ideal world, we would’ve been able to fetch all this information via a single Graph API query, possibly via the use of the $expand operator. However, there are certain limitations (and bugs) with the current implementation of $expand, so my advice would be to stay clear of it. If nothing else, the limit of maximum of 2o entries returned should warn you off.

Now, we can technically obtain the same set of data even faster, by querying the list of AU objects and enumerating their membership, instead of doing things on a per-user basis. As with most of my script samples however, you should treat the code as a proof of concept, suitable to specific scenarios only. Yes, there are faster ways to generate the same output when you are interested in a tenant-wide inventory. The method used by the script however is much more suitable when you want to obtain the AU membership of a single user, or a group of users.

Without further ado, you can get the script from my GitHub repo. Make sure to replace the “connectivity” bits (lines 10-40) with your preferred method to obtain an access token. At the very least, replace the variables in lines 14-16, which are needed to obtain a token via the client credentials flow, the default one used by the script. Apart from that, you need to 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
.\AU_memberOf_inventory.ps1 -UserList "vasil","" ,"b0a68760-1234-1234-1234-8d1ae78f15dc"

#import the list of users from a CSV file
.\AU_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. Lastly, here’s a sample of the script’s output:

I specifically opted to use the “each AU membership on a new line” format, as opposed to the more “condensed” “one line per user” format, as it quickly allows you to filter the resulting CSV file not just on the user(s), but it also allows you to quickly list all members of a given AU, or even pinpoint users that are not a member of any AU. In any case, feel free to adjust the output as you see fit.

As usual, let me know if you run into any issues or have questions about the chosen method to generate AU membership. Remember that this is just a quick proof of concept script which lacks the touch and polish needed for a full blown production-ready solution! Especially when it comes to handling authentication. Proper error handling is another example of where the script should be improved. And, once Microsoft releases support for “nesting” AUs, you might consider replacing the /memberOf query with a /transitiveMemberOf one 🙂

Posted in Azure AD, Exchange Online, Graph API, Microsoft 365, Office 365, PowerShell | Leave a comment

Generating a report of users’ group membership (MemberOf inventory) V2

Time to update another one of my sample scripts to take advantage of the goodness the Exchange Online V3 PowerShell module provides. This time, we will be taking on the “memberOf inventory” script, or in other words the script that provides a list of all Exchange groups a given user is a member of, across all users within the organization or a subset of them. As this is an Exchange-based script, only group objects recognized by Exchange Online will be included: distribution groups, mail-enabled security groups and Microsoft 365 Groups. It does not include Azure AD (pure) security groups.

Similarly, the set of users for which the members query is executed is based on the information available within ExODS. In other words, only users that are recognized as valid Exchange recipients will be returned. Since we’re only covering membership to Exchange-related groups anyway, this is only natural, although there are some corner cases where a user, technically not recognized as a valid recipient by Exchange Online, can still appear within group membership. This is one of the improvements made in the “V2” version of the script – it will now include all Exchange Online user objects with an RecipientTypeDetails value of User. A set of switch parameters covers each individual recipient type, and you can toggle them as you see fit:

  • IncludeUserMailboxes – this is the default value, used when you run the script without providing any parameters.
  • IncludeSharedMailboxes – use this switch to include user objects corresponding to Shared mailboxes in the output.
  • IncludeRoomMailboxes – use this switch to include user objects corresponding to room, equipment and booking mailboxes.
  • IncludeMailUsers – use this switch to include mail user objects.
  • IncludeGuestUsers – use this switch to include Guest users (which are a special type of mail users).
  • IncludeMailContacts – although technically not a user object, sometimes it can be useful to also get inventory of which groups your Mail Contacts have been added to.
  • IncludeUsers – use this switch to include the aforementioned non-recipient User objects.
  • IncludeAll – use this switch to include all of the above.

As a side note, and with the hope to better illustrate the logic introduced above, here’s how to get the full set of users within your Exchange Online environment.

Get-User | group RecipientTypeDetails -NoElement | sort Count -Descending

Count Name
----- ----
16 UserMailbox
13 SharedMailbox
10 GuestMailUser
9 User
6 MailUser
5 SchedulingMailbox
4 RoomMailbox
2 DiscoveryMailbox
1 TeamMailbox
1 EquipmentMailbox

With the newly introduced parameters, we’re accounting for all of these user object types, apart from DiscoveryMailbox and TeamMailbox. Those last two are (mostly) a thing of the past now, and not something you would want to include in a report anyway.

As before, the script will use a server-side filter to return a list of all Recipient, for which the given user is a member. This is done via the Get-Recipient cmdlet and a filter based on the Members query, run against the object’s DistinguishedName value. In other words, for each user we run the following:

Get-Recipient -Filter "Members -eq '$dn'" 

where the $dn variable holds the (sanitized) value of the DistinguishedName property. With the V3 module and the InvokeCommand method, we get most of the benefits of the REST-based cmdlets, so performance-wise the script should fare much better. Even better performance would be possible by utilizing the Get-EXORecipient cmdlet instead, which allows us to limit the output to just select properties. However, the REST-based cmdlets continue to have issues with handling special characters, such as the “#” character found in the DistinguishedName value of every GuestMailUser object, so using the Get-Recipient cmdlet is an acceptable tradeoff.

The connectivity part of the script has also changed. It now explicitly checks for the presence of the REST-based cmdlets, as a way of detecting V2/V3 version of the module. Even though you can still obtain the same data via good old Remote PowerShell Session connectivity, it’s time to move forward. This also means that the new version of the script will not run against on-premises installs, unless you modify the Check-Connectivity function.

Few small changes have been made to the output as well. First, to accommodate for the non-recipient User  objects, which do not have a PrimarySMTPAddress, the output will use an “Identifier” column now. The value of the Identifier will depend on the object type – mailboxes will show the Primary SMTP address, mail users will show the External Email Address and non-recipient users will show the ExternalDirectoryObjectId instead. And to make things a bit clearer, we’ve also added the RecipientTypeDetails column to the output. Output will be exported to a CSV file in the working directory by default, and also stored in the global variable $varDGMemberOf for reuse.

And with that, you should be ready to try the new version. You can download the script from GitHub here:

To run the script against all regular users (users with mailboxes) and Guest users within your tenant, use the following syntax:

.\DG_MemberOf_inventoryV2.ps1 -IncludeGuestUsers -IncludeUserMailboxes

To include non-recipient users, use the –IncludeUsers switch:

.\DG_MemberOf_inventoryV2.ps1 -IncludeUsers

To include all supported user types, use the –IncludeAll switch:

.\DG_MemberOf_inventoryV2.ps1 -IncludeAll

Before closing, it’s worth mentioning one more time that this script only includes objects recognized by Exchange Online, thus it does not always provide the full picture. You might want to run a similar exercise against Azure AD, by leveraging the MemberOf/transitiveMemberOf endpoint. I’ll likely do a sample script on that in the coming days/weeks.

As always, do let me know if you run into any issues with the script or have comments on it.

Posted in Exchange Online, Microsoft 365, Office 365, PowerShell | 1 Comment

Azure AD custom roles with support for granular User management permissions

Role-based Access Control (RBAC for short) across Azure AD (and Microsoft 365 as a whole) has been a multi-year effort for Microsoft. While some of the individual workloads have their own, and in some cases very robust, permission models, the lack of single overarching solution has long plagued Microsoft 365 tenants, especially those that span multiple companies or geo-locations. On the positive side, the lack of proper RBAC support has given ISVs an opportunity to step in an offer their own solutions.

To illustrate just how long Microsoft has been working on this, the first building block of the RBAC model, namely Administrative units, was introduced back in December of 2014! I covered the initial implementation in an article dated early 2015. It took Microsoft 3 years (!) before the relevant UI bits were added to the Office 365 Admin center, which we covered back in 2018. It took them even longer to move past the initial set of roles supported, and the introduction of custom Azure AD roles. Now, at long last, Microsoft has released support for creating custom Azure AD roles for managing User objects, which complemented with the previously released support for Groups, devices, application and service principal objects finally puts the Azure AD RBAC model on the big scene. Well almost, as some bits are still in Preview, and thus should not be used in production environments just yet.

To get the list of operations currently supported for granular delegation, you can refer to the official documentation. You can also get the same details right within the Azure AD blade, when creating a new custom role. The steps are as follows: login to the Azure AD blade with a user holding the Privileged Role Administrator or Global Administrator role, go to Roles and administrators > hit the New custom role button. Provide a Name for the new role, and optional (but highly recommended) Description. You can also decide whether to start building the new role from scratch or by copying an existing role, by selecting the corresponding option under the Baseline permissions control. On the next page, Permissions, you will get the full list of granular operations currently supported by the Azure AD RBAC model.

Interestingly enough, when it comes to user operations, the list of currently supported entries is rather small. As evident from the Permissions page on the New custom role wizard, or the official documentation for that matter, we have no way to delegate access to the delete user operation, changing password or any authentication-related property, or even read/update user’s photo. In fact, out of 111 resourceActions currently listed as available for user objects under the resource, only 22 are supported for granular role delegation. Then again, this is still a preview functionality, so we can expect things to change before release.

That said, even with the limited options available, we are able to go more granular than the built-in roles. For example, we can create a custom role that only allows for direct assignment of licenses to user objects. Unlike the built-in License administrator role, this custom role (let’s call it Manage Licenses Only) will not allow group-based license assignments. In addition, the built-in role also comes with some additional privileges, such as being able to open the Service Health Dashboard and work with some other parts of the Microsoft 365 Admin Center. And, like all the built-in roles, it inherits permissions from the default Directory Readers role. In effect, a user with the License administrator role has access to a lot more than just license information within the organization, even though the majority of this information is read-only. On the other hand, a custom role allows you to go much more granular even on the “read-only” permissions, and you can limit those to just the standard user properties.

Whichever individual permissions you assign to a custom role is up to you. As mentioned above, only a handful of scenarios are supported for user objects currently, but a custom role is not limited to just one type of objects to manage, and more permissions will likely become available in the future. Once you are satisfied with the selection of permissions for your new custom role, press the Next button and proceed to the Review + Create page to verify the selection, then hit the Create button to complete the process.

The newly created Azure AD admin role allows us to define which specific operations can be performed by a user that has been assigned the role. Another, and arguably even more important part of the RBAC model, is being able to specify the set of objects against which a given role will be applied. In other words, the scope of the role. In Azure AD, the scope is defined as part of a role assignment, which is basically a link between a directory principal (such as user or service principal object) and a role definition (such as the custom role we created above), granted at a specific directory scope. The scope can be a single user or group object, but most commonly represents an administrative unit object.

As an example, let’s assign our Manage Licenses Only custom role to a given user, and make sure he can only use his newly assigned privileges against users in a given AU, for example the Bulgaria users AU. To create the role assignment, go to the Azure AD blade > Administrative units > select the AU in question > Roles and administrators. Here, you should see a list of all (built-in and custom-created) Azure AD roles that support an AU-scoped role assignment. Clock on the Manage Licenses Only entry, then hit the Add assignments button. The only thing left to configure is the set of user(s) to which the role assignment will be linked to, which you can do by hitting the Select Members link. As in my case the tenant is using Privileged Identity Management, I will also have to select the assignment type and duration. Once you hit the Assign button, the relevant user(s) will be able to enjoy their newly assigned privileges, after obtaining a fresh access token that is.

Next, let’s see how the new custom role can be leveraged by the user. It’s worth re-iterating that the role we created will not allow the user to login to the Microsoft 365 Admin center, so any operations will be limited to the Azure AD blade. And since we created a role assignment at an AU scope, even within the Azure AD blade the user assigned said role will be limited to performing actions against only a subset of the users. Lastly, even for users in scope, the set of actions will be restricted to reading the user properties, updating the usage location and updating license assignments.

To illustrate the effect of assigning the scoped custom role, refer to the screenshot below. Notice the difference in the available UI elements for a user out of scope (top left) and user in scope of the role assignment (top right). The UI will automatically disable any control the user does not have permissions for, such as the list of profile properties show in the middle. While other UI elements might show as enabled, for example the Reset password button, they will show an “access denied” or “insufficient privileged” type of error when accessed. In effect, for users outside of the role assignment scope, our newly appointed admin will not be able to perform any action, not even changing their license. For users in scope, the only changes he will be able to perform is updating the Usage location property (middle right) and managing license assignments (bottom right). In addition, the Reprocess action will also be unavailable, as we did not include it in the role definition.

In effect, we have created a custom, granular role assignment, scoped to only a subset of the users of the tenant. With the newly introduced support for user objects, we can now exert granular control over each action the user will be able to perform, down to individual attributes in some cases, which serves to illustrate the flexibility of the RBAC model chosen by Azure AD. As more user-specific actions become available and other preview functionalities roll out, we will finally get to a point where Azure AD RBAC is ready to tackle the challenges posed by multi-national companies or large organizations in general. Don’t forget that this functionality requires Azure AD Premium licensing though.

Before closing, let me also mention that the configuration steps detailed above are not limited to the Azure AD UI only. Graph API offers full support for listing, creating and managing custom Azure AD roles and role assignments. In fact, some of the bits of information presented above were taken directly out of the corresponding Graph API endpoints, i.e. this endpoint gives you the list of all available resourceActions for Azure AD. Refer to the official Graph API documentation for more info. Also, I’ve definitely not covered all the intricacies of the Azure AD RBAC model, for example I didn’t talk about the other scope types available. Refer to my previous articles or the official documentation for details on those.

Oh, and let me re-iterate once again that custom roles do not inherit the default user permissions in Azure AD. This is contrary to the behavior of the built-in roles, and is controlled by the corresponding InheritsPermissionsFrom property/relationship. Keep this fact in mind when planning your custom Azure AD role implementation!

Posted in Azure AD, Microsoft 365, Office 365 | Leave a comment

Conditional access policies add more granular controls for external users

As part of a new preview functionality, Microsoft is introducing an expanded set of controls for Conditional access policies, revolving around granularly including/excluding specific guest users. You will find the new controls as part of the Users and groups > Select users and groups > Guest or external users selection. Therein, you can choose from six types of Guest or external users categories, as follows:

  • B2B collaboration guest users – the “standard” type of Guest user. The userType value is Guest, and permissions are inherited from the default Guest user role.
  • B2B collaboration member users – similar to the above, but this group includes objects for which the userType value has been converted to Member. Thus they have an expanded set of permissions within the directory.
  • Local guest users – represents users created/managed within your own Azure AD tenant, but with userType value set to Guest. The difference with the first scenario is the “source of authority” – in this case your local Azure AD instance.
  • B2B direct connect users – currently only represented by Teams shared channel members, those are external users leveraging the B2B direct connect model.
  • Service provider users – a special flavor of B2B direct connect, tailored to partners/service providers. Configured by toggling the isServiceProvider flag of a cross-tenant access policy object.
  • Other external users – a blanket category, including all other objects that don’t fall into any of the above categories.

After selecting one or more of the above categories, you can then select whether to further scope down the control by restricting it to specific Azure AD tenant(s), or choose All tenants. You can specify either the tenant GUID or one of its domains, including the default one or any custom-verified domains. The same controls are also available for the Exclude scenario, giving you a lot of flexibility.

Apart from the UI controls, the set of Graph API endpoints has also been updated to cater to the new functionality, albeit only available under /beta for the time being. We have the includeGuestsOrExternalUsers and excludeGuestsOrExternalUsers properties added to the main CA resource type, both of which are representing a conditionalAccessGuestsOrExternalUsers object. Within said object, the guestOrExternalUserTypes property represents the selection of the six categories as detailed in the list above. The corresponding values to use with the Graph API are as follows: b2bCollaborationGuest, b2bCollaborationMember, internalGuest, b2bDirectConnectUserserviceProvider and OtherExternalUser.

The tenant controls on the other hand are represented via the externalTenants property, of the conditionalAccessExternalTenants resource type. Within it, the membershipKind property designates the type of selection made, with possible values of all (to include all Azure AD tenants) or enumerated (specific Azure AD tenants). The latter requires yet another property to be configured, namely the members string collection of conditionalAccessEnumeratedExternalTenants type. Here’s how an example configuration would look like:

"includeGuestsOrExternalUsers": {
    "guestOrExternalUserTypes": "b2bCollaborationGuest",
    "externalTenants": {
        "@odata.type": "#microsoft.graph.conditionalAccessEnumeratedExternalTenants",
        "membershipKind": "enumerated",
        "members": [

Refer to the official documentation for more details. Also do note that once a policy has been updated to use the new controls, you will only find it under the /beta endpoint – don’t panic if no matching object is returned under /v1.0 🙂

Posted in Azure AD, Graph API, Microsoft 365, Office 365 | 1 Comment