From 2eb5c9969b9a4dc500c7c9ed82e3d87431c2c59f Mon Sep 17 00:00:00 2001 From: James Smith | Regional IT Australia Date: Tue, 21 Apr 2026 14:14:16 +1000 Subject: [PATCH] Add explicit owner group support for Teams and Channels --- .../config_group_team_mapping.json | 15 +- .../M365 Teams Membership Sync.ps1 | 297 ++++++++++++++---- Microsoft 365/Teams Membership Sync/README.md | 15 +- 3 files changed, 266 insertions(+), 61 deletions(-) diff --git a/Microsoft 365/Teams Membership Sync/Config Templates/config_group_team_mapping.json b/Microsoft 365/Teams Membership Sync/Config Templates/config_group_team_mapping.json index 4fc3d72..5f39360 100644 --- a/Microsoft 365/Teams Membership Sync/Config Templates/config_group_team_mapping.json +++ b/Microsoft 365/Teams Membership Sync/Config Templates/config_group_team_mapping.json @@ -15,6 +15,13 @@ "M365_Group_DisplayName": "Administrative Assistants", "M365_Group_ID": "xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" } + ], + "OwnerGroups": + [ + { + "M365_Group_DisplayName": "Administration Managers", + "M365_Group_ID": "xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + } ] }, { @@ -29,6 +36,9 @@ "M365_Group_DisplayName": "Business", "M365_Group_ID": "xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" } + ], + "OwnerGroups": + [ ] }, { @@ -43,6 +53,9 @@ "M365_Group_DisplayName": "Payroll Managers", "M365_Group_ID": "xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" } + ], + "OwnerGroups": + [ ] }, { @@ -57,4 +70,4 @@ } ] } -] \ No newline at end of file +] diff --git a/Microsoft 365/Teams Membership Sync/M365 Teams Membership Sync.ps1 b/Microsoft 365/Teams Membership Sync/M365 Teams Membership Sync.ps1 index 07ad54d..b2cb82c 100644 --- a/Microsoft 365/Teams Membership Sync/M365 Teams Membership Sync.ps1 +++ b/Microsoft 365/Teams Membership Sync/M365 Teams Membership Sync.ps1 @@ -12,7 +12,7 @@ # This Script Does the Following: # 1. Adds and, optionally, removes users to/from Teams, Team Channels, & M365 Groups based on their M365 or Azure AD group membership. -# 2. Updates member ownership role for current Team & Channel members, if necessary. +# 2. Updates Team & Channel ownership role for members based on mapped owner groups or per-group owner role settings, if necessary. # 3. Optionally, logs information, errors, warnings, & debug data. # 4. Optionally, emails alert messages on errors and/or warnings. @@ -612,9 +612,12 @@ try # Get group membership. # Get recursive/transitive user membership, if enabled. Otherwise, get direct user membership only. + [array]$MembershipSourceGroups = @($mapping.Groups | Where-Object { $null -ne $_ }) + [array]$OwnerSourceGroups = @($mapping.OwnerGroups | Where-Object { $null -ne $_ }) + [array]$SourceGroupDisplayNames = @($MembershipSourceGroups.M365_Group_DisplayName) + @($OwnerSourceGroups.M365_Group_DisplayName) $Members = [System.Collections.Generic.List[Object]]::new() $MemberRoles = [System.Collections.Generic.List[Object]]::new() - foreach ($mapGroup in $mapping.Groups) + foreach ($mapGroup in $MembershipSourceGroups) { if ($EnableGroupRecursion) { @@ -631,13 +634,48 @@ try if ($Members.Id -notcontains $listItemToAdd.Id) { $Members.Add($ListItemToAdd) + } + if (($mapGroup.Role -eq 'Owner') -and ($MemberRoles.MemberID -notcontains $ListItemToAdd.Id)) + { $MemberRole = New-Object System.Object $MemberRole | Add-Member -MemberType NoteProperty -Name "TeamDisplayName" -Value $mapping.M365_Team_DisplayName $MemberRole | Add-Member -MemberType NoteProperty -Name "TeamID" -Value $mapping.M365_Team_ID $MemberRole | Add-Member -MemberType NoteProperty -Name "MemberID" -Value $ListItemToAdd.Id $MemberRole | Add-Member -MemberType NoteProperty -Name "MemberDisplayName" -Value $ListItemToAdd.AdditionalProperties.displayName - $MemberRole | Add-Member -MemberType NoteProperty -Name "Role" -Value $mapGroup.Role + $MemberRole | Add-Member -MemberType NoteProperty -Name "Role" -Value 'Owner' + $MemberRoles.Add($MemberRole) + } + } + } + + foreach ($ownerGroup in $OwnerSourceGroups) + { + if ($EnableGroupRecursion) + { + $ListItemsToAdd = Get-MgGroupTransitiveMember -GroupId $ownerGroup.M365_Group_ID -All| Select-Object * + } + else + { + $ListItemsToAdd = Get-MgGroupMember -GroupId $ownerGroup.M365_Group_ID -All | Select-Object * + } + + foreach ($listItemToAdd in $ListItemsToAdd) + { + # Add if not already in the list. + if ($Members.Id -notcontains $listItemToAdd.Id) + { + $Members.Add($ListItemToAdd) + } + + if ($MemberRoles.MemberID -notcontains $ListItemToAdd.Id) + { + $MemberRole = New-Object System.Object + $MemberRole | Add-Member -MemberType NoteProperty -Name "TeamDisplayName" -Value $mapping.M365_Team_DisplayName + $MemberRole | Add-Member -MemberType NoteProperty -Name "TeamID" -Value $mapping.M365_Team_ID + $MemberRole | Add-Member -MemberType NoteProperty -Name "MemberID" -Value $ListItemToAdd.Id + $MemberRole | Add-Member -MemberType NoteProperty -Name "MemberDisplayName" -Value $ListItemToAdd.AdditionalProperties.displayName + $MemberRole | Add-Member -MemberType NoteProperty -Name "Role" -Value 'Owner' $MemberRoles.Add($MemberRole) } } @@ -652,6 +690,7 @@ try # Log debug info, if enabled. if ($LoggingEnabled -and $LogDebugInfo) {Write-PSFMessage -Level Debug -Message "Desired Users: $($Users.AdditionalProperties.userPrincipalName -join ', ')"} if ($LoggingEnabled -and $LogDebugInfo) {Write-PSFMessage -Level Debug -Message "Desired Groups: $($Groups.AdditionalProperties.displayName -join ', ')"} + if ($LoggingEnabled -and $LogDebugInfo) {Write-PSFMessage -Level Debug -Message "Desired Owners: $($($MemberRoles | Select-Object -ExpandProperty MemberDisplayName) -join ', ')"} if ($LoggingEnabled -and $LogDebugInfo) {Write-PSFMessage -Level Debug -Message "Current Team Members (Email): $($CurrentTeamMembers.AdditionalProperties.email -join ', ')"} # Add users if there is at least one user in the mapped groups. @@ -716,17 +755,55 @@ try } else { - if ($LoggingEnabled) {Write-PSFMessage -Level Important -Message "No users in group mapping for Team `'$($mapping.M365_Team_DisplayName)`' & group(s): $($mapping.Groups.M365_Group_DisplayName -join ", ")"} + if ($LoggingEnabled) {Write-PSFMessage -Level Important -Message "No users in group mapping for Team `'$($mapping.M365_Team_DisplayName)`' & group(s): $($SourceGroupDisplayNames -join ", ")"} } + # Refresh current Team members to include any newly-added users and owner roles. + [array]$CurrentTeamMembers = Get-MgTeamMember -TeamId $mapping.M365_Team_ID -All + # Update Existing Team Members - # Remove Team members, if enabled in config. - # Also Add/Remove Team member Owner role, if needed. + # Add Team member Owner role first so that last-owner removals don't fail when another mapped member should be promoted. + # Then remove extra Team members, if enabled in config, and remove Team member Owner role, if needed. # Note: This property contains additional qualifiers only when relevant - for example, if the member has owner privileges, the roles property contains owner as one of the values. # Similarly, if the member is an in-tenant guest, the roles property contains guest as one of the values. # A basic member should not have any values specified in the roles property. An Out-of-tenant external member is assigned the owner role. # More info > https://learn.microsoft.com/en-us/powershell/module/microsoft.graph.teams/update-mgteammember + foreach ($currentTeamMember in $CurrentTeamMembers) + { + # Skip excluded accounts indicated by config. + if ($MemberRemovalExclusions.Id -contains $currentTeamMember.AdditionalProperties.userId) + { + continue + } + + # Only promote users that are meant to remain Team members. + if ($Users.Id -contains $currentTeamMember.AdditionalProperties.userId) + { + $MemberIsCurrentOwner = if ($($currentTeamMember).Roles -contains 'owner') {$true} else {$false} + $MemberMappedRoles = $MemberRoles | Where-Object -Property MemberID -EQ $($currentTeamMember.AdditionalProperties.userId) + $MemberShouldBeOwner = if ($($MemberMappedRoles).Role -eq 'Owner') {$true} else {$false} + + if ((-not $MemberIsCurrentOwner) -and ($MemberShouldBeOwner)) + { + [array]$NewRolesValue = $currentTeamMember.Roles + 'owner' + + $Parameters = @{ + "@odata.type" = "#microsoft.graph.aadUserConversationMember" + roles = @( + $NewRolesValue + ) + } + + if ($LoggingEnabled) {Write-PSFMessage -Level Significant -Message "Adding ownership role for Team '$($mapping.M365_Team_DisplayName)' member: $($currentTeamMember.AdditionalProperties.email)"} + $UpdateTeamMemberResult = Update-MgTeamMember -ConversationMemberId $currentTeamMember.Id -TeamId $mapping.M365_Team_ID -BodyParameter $Parameters -ErrorAction Stop + } + } + } + + # Refresh current Team members after owner promotions so last-owner checks operate on current state. + [array]$CurrentTeamMembers = Get-MgTeamMember -TeamId $mapping.M365_Team_ID -All + foreach ($currentTeamMember in $CurrentTeamMembers) { # Skip excluded accounts indicated by config. @@ -741,7 +818,24 @@ try if (($Users.Id -notcontains $currentTeamMember.AdditionalProperties.userId)) { if ($LoggingEnabled) {Write-PSFMessage -Level Significant -Message "Removing member from Team `'$($mapping.M365_Team_DisplayName)`': $($currentTeamMember.DisplayName)"} - Remove-MgTeamMember -TeamId $mapping.M365_Team_ID -ConversationMemberId $currentTeamMember.Id + + try + { + Remove-MgTeamMember -TeamId $mapping.M365_Team_ID -ConversationMemberId $currentTeamMember.Id -ErrorAction Stop + } + catch + { + if ($_.Exception.Message -match 'last owner') + { + if ($LoggingEnabled) {Write-PSFMessage -Level Warning -Message "Cannot remove member from Team `'$($mapping.M365_Team_DisplayName)`': $($currentTeamMember.DisplayName). They are currently the last owner of the Team."} + $CustomWarningMessage += "`nWARNING: Cannot remove member from Team `'$($mapping.M365_Team_DisplayName)`': $($currentTeamMember.DisplayName). They are currently the last owner of the Team." + continue + } + else + { + throw $_ + } + } # Go to next member because no need to update ownership. continue @@ -755,41 +849,39 @@ try # Remove Owner role, if necessary if (($MemberIsCurrentOwner) -and (-not $MemberShouldBeOwner)) { - [array]$NewRolesValue = foreach ($currentMemberRole in $currentTeamMember.Roles) + [array]$NewRolesValue = @(foreach ($currentMemberRole in $currentTeamMember.Roles) { if ($currentMemberRole -ne 'owner') { $currentMemberRole } - } + }) $Parameters = @{ "@odata.type" = "#microsoft.graph.aadUserConversationMember" - roles = @( - $NewRolesValue - ) + roles = $NewRolesValue } if ($LoggingEnabled) {Write-PSFMessage -Level Significant -Message "Removing ownership role for Team '$($mapping.M365_Team_DisplayName)' member: $($currentTeamMember.AdditionalProperties.email)"} - $UpdateTeamMemberResult = Update-MgTeamMember -ConversationMemberId $currentTeamMember.Id -TeamId $mapping.M365_Team_ID -BodyParameter $Parameters - } - # Add Owner role, if necessary - if ((-not $MemberIsCurrentOwner) -and ($MemberShouldBeOwner)) - { - [array]$NewRolesValue = $currentTeamMember.Roles + 'owner' - - $Parameters = @{ - "@odata.type" = "#microsoft.graph.aadUserConversationMember" - roles = @( - $NewRolesValue - ) + try + { + $UpdateTeamMemberResult = Update-MgTeamMember -ConversationMemberId $currentTeamMember.Id -TeamId $mapping.M365_Team_ID -BodyParameter $Parameters -ErrorAction Stop + } + catch + { + if ($_.Exception.Message -match 'last owner') + { + if ($LoggingEnabled) {Write-PSFMessage -Level Warning -Message "Cannot remove ownership role for Team '$($mapping.M365_Team_DisplayName)' member: $($currentTeamMember.AdditionalProperties.email). They are currently the last owner of the Team."} + $CustomWarningMessage += "`nWARNING: Cannot remove ownership role for Team '$($mapping.M365_Team_DisplayName)' member: $($currentTeamMember.AdditionalProperties.email). They are currently the last owner of the Team." + continue + } + else + { + throw $_ + } } - - if ($LoggingEnabled) {Write-PSFMessage -Level Significant -Message "Adding ownership role for Team '$($mapping.M365_Team_DisplayName)' member: $($currentTeamMember.AdditionalProperties.email)"} - $UpdateTeamMemberResult = Update-MgTeamMember -ConversationMemberId $currentTeamMember.Id -TeamId $mapping.M365_Team_ID -BodyParameter $Parameters } - } } @@ -806,9 +898,12 @@ try # Get group membership. # Get recursive/transitive user membership, if enabled. Otherwise, get direct user membership only. + [array]$MembershipSourceGroups = @($mapping.Groups | Where-Object { $null -ne $_ }) + [array]$OwnerSourceGroups = @($mapping.OwnerGroups | Where-Object { $null -ne $_ }) + [array]$SourceGroupDisplayNames = @($MembershipSourceGroups.M365_Group_DisplayName) + @($OwnerSourceGroups.M365_Group_DisplayName) $Members = [System.Collections.Generic.List[Object]]::new() $MemberRoles = [System.Collections.Generic.List[Object]]::new() - foreach ($mapGroup in $mapping.Groups) + foreach ($mapGroup in $MembershipSourceGroups) { if ($EnableGroupRecursion) { @@ -825,7 +920,10 @@ try if ($Members.Id -notcontains $listItemToAdd.Id) { $Members.Add($ListItemToAdd) + } + if (($mapGroup.Role -eq 'Owner') -and ($MemberRoles.MemberID -notcontains $ListItemToAdd.Id)) + { $MemberRole = New-Object System.Object $MemberRole | Add-Member -MemberType NoteProperty -Name "TeamDisplayName" -Value $mapping.M365_Team_DisplayName $MemberRole | Add-Member -MemberType NoteProperty -Name "TeamID" -Value $mapping.M365_Team_ID @@ -833,7 +931,41 @@ try $MemberRole | Add-Member -MemberType NoteProperty -Name "ChannelID" -Value $mapping.M365_Channel_ID $MemberRole | Add-Member -MemberType NoteProperty -Name "MemberID" -Value $ListItemToAdd.Id $MemberRole | Add-Member -MemberType NoteProperty -Name "MemberDisplayName" -Value $ListItemToAdd.AdditionalProperties.displayName - $MemberRole | Add-Member -MemberType NoteProperty -Name "Role" -Value $mapGroup.Role + $MemberRole | Add-Member -MemberType NoteProperty -Name "Role" -Value 'Owner' + $MemberRoles.Add($MemberRole) + } + } + } + + foreach ($ownerGroup in $OwnerSourceGroups) + { + if ($EnableGroupRecursion) + { + $ListItemsToAdd = Get-MgGroupTransitiveMember -GroupId $ownerGroup.M365_Group_ID -All | Select-Object * + } + else + { + $ListItemsToAdd = Get-MgGroupMember -GroupId $ownerGroup.M365_Group_ID -All | Select-Object * + } + + foreach ($listItemToAdd in $ListItemsToAdd) + { + # Add if not already in the list. + if ($Members.Id -notcontains $listItemToAdd.Id) + { + $Members.Add($ListItemToAdd) + } + + if ($MemberRoles.MemberID -notcontains $ListItemToAdd.Id) + { + $MemberRole = New-Object System.Object + $MemberRole | Add-Member -MemberType NoteProperty -Name "TeamDisplayName" -Value $mapping.M365_Team_DisplayName + $MemberRole | Add-Member -MemberType NoteProperty -Name "TeamID" -Value $mapping.M365_Team_ID + $MemberRole | Add-Member -MemberType NoteProperty -Name "ChannelDisplayName" -Value $mapping.M365_Channel_DisplayName + $MemberRole | Add-Member -MemberType NoteProperty -Name "ChannelID" -Value $mapping.M365_Channel_ID + $MemberRole | Add-Member -MemberType NoteProperty -Name "MemberID" -Value $ListItemToAdd.Id + $MemberRole | Add-Member -MemberType NoteProperty -Name "MemberDisplayName" -Value $ListItemToAdd.AdditionalProperties.displayName + $MemberRole | Add-Member -MemberType NoteProperty -Name "Role" -Value 'Owner' $MemberRoles.Add($MemberRole) } } @@ -849,6 +981,7 @@ try # Log debug info, if enabled. if ($LoggingEnabled -and $LogDebugInfo) {Write-PSFMessage -Level Debug -Message "Desired Users: $($Users.AdditionalProperties.userPrincipalName -join ', ')"} if ($LoggingEnabled -and $LogDebugInfo) {Write-PSFMessage -Level Debug -Message "Desired Groups: $($Groups.AdditionalProperties.displayName -join ', ')"} + if ($LoggingEnabled -and $LogDebugInfo) {Write-PSFMessage -Level Debug -Message "Desired Owners: $($($MemberRoles | Select-Object -ExpandProperty MemberDisplayName) -join ', ')"} if ($LoggingEnabled -and $LogDebugInfo) {Write-PSFMessage -Level Debug -Message "Current Channel Members (Email): $($CurrentChannelMembers.AdditionalProperties.email -join ', ')"} # Add users if there is at least one user in the mapped groups. @@ -914,16 +1047,54 @@ try } else { - if ($LoggingEnabled) {Write-PSFMessage -Level Important -Message "No users in group mapping for Channel `'$($mapping.M365_Team_DisplayName)\$($mapping.M365_Channel_DisplayName)`' & group(s): $($mapping.Groups.M365_Group_DisplayName -join ", ")"} + if ($LoggingEnabled) {Write-PSFMessage -Level Important -Message "No users in group mapping for Channel `'$($mapping.M365_Team_DisplayName)\$($mapping.M365_Channel_DisplayName)`' & group(s): $($SourceGroupDisplayNames -join ", ")"} } + # Refresh current Channel members to include any newly-added users and owner roles. + [array]$CurrentChannelMembers = Get-MgTeamChannelMember -TeamId $mapping.M365_Team_ID -ChannelId $mapping.M365_Channel_ID -All + # Update Existing Channel Members - # Remove Channel members, if enabled in config. - # Also Add/Remove Channel member Owner role, if needed. + # Add Channel member Owner role first so that last-owner removals don't fail when another mapped member should be promoted. + # Then remove Channel members, if enabled in config, and remove Channel member Owner role, if needed. # Note: This property contains additional qualifiers only when relevant - for example, if the member has owner privileges, the roles property contains owner as one of the values. # Similarly, if the member is an in-tenant guest, the roles property contains guest as one of the values. # A basic member should not have any values specified in the roles property. An Out-of-tenant external member is assigned the owner role. # More info > https://learn.microsoft.com/en-us/powershell/module/microsoft.graph.teams/update-mgteamchannelmember + foreach ($currentChannelMember in $CurrentChannelMembers) + { + # Skip excluded accounts indicated by config. + if ($MemberRemovalExclusions.Id -contains $currentChannelMember.AdditionalProperties.userId) + { + continue + } + + # Only promote users that are meant to remain Channel members. + if ($Users.Id -contains $currentChannelMember.AdditionalProperties.userId) + { + $MemberIsCurrentOwner = if ($($currentChannelMember).Roles -contains 'owner') {$true} else {$false} + $MemberMappedRoles = $MemberRoles | Where-Object -Property MemberID -EQ $($currentChannelMember.AdditionalProperties.userId) + $MemberShouldBeOwner = if ($($MemberMappedRoles).Role -eq 'Owner') {$true} else {$false} + + if ((-not $MemberIsCurrentOwner) -and ($MemberShouldBeOwner)) + { + [array]$NewRolesValue = $currentChannelMember.Roles + 'owner' + + $Parameters = @{ + "@odata.type" = "#microsoft.graph.aadUserConversationMember" + roles = @( + $NewRolesValue + ) + } + + if ($LoggingEnabled) {Write-PSFMessage -Level Significant -Message "Adding ownership role for Channel '$($mapping.M365_Team_DisplayName)\$($mapping.M365_Channel_DisplayName)' member: $($currentChannelMember.AdditionalProperties.email)"} + $UpdateChannelMemberResult = Update-MgTeamChannelMember -ConversationMemberId $currentChannelMember.Id -TeamId $mapping.M365_Team_ID -ChannelId $mapping.M365_Channel_ID -BodyParameter $Parameters -ErrorAction Stop + } + } + } + + # Refresh current Channel members after owner promotions so last-owner checks operate on current state. + [array]$CurrentChannelMembers = Get-MgTeamChannelMember -TeamId $mapping.M365_Team_ID -ChannelId $mapping.M365_Channel_ID -All + foreach ($currentChannelMember in $CurrentChannelMembers) { # Skip excluded accounts indicated by config. @@ -937,7 +1108,24 @@ try if ($Users.Id -notcontains $currentChannelMember.AdditionalProperties.userId) { if ($LoggingEnabled) {Write-PSFMessage -Level Significant -Message "Removing member from Channel `'$($mapping.M365_Team_DisplayName)\$($mapping.M365_Channel_DisplayName)`': $($currentChannelMember.DisplayName)"} - Remove-MgTeamChannelMember -TeamId $mapping.M365_Team_ID -ChannelId $mapping.M365_Channel_ID -ConversationMemberId $currentChannelMember.Id + + try + { + Remove-MgTeamChannelMember -TeamId $mapping.M365_Team_ID -ChannelId $mapping.M365_Channel_ID -ConversationMemberId $currentChannelMember.Id -ErrorAction Stop + } + catch + { + if ($_.Exception.Message -match 'last owner') + { + if ($LoggingEnabled) {Write-PSFMessage -Level Warning -Message "Cannot remove member from Channel `'$($mapping.M365_Team_DisplayName)\$($mapping.M365_Channel_DisplayName)`': $($currentChannelMember.DisplayName). They are currently the last owner of the Channel."} + $CustomWarningMessage += "`nWARNING: Cannot remove member from Channel `'$($mapping.M365_Team_DisplayName)\$($mapping.M365_Channel_DisplayName)`': $($currentChannelMember.DisplayName). They are currently the last owner of the Channel." + continue + } + else + { + throw $_ + } + } # Go to next member because no need to update ownership. continue @@ -951,39 +1139,38 @@ try # Remove Owner role, if necessary if (($MemberIsCurrentOwner) -and (-not $MemberShouldBeOwner)) { - [array]$NewRolesValue = foreach ($currentMemberRole in $currentChannelMember.Roles) + [array]$NewRolesValue = @(foreach ($currentMemberRole in $currentChannelMember.Roles) { if ($currentMemberRole -ne 'owner') { $currentMemberRole } - } + }) $Parameters = @{ "@odata.type" = "#microsoft.graph.aadUserConversationMember" - roles = @( - $NewRolesValue - ) + roles = $NewRolesValue } if ($LoggingEnabled) {Write-PSFMessage -Level Significant -Message "Removing ownership role for Channel '$($mapping.M365_Team_DisplayName)\$($mapping.M365_Channel_DisplayName)' member: $($currentChannelMember.AdditionalProperties.email)"} - $UpdateChannelMemberResult = Update-MgTeamChannelMember -ConversationMemberId $currentChannelMember.Id -TeamId $mapping.M365_Team_ID -ChannelId $mapping.M365_Channel_ID -BodyParameter $Parameters - } - - # Add Owner role, if necessary - if ((-not $MemberIsCurrentOwner) -and ($MemberShouldBeOwner)) - { - [array]$NewRolesValue = $currentChannelMember.Roles + 'owner' - $Parameters = @{ - "@odata.type" = "#microsoft.graph.aadUserConversationMember" - roles = @( - $NewRolesValue - ) + try + { + $UpdateChannelMemberResult = Update-MgTeamChannelMember -ConversationMemberId $currentChannelMember.Id -TeamId $mapping.M365_Team_ID -ChannelId $mapping.M365_Channel_ID -BodyParameter $Parameters -ErrorAction Stop + } + catch + { + if ($_.Exception.Message -match 'last owner') + { + if ($LoggingEnabled) {Write-PSFMessage -Level Warning -Message "Cannot remove ownership role for Channel '$($mapping.M365_Team_DisplayName)\$($mapping.M365_Channel_DisplayName)' member: $($currentChannelMember.AdditionalProperties.email). They are currently the last owner of the Channel."} + $CustomWarningMessage += "`nWARNING: Cannot remove ownership role for Channel '$($mapping.M365_Team_DisplayName)\$($mapping.M365_Channel_DisplayName)' member: $($currentChannelMember.AdditionalProperties.email). They are currently the last owner of the Channel." + continue + } + else + { + throw $_ + } } - - if ($LoggingEnabled) {Write-PSFMessage -Level Significant -Message "Adding ownership role for Channel '$($mapping.M365_Team_DisplayName)\$($mapping.M365_Channel_DisplayName)' member: $($currentChannelMember.AdditionalProperties.email)"} - $UpdateChannelMemberResult = Update-MgTeamChannelMember -ConversationMemberId $currentChannelMember.Id -TeamId $mapping.M365_Team_ID -ChannelId $mapping.M365_Channel_ID -BodyParameter $Parameters } } } @@ -1088,4 +1275,4 @@ catch Write-PSFMessage -Level Important -Message "---SCRIPT END---" Wait-PSFMessage # Make Sure Logging Is Flushed Before Terminating } -} \ No newline at end of file +} diff --git a/Microsoft 365/Teams Membership Sync/README.md b/Microsoft 365/Teams Membership Sync/README.md index c67325c..d6bf781 100644 --- a/Microsoft 365/Teams Membership Sync/README.md +++ b/Microsoft 365/Teams Membership Sync/README.md @@ -8,7 +8,8 @@ A PowerShell script that syncs members of Microsoft 365 and Azure AD groups to M ## Features -- Adds mapped group members to Teams and Channels (Private Channels only). +- Adds mapped group members to Teams, and Channels (Private Channels only). +- Adds mapped owner/admin groups to Teams and Channels. - Optionally removes members who no longer are mapped to a Team or Channel (allows for user exceptions if enabled). - Optionally allows for group recursion/nesting. - Written to take advantage of the latest Microsoft Microsoft Graph API PowerShell module. @@ -100,16 +101,20 @@ Debugging ### **config_group_team_mapping.json** -JSON file that contains an array of Teams and/or Team Channels (Private Channels only) that you want to add group members to. You should only have one entry for each Team or Channel. When mapping Private Channel memberships, you need to specify which Team the Channel belongs to. +JSON file that contains an array of Teams, Team Channels (Private Channels only), and/or M365 Groups that you want to sync group memberships to. You should only have one entry for each Team, Channel, or Group. When mapping Private Channel memberships, you need to specify which Team the Channel belongs to. -- **MapType (String):** Specify whether the included groups are being given access to a Team or a Channel. Use 'Team' or 'Channel'. +- **MapType (String):** Specify whether the included groups are being given access to a Team, Channel, or Group. Use 'Team', 'Channel', or 'Group'. - **M365_Team_DisplayName (String):** Optionally, enter a name for the Team. This field is only used to more easily identify the Team when looking at the config file. - **M365_Team_ID (String):** Enter the Team ID. You can find it using [Get-MgTeam](https://learn.microsoft.com/en-us/powershell/module/microsoft.graph.teams/get-mgteam) or in the [Teams Admin Center](https://admin.teams.microsoft.com/teams/manage) (there it's called the 'Group ID'). - **M365_Channel_DisplayName (String):** Only used when 'MapType' is set to 'Channel'. Optionally, enter a name for the Channel. This field is only used to more easily identify the Channel when looking at the config file. - **M365_Channel_ID (String):** Enter the Channel ID. You can find it using [Get-MgTeamChannel](https://learn.microsoft.com/en-us/powershell/module/microsoft.graph.teams/get-mgteamchannel) or in the [Teams Admin Center](https://admin.teams.microsoft.com/teams/manage) (you can find it when inside the channel by looking at the URL in your web browser and copying everything after '/channels/'). E.g., "19:aac3e13cd5f99827b60cdb0b6df37a3e@thread.tacv2". -- **Groups (Array):** Array containing the following fields for *each* group. You can map zero (if you want a placeholder for a Team/Channel) or more groups to a Team or Channel. +- **M365_Group_DisplayName (String):** Only used when 'MapType' is set to 'Group'. Optionally, enter a name for the Group. This field is only used to more easily identify the Group when looking at the config file. +- **M365_Group_ID (String):** Only used when 'MapType' is set to 'Group'. Enter the Group ID. You can find it using [Get-MgGroup](https://learn.microsoft.com/en-us/powershell/module/microsoft.graph.groups/get-mggroup) or, for Azure AD groups only, in the [Azure AD Admin Center](https://aad.portal.azure.com/#view/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/~/Groups). +- **Groups (Array):** Array containing the following fields for *each* member group. You can map zero (if you want a placeholder for a Team/Channel/Group) or more groups to a Team, Channel, or Group. +- **OwnerGroups (Array):** Optional array containing the following fields for *each* owner/admin group. This is used for Team and Channel mappings only. Users from these groups are also added as members and are then assigned the owner role. You can map zero or more owner/admin groups. - **M365_Group_DisplayName (String):** Optionally, enter a name for the group. This field is only used to more easily identify the group when looking at the config file. - **M365_Group_ID (String):** Enter the group ID. You can find it using [Get-MgGroup](https://learn.microsoft.com/en-us/powershell/module/microsoft.graph.groups/get-mggroup) or, for Azure AD groups only, in the [Azure AD Admin Center](https://aad.portal.azure.com/#view/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/~/Groups). +- **Role (String):** Optional field on a `Groups` entry. If you set this to `Owner` for a Team or Channel mapping, the script will also assign the owner role for users from that source group. `OwnerGroups` is the newer explicit alternative, but this field remains supported for backward compatibility. --- @@ -118,4 +123,4 @@ JSON file that contains an array of Teams and/or Team Channels (Private Channels JSON file that contains an array of users who should not be removed from Teams or Private Channels, even if they no longer are a member of a mapped group. This only applies when 'RemoveExtraTeamMembers' or 'RemoveExtraChannelMembers' is set to true. Create an array entry for *each* user you want to exclude. - **UserPrincipalName (String):** Optionally, enter the UPN for the user you want a removal exception for. This field is only used to more easily identify the user when looking at the config file. -- **Id (String):"** Enter the user ID. You can find it using [Get-MgUser](https://learn.microsoft.com/en-us/powershell/module/microsoft.graph.users/get-mguser) or, for Azure AD users only, in the [Azure AD Admin Center](https://aad.portal.azure.com/#view/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/~/Users). \ No newline at end of file +- **Id (String):"** Enter the user ID. You can find it using [Get-MgUser](https://learn.microsoft.com/en-us/powershell/module/microsoft.graph.users/get-mguser) or, for Azure AD users only, in the [Azure AD Admin Center](https://aad.portal.azure.com/#view/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/~/Users).