O365 - RBAC Role Groups Based on AD Groups

I still haven't had the time to implement this in my test domain but there may be a viable solution to RBAC based on AD groups through one of the two options below:

This is all based on https://community.powerbi.com/t5/Community-Blog/Power-BI-Group-management-using-Active-Directory-roles-and/ba-p/308092

Set-ExecutionPolicy Unrestricted -Scope CurrentUser -Force

# Get security credential based on a user name and password
$User_Credential = Get-Credential

# Get Exchange cmdlets
$Session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri https://outlook.office365.com/powershell-liveid/ -Credential $User_Credential -Authentication Basic -AllowRedirection
Import-PSSession $Session -Allowclobber

# Store Role and Group names; change as required
$AD_Role_Name = "AD Role name here"
$O365_Group_Name = "O365 Group name here"

# Get AD role membership
$Active_Directory_Role = get-group -Identity $AD_Role_Name | Select-Object -Property Members | foreach { $_.Members } | Sort-Object

# Get O365 Group membership
$Office_365_Group = get-group -Identity $O365_Group_Name | Select-Object -Property Members | foreach { $_.Members } | Sort-Object

# Find and store new AD role users
$Add_O365_Group = Compare-Object $Active_Directory_Role $Office_365_Group -PassThru | Left_Side

# Add new AD role users
$Add_O365_Group | % {Add-UnifiedGroupLinks -Identity $O365_Group_Name -LinkType Members -links "$_"}

# Debug print users
Write-Host "Added the following users: " $Add_O365_Group -foregroundcolor black -backgroundColor Green

# Find and store old AD role users; switch filter position of AD_Role and O365_Group
$Remove_O365_Group = Compare-Object $Office_365_Group $Active_Directory_Role -PassThru | Left_Side

# Remove old AD role users
$Remove_O365_Group | % {Remove-UnifiedGroupLinks -Identity $O365_Group_Name -LinkType Members -links "$_"}

# Debug print users
Write-Host "Removed the following users: " $Remove_O365_Group -foregroundcolor black -backgroundColor Yellow

This is all based on https://techcommunity.microsoft.com/t5/Microsoft-Teams-Blog/Syncing-Security-Groups-with-team-membership/ba-p/241959 author is Dan Stevenson

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

# 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.