Script to Backup and Restore Azure AD Group Members and Owners by ImmutableId

9 min. readlast update: 09.26.2024

This script facilitates the backup and restoration of Azure AD group members and owners based on their ImmutableId. It connects to a source tenant to back up group details, members, and owners, storing them in XML files. For restoration, it connects to a destination tenant, creates missing groups, and syncs members and owners by matching their ImmutableId. Additionally, the script includes functions to handle keyword replacements in the XML files, useful when migrating between different tenants.

Here is the script:

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

# Source and destination tenant domain names
$sourceTenantDomain = ""
$sourceTenantDomainName =@("", "domain")
$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 Backup-Groups {
    New-DirectoryIfNotExist -Path $backupFolder

    try {
        Connect-AzureAD -TenantId $sourceTenantDomain -ErrorAction Stop
    }
    catch {
        Write-Output "Failed to connect to the source tenant. Error: $($_.Exception.Message)"
        return
    }

    try {
        $allGroups = Get-AzureADMSGroup -All $true |
            Where-Object {
                $null -eq $_.OnPremisesSecurityIdentifier -and
                $_.GroupTypes -notcontains "Unified" -and
                $_.MailEnabled -eq $true -and
                $_.MembershipRule -ne "All Users"
            }
    }
    catch {
        Write-Output "Failed to retrieve groups. Error: $($_.Exception.Message)"
        return
    }

    foreach ($sourceGroup in $allGroups) {
        try {
            $sourceGroupMembers = Get-AzureADGroupMember -ObjectId $sourceGroup.Id -All $true
            $sourceGroupOwner = Get-AzureADGroupOwner -ObjectId $sourceGroup.Id -All $true
            $exportPath = Join-Path -Path $backupFolder -ChildPath "$($sourceGroup.Id)_$($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 Restore-Groups {
    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-AzureAD -TenantId $destinationTenantDomain -ErrorAction Stop
        }
        catch {
            Write-Output "Failed to connect to the destination tenant. Error: $($_.Exception.Message)"
            return
        }

        $GroupsInfo = @()

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

                if ($null -eq $destinationGroup) {
                    $destinationGroup = New-AzureADMSGroup -DisplayName $($sourceGroup.DisplayName) -MailEnabled $($sourceGroup.MailEnabled) -MailNickname $($sourceGroup.MailNickname) -SecurityEnabled $($sourceGroup.SecurityEnabled) -Description $($sourceGroup.Description) -GroupTypes $($sourceGroup.GroupTypes) -IsAssignableToRole $($sourceGroup.IsAssignableToRole) -MembershipRule $($sourceGroup.MembershipRule) -MembershipRuleProcessingState $($sourceGroup.MembershipRuleProcessingState) -Visibility $($sourceGroup.Visibility)
                }

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

                if ($sourceGroup.GroupTypes -notcontains 'DynamicMembership') {
                    $exportPath = Join-Path -Path $backupFolder -ChildPath "$($sourceGroup.Id)_$($sourceGroup.DisplayName)"
                    $xmlPath = $exportPath + '_members.xml'
                    $sourceGroupMembers = Import-Clixml -Path $xmlPath
                    $sourceGroupMembers = $sourceGroupMembers | Where-Object { $_.ObjectType -eq "User" }
                    $destinationGroupMembers = Get-AzureADGroupMember -ObjectId $destinationGroup.Id | Where-Object { $_.ObjectType -eq "User" } -ErrorAction SilentlyContinue

                    foreach ($sourceMember in $sourceGroupMembers) {
                        if ($sourceMember.ImmutableId -notin $destinationGroupMembers.ImmutableId) {
                            $userToAdd = Get-AzureADUser -Filter "ImmutableId eq '$($sourceMember.ImmutableId)'"

                            if ($userToAdd.Count -gt 1) {
                                Write-Output "Multiple users found with the same ImmutableId: $($userToAdd.UserPrincipalName)"
                                continue
                            }

                            if ($userToAdd) {
                                Add-AzureADGroupMember -ObjectId $destinationGroup.Id -RefObjectId $userToAdd.ObjectId
                            }
                        }
                    }

                    foreach ($destinationMember in $destinationGroupMembers) {
                        if ($destinationMember.ImmutableId -notin $sourceGroupMembers.ImmutableId) {
                            Remove-AzureADGroupMember -ObjectId $destinationGroup.Id -MemberId $destinationMember.ObjectId
                        }
                    }
                }

                $xmlPath = $exportPath + '_Owner.xml'
                $sourceGroupOwner = Import-Clixml -Path $xmlPath
                $sourceGroupOwner = $sourceGroupOwner | Where-Object { $_.ObjectType -eq "User" }
                $destinationGroupOwner = Get-AzureADGroupOwner -ObjectId $destinationGroup.Id | Where-Object { $_.ObjectType -eq "User" } -ErrorAction SilentlyContinue

                foreach ($sourceOwner in $sourceGroupOwner) {
                    if ($sourceOwner.ImmutableId -notin $destinationGroupOwner.ImmutableId) {
                        $userToAdd = Get-AzureADUser -Filter "ImmutableId eq '$($sourceOwner.ImmutableId)'"

                        if ($userToAdd.Count -gt 1) {
                            Write-Output "Multiple users found with the same ImmutableId: $($userToAdd.UserPrincipalName)"
                            continue
                        }

                        if ($userToAdd) {
                            Add-AzureADGroupOwner -ObjectId $destinationGroup.Id -RefObjectId $userToAdd.ObjectId
                        }
                    }
                }

                foreach ($destinationOwner in $destinationGroupOwner) {
                    if ($destinationOwner.ImmutableId -notin $sourceGroupOwner.ImmutableId) {
                        Remove-AzureADGroupOwner -ObjectId $destinationGroup.Id -OwnerId $destinationOwner.ObjectId
                    }
                }
            }
            catch {
                Write-Output "Failed to restore group $($sourceGroup.DisplayName). Error: $($_.Exception.Message)"
            }
        }

        $path = Join-Path -Path $backupFolder -ChildPath ".GroupsInfo.csv"
        $GroupsInfo | Export-Clixml -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?