Script to Backup and Restore Distribution Group Members and Owners Using ImmutableId in Azure AD

10 min. readlast update: 09.26.2024

This script facilitates the backup and restoration of Azure AD distribution group members and owners by ImmutableId across two different tenants. The script retrieves distribution groups from the source tenant, along with their members and owners, and exports them into XML files. It then restores the groups in the destination tenant, comparing and syncing members and owners based on their ImmutableId. The script also includes functions for renaming backup folders and replacing tenant-specific information in XML files during the migration process.

Here is the script:

# This script restore the members by ImmutableId!!!
$backupFolder = "$home\Documents\GroupsBackup"

# Source and destination tenant domain names
$sourceTenantDomain = ""
$sourceTenantDomainName =@("", "oXXXclXud.works")
$destinationTenantDomain = ""
$destinationTenantDomainName = ""
function New-DirectoryIfNotExist {
    param(
        [string]$Path
    )

    if (Test-Path -Path $Path -PathType Container) {
        $FolderProperty = Get-ItemProperty $Path
        $date = ($FolderProperty.CreationTime.ToString()).Split('/')
        $date += $date[2].Split(' ')
        $date += $date[4].Replace(':', '')

        $suffix = '-' + $date[3] + '-' + $date[1] + '-' + $date[5]
        $NewName = 'GroupsBackup' + $suffix
        Rename-Item -Path $Path -NewName $NewName
    }

    New-Item -ItemType Directory -Path $Path | Out-Null
    Write-Output "Directory '$Path' created."
}
function Get-UserInfo ($UserId) {
    $User = $Script:UsersCache | Where-Object {$_.Id -eq $UserId}
    if ($null -eq $User) {
        $User = Get-MgUser -UserId $UserId -Property Id,UserPrincipalName,OnPremisesImmutableId | Select-Object Id,UserPrincipalName,OnPremisesImmutableId
        $Script:UsersCache += $User
    }
    return $User
}
function Backup-Groups {
    $Script:UsersCache = @()
    New-DirectoryIfNotExist -Path $backupFolder

    try {
        Connect-ExchangeOnline -Organization $sourceTenantDomain -ErrorAction Stop
        Connect-MgGraph -TenantId "$sourceTenantDomain" -Scopes "User.Read.All","User.ReadBasic.All","Directory.Read.All" -ErrorAction Stop
    }
    catch {
        Write-Output "Failed to connect to the source tenant. Error: $($_.Exception.Message)"
        return
    }

    try {
        $allGroups = Get-DistributionGroup |
            Where-Object {$false -eq $_.IsDirSynced -and
            ("MailUniversalDistributionGroup" -eq $_.RecipientTypeDetails -or
            "MailUniversalSecurityGroup" -eq $_.RecipientTypeDetails)}
    }
    catch {
        Write-Output "Failed to retrieve groups. Error: $($_.Exception.Message)"
        return
    }

    foreach ($sourceGroup in $allGroups) {
        Write-Output "Backing up $($sourceGroup.DisplayName)"
        try {
            $sourceGroupMembers = @()
            $GroupMembers = Get-DistributionGroupMember -Identity $($sourceGroup.Guid) |
            Where-Object RecipientType -eq "UserMailbox" |
            Select-Object -ExpandProperty ExternalDirectoryObjectId -ErrorAction SilentlyContinue
            if ($GroupMembers){
                $GroupMembers | ForEach-Object {
                    $sourceGroupMembers += Get-UserInfo -UserId $_
                }
            }
            
            if ($sourceGroup.ManagedBy){
                $sourceGroupOwner = @()
                foreach ($GroupOwner in $($sourceGroup.ManagedBy)) {
                    $sourceGroupOwner += Get-UserInfo -UserId $GroupOwner
                } 
            }

            $exportPath = Join-Path -Path $backupFolder -ChildPath "$($sourceGroup.Guid)_$($sourceGroup.DisplayName)"

            $xmlPath = $exportPath + '_Info.xml'
            $sourceGroup | Export-Clixml -Path $xmlPath

            $xmlPath = $exportPath + '_members.xml'
            $sourceGroupMembers | Export-Clixml -Path $xmlPath

            $xmlPath = $exportPath + '_Owner.xml'
            $sourceGroupOwner | Export-Clixml -Path $xmlPath
        }
        catch {
            Write-Output "Failed to backup group $($sourceGroup.DisplayName). Error: $($_.Exception.Message)"
        }
    }
}
function Set-HashTableData ($Object){
    $hash = @{}
    $Object.psobject.properties | ForEach-Object {$hash[$_.Name] = $_.Value }
    
    return $hash
}
function Restore-Groups {
    $Script:UsersCache = @()
    if (Test-Path -Path $backupFolder -PathType Container) {
        $AllGroups = @()
        $xmlFiles = Get-ChildItem -Path $backupFolder

        $xmlFiles | Where-Object Name -Like "*_Info.xml" | ForEach-Object {
            $AllGroups += Import-Clixml -Path $_.FullName
        }

        try {
            Connect-ExchangeOnline -Organization $destinationTenantDomain -ErrorAction Stop
            Connect-MgGraph -TenantId "$destinationTenantDomain" -Scopes "User.Read.All","User.ReadBasic.All","Directory.Read.All" -ErrorAction Stop
        }
        catch {
            Write-Output "Failed to connect to the destination tenant. Error: $($_.Exception.Message)"
            return
        }

        $GroupsInfo = @()

        foreach ($sourceGroup in $AllGroups) {
            try {
                $destinationGroup = Get-DistributionGroup -Filter "Alias -eq '$($sourceGroup.Alias)'" -ErrorAction SilentlyContinue

                if ($null -eq $destinationGroup) {
                    if ($($sourceGroup.GroupType) -Like "*SecurityEnabled*"){
                        $sourceGroup | Add-Member -Name 'Type' -Value 'Security' -MemberType NoteProperty
                    }
                    #$sourceGroup = $sourceGroup |Select-Object -ExcludeProperty OrganizationalUnit,SamAccountName,Identity
                    $NewGroupHash = Set-HashTableData -Object ($sourceGroup |Select-Object Alias,Name,BccBlocked,BypassNestedModerationEnabled,CopyOwnerToMember,Description,DisplayName,MemberDepartRestriction,MemberJoinRestriction,ModeratedBy,ModerationEnabled,Notes,PrimarySmtpAddress,RequireSenderAuthenticationEnabled,SendModerationNotifications,Type)
                    Write-Output "Creating new group $($sourceGroup.Alias)"
                    $destinationGroup = New-DistributionGroup @NewGroupHash -ErrorAction Stop
                }

                $GroupsInfo += @{
                    sourceGroupID = $($sourceGroup.Guid);
                    GroupDisplayName = $($sourceGroup.DisplayName);
                    destinationGroupID = $($destinationGroup.Guid)
                }


                $exportPath = Join-Path -Path $backupFolder -ChildPath "$($sourceGroup.Guid)_$($sourceGroup.DisplayName)"
                $xmlPath = $exportPath + '_members.xml'
                $sourceGroupMembers = Import-Clixml -Path $xmlPath
                $destinationGroupMembers = @()
                $GroupMembers = Get-DistributionGroupMember -Identity $destinationGroup.Guid |
                Where-Object RecipientType -eq "UserMailbox" -ErrorAction SilentlyContinue |
                Select-Object -ExpandProperty ExternalDirectoryObjectId
                if ($GroupMembers){
                    $GroupMembers | ForEach-Object {
                        $destinationGroupMembers += Get-UserInfo -UserId $_
                    }
                }

                foreach ($sourceMember in $sourceGroupMembers) {
                    if ($sourceMember.OnPremisesImmutableId -notin $destinationGroupMembers.OnPremisesImmutableId) {
                        $userToAdd = Get-MgUser -Filter "OnPremisesImmutableId eq '$($sourceMember.OnPremisesImmutableId)'" -Property id,UserPrincipalName
                        
                        if ($userToAdd) {
                            Add-DistributionGroupMember -Identity $destinationGroup.Guid -Member $userToAdd.UserPrincipalName
                        }
                    }
                }

                foreach ($destinationMember in $destinationGroupMembers) {
                    if ($destinationMember.OnPremisesImmutableId -notin $sourceGroupMembers.OnPremisesImmutableId) {
                        Remove-DistributionGroupMember -Identity $destinationGroup.Guid -Member $destinationMember.UserPrincipalName
                    }
                }

                if($destinationGroup.ManagedBy){
                    $destinationGroupOwner =@()
                    foreach ($GroupOwner in $destinationGroup.ManagedBy) {
                        $destinationGroupOwner += Get-UserInfo -UserId $GroupOwner
                    }
                }

                $xmlPath = $exportPath + '_Owner.xml'
                $sourceGroupOwner = Import-Clixml -Path $xmlPath                
                foreach ($sourceOwner in $sourceGroupOwner) {
                    if ($sourceOwner.OnPremisesImmutableId -notin $destinationGroupOwner.OnPremisesImmutableId) {
                        $userToAdd = Get-MgUser -Filter "OnPremisesImmutableId eq '$($sourceOwner.OnPremisesImmutableId)'" -Property Id,UserPrincipalName
                        if ($userToAdd) {
                            Set-DistributionGroup -Identity $destinationGroup.Guid -ManagedBy @{Add="$($userToAdd.UserPrincipalName)"}
                        }
                    }
                }

                foreach ($destinationOwner in $destinationGroupOwner) {
                    if ($destinationOwner.ImmutableId -notin $sourceGroupOwner.ImmutableId) {
                        $userToRemove = Get-MgUser -Filter "OnPremisesImmutableId eq '$($sourceOwner.OnPremisesImmutableId)'" -Property Id,UserPrincipalName
                        Set-DistributionGroup -Identity $destinationGroup.Guid -ManagedBy @{Remove="$($userToRemove.UserPrincipalName)"}
                    }
                }

            }
            catch {
                Write-Output "Failed to restore group $($sourceGroup.DisplayName). Error: $($_.Exception.Message)"
            }
        }

        $path = Join-Path -Path $backupFolder -ChildPath ".GroupsInfo.csv"
        $GroupsInfo | Export-Csv -Path $path
    }
}
# Function to find and replace keywords in XML file
function FindAndReplaceKeywordsInXML {
    param (
        [string]$FilePath,
        $Keywords
    )

    # Read the content of the XML file
    $content = Get-Content -Path $FilePath -Raw

    # Check if the file contains any of the specified keywords
    $foundKeywords = $Keywords | Where-Object { $content -match $_ }

    if ($foundKeywords.Count -gt 0) {
        # Replace the keywords with "I_Find_U"
        $content = $content -replace ($Keywords -join '|'), "$destinationTenantDomainName"

        # Save the modified content back to the same file
        $content | Set-Content -Path $FilePath
        Write-Output "Keywords replaced in $FilePath"
    } else {
        Write-Output "No keywords found in $FilePath"
    }
}
function Convert-Domains {
    # Set the folder path where XML files are located
    $FolderPath = $backupFolder

    # Set the keywords to search for
    $Keywords = $sourceTenantDomainName

    # Get a list of all XML files in the folder
    $xmlFiles = Get-ChildItem -Path $FolderPath -Filter "*.xml" -File

    # Process each XML file
    foreach ($file in $xmlFiles) {
        FindAndReplaceKeywordsInXML -FilePath $file.FullName -Keywords $Keywords
    }
}
# Uncomment the following lines to execute the backup or restore functions
# Backup-Groups
# Convert-Domains
# Restore-Groups

Was this article helpful?