As I’m going over some of my old PowerShell script samples and updating them to the latest and greatest, I do a lot of testing of various cmdlets and Graph API methods. It is quite amusing to see just how many issues manifest themselves as part of this process. Or quite sad, really. In this article, we will go over some examples of broken experiences with Microsoft 365 Group membership/ownership.
The first set of examples revolves around the requirement that a group owner must also be a member. This is actually not how things used to work back in the day, but at some point Microsoft decided to enforce this requirement. To date, Exchange Online’s *-UnifiedGroupLinks cmdlets comply with this behavior and will prevent you from adding (or removing) a user from the corresponding ownership/membership list. Turns out, the Azure AD blade couldn’t care less about this requirement, so we have a major disconnect in the experiences between Exchange Online and Azure AD. Let’s see some examples.
Here’s a simple scenario – a Microsoft 365 Group with a single member/owner. The first screenshot below will show you what happens if I try to remove the user from the group’s membership list using Exchange Online’s Remove-UnifiedGroupLinks cmdlet. As the user is also an owner, I’m prevented from doing this. Yet, on Azure AD side, the behavior is quite different. Both the Remove-AzureADGroupMember, Remove-TeamUser and Remove-MgGroupMemberByRef cmdlets, as well as the underlying Graph API method will happily allow you to make the change (in case you were wondering, Remove-MsolGroupMember does not support modifying the membership of M365 Group objects).
OK, so we can remove a member and leave him as an owner, perhaps the explanation is simple – the requirement is being enforced “client-side” by the Exchange Online cmdlets. But let’s see what happens when we have more than one owner of the Group. We select a group with two owners (both also listed as members), and try to remove one of them. Exchange Online and Graph API calls both behave as in the previous scenario. But this time, let’s focus on the experience with the Teams cmdlets. Technically, the cmdlets issue Graph API queries, so the expected result should be the same. Or is it?
Get-UnifiedGroupLinks 1fc47b5e-b6ca-44b4-ae1b-318fe0846d7a -LinkType member Name RecipientType ---- ------------- vasil UserMailbox HuKu UserMailbox pesho UserMailbox Get-UnifiedGroupLinks 1fc47b5e-b6ca-44b4-ae1b-318fe0846d7a -LinkType owner Name RecipientType ---- ------------- HuKu UserMailbox vasil UserMailbox
What’s interesting is that the highlighted parts of the cmdlet output above not only indicate that Graph API calls were made, they also make an effort to check whether the user to be removed is an owner. Even though we specifically requested to remove him from the member “role” only. Obviously, as far as Teams is concerned, an owner should also be a member. So Teams goes a step further and removes the user from both the owners and members list.
In fact, if we return to the previous scenario (Team/Group with single owner/member) and try to remove the last/only owner/member, the Remove-TeamUser cmdlet will complain with the error message shown below. Even though the Teams module cmdlets are basically wrapped Graph API calls, we observe a different behavior compared to the “pure” Graph queries. Talk about predictable experience.
The behavior described above begs the question whether we can also add a group owner directly, without adding him as a member first. The results are quite similar – Exchange Online will prevent you from doing this (first screenshot), whereas the Graph API and any cmdlet that depends on it will happily allow the operation. But we are again seeing a difference in behavior between the “pure” Graph API calls and the wrapped up Teams cmdlets. Whereas the former will add the user only as owner (second screenshot), the latter will add him to the members list as well (third screenshot):
In effect, we again have disagreement between the various methods, and a resulting mess:
Get-MgGroupOwner -GroupId 7503c511-dd24-45bf-8b43-6a280b815121 | select Id,@{n="UPN";e={$_.AdditionalProperties.userPrincipalName}} Id UPN -- --- 584b2b38-888c-4b85-8871-c9766cb4791b vasil@michev.info e0d7442c-8cd8-4e65-8ede-ec9887816677 HuKu@michev.onmicrosoft.com 064abb3c-0812-44f9-bdcc-eea7e6ea398b gosho@michev.info Get-MgGroupMember -GroupId 7503c511-dd24-45bf-8b43-6a280b815121 | select Id,@{n="UPN";e={$_.AdditionalProperties.userPrincipalName}} Id UPN -- --- 584b2b38-888c-4b85-8871-c9766cb4791b vasil@michev.info 064abb3c-0812-44f9-bdcc-eea7e6ea398b gosho@michev.info
Sadly, it doesn’t end here. The dissonance extends further into Graph’s own methods, resulting in experience which is definitely in the “bug” category. See, whereas “regular” Graph API operations will prevent you from removing the last owner of a Group/Team, “batch” operations do not suffer from such limitations. Which is a shame, as in this scenario, all other methods are in agreement! On the screenshot below, you can see that both Exchange Online and Teams cmdlets will throw an error when you try to remove the last owner:
Similarly, the Graph API direct query and the Remove-MgGroupOwnerByRef cmdlet will also complain about this, and the scenario is even noted in the official documentation. Quote:
When owners are assigned to a group, the last owner (a user object) of the group cannot be removed.
Yet, if we perform a BATCH operation, the above restriction seems to be bypassed. An easy way to test this, without having to deal with annoying JSONs, is to use the Azure AD blade, operations within which have slowly been converted to use direct Graph API calls. As an added bonus, we can use this scenario to illustrate that the Azure AD blade UI suffers from the same implementation mishaps as the other Graph API based methods outlined above.
Anyway, to demonstrate the bug with last owner removal, open the Azure AD blade, navigate to the Groups section and select any Microsoft 365 Group with more than one Owner. Or any Group really, as you can just add a new owner(s) on the go. In the short video below, I’ve selected a group with a single owner (which I’m prevented from removing), added a new one then refreshed until the UI reflected the updated ownership list. Then, I proceeded to select both owners, hit the F12 button to show the network capture and then hit the Remove button. The Network tab shows a BATCH request issued with two DELETE operations, one for each owner. For each of the requests, a “success” status of 204 was returned and in effect, all owners were removed, which is also confirmed once we refresh the page.
Similarly, the screenshot below corresponds to the same operation performed on another group (so yes, it’s reproducible), and summarizes the important details. You can see the set of two owners, confirmation from the UI that their removal was successful as well as the actual batch requests issued.
Interestingly, the implementation on Exchange Online side does not suffer from the same bug. As the -Links parameter supports an array of user objects, you can issue the below cmdlet to try and remove all the owners in one go. Yet, processing on the backend seems to be smart enough to detect that such removal operation will leave the group ownerless, and stops you:
Remove-UnifiedGroupLinks 7503c511-dd24-45bf-8b43-6a280b815121 -LinkType owner -Links vasil,gosho,huku -Confirm:$false Write-ErrorMessage : |Microsoft.Exchange.Management.Recipient.MinGroupOwnersCriteriaBreachedException|You can't remove the owner from this group because the person you're removing is currently the only owner. You need to promote another member to owner before you proceed.
So, in summary, even in 2023, Microsoft is yet to get their story straight on the topic of Microsoft 365 Groups and their membership and ownership lists. Tho requirements should be in effect: a Group owner must also be a member, and the last owner of a Group cannot be removed. At least, that’s the theory.
In this article, we examined the behavior of various administrative endpoints with regards to removing a Group member who is also an owner, adding an owner without making him a member and removing the last owner. In none of the three examined scenarios do we see a uniform (and predictable) experience across all admin endpoints. Even in the “remove last owner” scenario, the behavior for which is backed by official documentation, we observe inconsistencies. Great success!
Remove a member that is also an owner | Add owner without adding him as member | Remove last owner | |
Exchange Online cmdlets | Blocked | Blocked | Blocked |
Graph API | Allowed, leaves user as owner | Allowed | Blocked, but BUGGED when using BATCH requests |
Teams cmdlets | Allowed, removes user as member AND owner
Blocked if it’s the last owner |
Allowed, adds as member too | Blocked |