public/Set-EntraIdGroup.ps1
<# .SYNOPSIS Ensures an Entra ID (Azure AD) group exists with the specified name, description, and members (UPNs). .DESCRIPTION This function checks if a group with the given name exists. If not, it creates the group with the specified name and description. It then ensures the group description is correct and synchronizes the group membership to match the provided list of UPNs (adding missing members and removing extra ones). Owners can also be specified and synchronized. .PARAMETER GroupName The display name of the Entra ID group to ensure exists and is configured. .PARAMETER Description The description to set on the group. .PARAMETER Members An array of user principal names (UPNs) to be members of the group. .PARAMETER Owners An array of user principal names (UPNs) to be owners of the group. .PARAMETER GroupMembershipType Specifies the type of group membership. Valid values are "Direct" or "Dynamic". Defaults to "Direct". .PARAMETER IsAssignableToRole Indicates whether the group can be assigned to a role. Defaults to $false. .EXAMPLE Set-EntraIdGroup -DisplayName "Platform Admins" -Description "Admins for the platform" -Members @("alice@contoso.com", "bob@contoso.com") -Owners @("carol@contoso.com") Ensures the group "Platform Admins" exists with the specified description, members, and owners. .EXAMPLE Set-EntraIdGroup -DisplayName "Dynamic Group" -Description "Dynamic membership group" -GroupMembershipType "Dynamic" -Members @("(user.department -eq 'IT')") Creates or updates a dynamic group with the specified membership rule. .NOTES Author: Remco Vermeer Date: 10 September 2025 #> function Set-EntraIdGroup { [CmdletBinding(SupportsShouldProcess = $true)] param( [Parameter(Mandatory)] [string]$DisplayName, [Parameter(Mandatory = $false)] [ValidateSet("Direct", "Dynamic")] [string]$GroupMembershipType = "Direct", [Parameter(Mandatory)] [string]$Description, [Parameter(Mandatory = $false)] [array]$Members, [Parameter(Mandatory)] [array]$Owners, [Parameter(Mandatory = $false)] [bool]$IsAssignableToRole = $false ) process { Test-GraphAuth Write-Output "Processing Group: '$DisplayName'" $newGroup = $false $updateRequired = $false # Check if group exists $group = Get-MgGroup -Filter "displayName eq '$DisplayName'" | Select-Object -First 1 if (-not $group) { # Common group parameters $newGroupParams = @{ DisplayName = $DisplayName Description = $Description MailEnabled = $false MailNickname = $DisplayName.Replace(' ', '') SecurityEnabled = $true IsAssignableToRole = $IsAssignableToRole } switch ($GroupMembershipType) { "Direct" { # No extra params needed } "Dynamic" { Write-Output "Creating dynamic group with rule: $Members[0]" $newGroupParams.GroupTypes = @("DynamicMembership") $newGroupParams.MembershipRule = $Members[0] # Assuming Members contains the rule as a string $newGroupParams.MembershipRuleProcessingState = "On" } default { throw "Unsupported GroupMembershipType: $GroupMembershipType" } } $newGroup = $true if ($PSCmdlet.ShouldProcess("Group '$DisplayName'", "Create group with parameters $($newGroupParams | ConvertTo-Json)")) { $group = New-MgGroup @newGroupParams Write-Verbose "Created group with ID: $($group.Id)" Write-Output "Created group '$DisplayName' ($GroupMembershipType membership)" } } else { $updateRequired = $false # Update description if needed if ($group.Description -ne $Description) { $updateParams = @{ GroupId = $group.Id Description = $Description } $updateRequired = $true if ($PSCmdlet.ShouldProcess("Group '$DisplayName'", "Update group description to '$Description'")) { Update-MgGroup @updateParams Write-Output "Updated description for group '$DisplayName'" } } if ($group.IsAssignableToRole -ne $IsAssignableToRole) { Write-Warning "IsAssignableToRole cannot be changed after group creation. Please delete and recreate the group if needed." } } # Synchronize members only for Direct groups if ($GroupMembershipType -eq "Direct") { if (-not $newGroup) { $currentMembers = Get-EntraIdGroupMember -GroupDisplayName $DisplayName Write-Verbose "Fetched group: $DisplayName current members. $($currentMembers | ConvertTo-Json -Depth 3)" } else { $currentMembers = @() Write-Verbose "New group created, no current members." } # Add missing members if ($Members) { $toAdd = $Members | Where-Object { $_ -notin $currentMembers } } # Remove extra members if ($Members) { $toRemove = $currentMembers | Where-Object { $_ -notin $Members } } else { $toRemove = $currentMembers } if ($toAdd.Count -gt 0) { $updateRequired = $true if ($PSCmdlet.ShouldProcess("Group '$DisplayName'", "Add Members $($toAdd | ConvertTo-Json)")) { Add-EntraIdGroupMember -GroupDisplayName $DisplayName -Members $toAdd } } if ($toRemove.Count -gt 0) { $updateRequired = $true if ($PSCmdlet.ShouldProcess("Group '$DisplayName'", "Remove Members $($toRemove | ConvertTo-Json)")) { Remove-EntraIdGroupMember -GroupDisplayName $DisplayName -Members $toRemove } } } # Get current owners (UPNs) $currentOwners = Get-EntraIdGroupOwner -GroupDisplayName $DisplayName $toAddOwners = $Owners | Where-Object { $_ -notin $currentOwners } $toRemoveOwners = $currentOwners | Where-Object { $_ -notin $Owners } if ($toAddOwners.Count -gt 0) { $updateRequired = $true if ($PSCmdlet.ShouldProcess("Group '$DisplayName'", "Add Owners $($toAddOwners | ConvertTo-Json)")) { Add-EntraIdGroupOwner -GroupDisplayName $DisplayName -Owners $toAddOwners } } if ($toRemoveOwners.Count -gt 0) { $updateRequired = $true if ($PSCmdlet.ShouldProcess("Group '$DisplayName'", "Remove Owners $($toRemoveOwners | ConvertTo-Json)")) { Remove-EntraIdGroupOwner -GroupDisplayName $DisplayName -Owners $toRemoveOwners } } if (-not $updateRequired) { Write-Output "Group '$DisplayName' is already in the desired state." } Write-Output "" } } |