Syncing Security Groups with team membership

In this post, I present a PowerShell script to synchronize the membership between security groups and Office 365 groups.

 

Security groups in Azure Active Directory (AAD) have long been a useful way to manage sets of users in the enterprise -- even going back to on-premises Active Directory and before that, Windows NT global groups. The Office 365 Groups service is the more modern way to address this need, used by Microsoft Teams, Planner, Outlook Groups, Yammer, Power BI, and more. Of course, they're not connected (yet), which is unfortunate but not atypical given the evolution of platforms and products and sometimes divergent scenarios.

 

Many companies use AAD security groups extensively, for good reason, and they have a lot of intellectual capital vested in the creation, curation, and management of those security groups. At Microsoft we've used security groups to manage access to internal support resources, bug databases, and source code systems. These companies logically want to leverage their security groups investment for Microsoft Teams and other Office 365 Groups-based services, but they can't right now. If you add a security group to a team membership list, Teams will do a one-time expansion of the security group (same for a distribution list), but any subsequent changes are not reflected in the team, and vice versa.

 

Obviously, a great solution would be to base the team membership directly on a security group, so that any changes to the security group are reflected in the team in Microsoft Teams, and vice versa. This would be similar to how Teams leverages the Office 365 Groups service. The engineering team is aware of this request and it is marked as on the backlog. You can provide additional input on the use case and priority via the User Voice feedback system, item 1861385. Similar user feedback has also been provided to the Office 365 Groups team, and you can read and vote on their feedback system too, item 33942997.

 

But while we wait for those engineering teams to get to this work (and deal with a thousand other demands on their time), let's take a look at a short-term solution that will unblock companies looking to synchronize security group membership with team membership.

 

The premise is straightforward: create a PowerShell script that will run periodically, maybe every 12 hours or 24 hours, which synchronizes one or more pairs of security group/Office 365 group. Now, the PowerShell interfaces are a little different for each type of group (see note above re: platform evolution and divergent scenarios), but with a little hacking and slashing, I got it to work reasonably well.

 

BIG WARNING: I was a physics major in college who fell backwards into software product management. I'm not a developer, and only sort of an "engineer" (in Canada, they probably wouldn't let me wear the pinky ring). My coding process involves a lot of trial-and-error mixed with Stack Overflow research. This code should not be considered production-ready. Rather, look at it as an illustrated proof-of-concept that actual real developers can use to build actual real code.

 

The source code is on GitHub, naturally: https://github.com/danspot/Danspot-Scripts-and-Samples-Emporium 

 

Here's roughly what the script does:

  1. Get the security group ID and Office 365 group ID; in the script, this is done via lookup based on user input, but in a real app, this should probably be read in from a configuration file
  2. Scan the membership of the security group
  3. Make sure all those users are also in the Office 365 group
  4. Remove anybody in the Office 365 group who is not in the security group

 

In this diagram, you can see how one user ("Rajesh") is in the AAD security group but not the Office 365 group, so that user should be added to the latter. And another user, "Stewart" is in the Office 365 group but not the security group, so that user should be removed. Bye Stewart!

 

group-sync-diagram.png

Here's the key part of the code that scans the security group and adds missing members (in a brute force way) to the Office 365 group:

 

 

# loop through all Security Group members and add them to a list
# might be more efficient (from a service API perspective) to have an inner foreach
# loop that verifies the user is not in the O365 Group
Write-Output "Loading list of Security Group members"
$securityGroupMembersToAdd = New-Object System.Collections.ArrayList
foreach ($securityGroupMember in $securityGroupMembers)
{
$memberType = $securityGroupMember.GroupMemberType
if ($memberType -eq 'User') {
$memberEmail = $securityGroupMember.EmailAddress
$securityGroupMembersToAdd.Add($memberEmail)
}
}

# add all the Security Group members to the O365 Group
# this is not super efficient - might be better to remove any existing members first
# this might need to be broken into multiple calls depending on API limitations
Write-Output "Adding Security Group members to O365 Group"
Add-UnifiedGroupLinks -Identity $O365GroupID -LinkType Members -Links $securityGroupMembersToAdd

 

And here's the part of the code that removes users who are in the Office 365 group but not the security group. Probably the trickiest part of the script was finding and aligning the user ID between the two different groups schemas.

 

 

# loop through the O365 Group and remove anybody who is not in the security group
Write-Output "Looking for O365 Group members who are not in Security Group"
$O365GroupMembersToRemove = New-Object System.Collections.ArrayList
foreach ($O365GroupMember in $O365GroupMembers) {
        $userFound = 0
        foreach ($emailAddress in $O365GroupMember.EmailAddresses) {
# trim the protocol ("SMTP:")
                $emailAddress = $emailAddress.substring($emailAddress.indexOf(":")+1,$emailAddress.length-$emailAddress.indexOf(":")-1)
                if ($securityGroupMembersToAdd.Contains($emailAddress)) { $userFound = 1 }
        }
        if ($userFound -eq 0) { $O365GroupMembersToRemove.Add($O365GroupMember) }
}


if ($O365GroupMembersToRemove.Count -eq 0) {
        Write-Output "   ...none found"
} else {
# remove members
        Write-Output " ... removing $O365GroupMembersToRemove"
                foreach ($memberToRemove in $O365GroupMembersToRemove) {
                Remove-UnifiedGroupLinks -Identity $O365GroupID -LinkType Members -Links $memberToRemove.name
        }
}

 

Important notes:

  1. This script should probably be run periodically, perhaps every 6 hours or every 24 hours, maybe on an admin’s desktop, or better yet, using Azure Automation.
  2. Either the security group or the Office 365 group should probably be designated as the "primary" and any changes to that would be reflected on the other, "replica" entity, and not vice-versa. For example, if the security group was the primary, but a user changed the team membership in Microsoft Teams (the replica), that change on the Teams side should be overwritten. Given most people interested in this solution probably have a lot of time and effort already invested in security groups, it’s likely that you'll want to make the security group the primary in this model.
  3. There are sometimes odd ways that emails are handled in the directory, so you may need to test and tweak the script to handle email addresses for your domain(s), especially if you have multiple email domains or users with secondary email addresses.
  4. This script probably requires more hardening against other situations like nested security groups, Unicode email addresses, resource and room accounts, etc.
  5. This script may not scale very well as currently written, although in practice that may not be a real problem. There may be limits to the number of users that can be added in one operation (so batching may be required). There are some foreach loops and brute-force adding of members, which probably isn't super efficient.
  6. It's probably a good idea to not do the cleanup to remove stray team members (in the Office 365 group) who are not in the security group. Rather, log that information and have a real human go and double check and remove if necessary. You wouldn't want a coding or configuration error to accidentally nuke every member of a team.
  7. In general, I think it's a good idea to create an audit log so all actions taken by the script are output to a log file, which a human can review. That file can then be stored somewhere in case of a bug or error, to make it easier to fix things.
  8. The script right now asks for your credentials (twice, since there are two different APIs being used). There are probably some PowerShell best practices for storing credentials in non-interactive mode, or somehow leveraging the OS credentials. Hard-coding credentials into the script is a shortcut but seems like a bad idea.
  9. As noted earlier, to use this in production, you'll probably want to make the script run from a configuration file containing a list of pairs of security group ID plus Office 365 group ID. You can get those IDs using some of the same API calls in the sample script (like building a separate script just for that), or via Graph Explorer for Office 365 or Graph Explorer for Azure AD.

 

And there you go! Use the comments to let me know how it works, suggest improvements, link to your own solutions, and more. Just remember not to add Stewart to any of your teams.

 

About the author: Dan Stevenson was one of the early creators of Microsoft Teams. He led the product management team behind key features like teams and channels, guest access, Teams for education, and the free version of Teams. He recently moved with his family to Taipei, Taiwan, where he leads Teams customer engineering for the Asia Pacific region.

By |2018-09-05T04:44:39+00:00September 4th, 2018|Uncategorized|0 Comments