Script to Backup and Restore Azure AD Group Members and Owners by ImmutableId
Azure AD Group Backup and Restore Script
Section titled “Azure AD Group Backup and Restore Script”Overview
Section titled “Overview”This comprehensive script facilitates the backup and restoration of Azure AD group members and owners based on their ImmutableId. It enables seamless migration between tenants by preserving group structures and membership relationships, with advanced features for domain replacement and XML-based data persistence.
Key Features
Section titled “Key Features”| Feature | Capability | Business Value |
|---|---|---|
| Group Backup | Exports group info, members, and owners to XML files | Preserves complete group structure |
| ImmutableId Matching | Uses ImmutableId for user identification across tenants | Ensures accurate user mapping |
| Cross-Tenant Migration | Supports migration between source and destination tenants | Enables tenant-to-tenant transfers |
| Domain Replacement | Replaces tenant-specific keywords in XML files | Adapts data for new environment |
Script Requirements
Section titled “Script Requirements”Ideal for: Migration specialists, Azure AD administrators, and enterprise architects
Prerequisites:
- Azure AD PowerShell Module installed
- Global Administrator role in both tenants
- User synchronization with ImmutableId preservation
- Sufficient permissions for group management
Configuration
Section titled “Configuration”Tenant Settings
Section titled “Tenant Settings”$backupFolder = "$home\Documents\GroupsBackup"$sourceTenantDomain = ""$sourceTenantDomainName = @("", "domain")$destinationTenantDomain = ""$destinationTenantDomainName = ""Implementation
Section titled “Implementation”# 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 filefunction 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-DomainsRestore-Groups
---
## Migration Process
### Phase 1: Backup1. **Execute Backup-Groups** function2. **XML files** created with group information3. **Members and owners** exported separately4. **Backup folder** automatically timestamped
### Phase 2: Domain Conversion1. **Run Convert-Domains** function2. **Tenant-specific keywords** replaced3. **Domain references** updated for destination4. **XML files** modified in place
### Phase 3: Restore1. **Execute Restore-Groups** function2. **Groups created** if not existing3. **Members matched** by ImmutableId4. **Ownership transferred** appropriately
---
## File Structure
### Backup Files Generated| File Type | Naming Convention | Content ||-----------|-------------------|---------|| **Group Info** | `{GroupId}_{DisplayName}_Info.xml` | Group properties and settings || **Members** | `{GroupId}_{DisplayName}_members.xml` | Member list with ImmutableId || **Owners** | `{GroupId}_{DisplayName}_Owner.xml` | Owner list with ImmutableId |
---
## Usage Scenarios
### Tenant-to-Tenant Migrations- Preserve group structures during mergers- Maintain access control during divestitures- Transfer business unit configurations
### Disaster Recovery- Backup group configurations- Restore after accidental deletions- Maintain business continuity
---
> **Key Takeaway:** This script provides a robust solution for Azure AD group migration, ensuring seamless transfer of group structures and membership relationships while maintaining data integrity through ImmutableId-based matching.