Public/New-CrossTenantMigrationTenantPreparation.ps1

function New-CrossTenantMigrationTenantPreparation {

    <#
        .SYNOPSIS
        The function creates a new cross tenant mailbox migration configuration.
        .DESCRIPTION
        The function processes all preparation tasks for a M365 cross tenant migration
        with primary focus on Exchange Online.
        These tasks include:
        - creating a new app registration (optionally) in the target tenant following
          Microsoft's guidelines for Exchange Online
        - creating a new client secret for the target tenant app registration
        - creating a source tenant admin consent URL for the target tenant app
          registration (currently the consent must be done manually)
        - creating a migration endpoint in the target tenant
        - creating an organization relationship in the target tenant
        - creating scoping groups in the source tenant
        - creating an organization relationship in the source tenant
        .PARAMETER SourceTenantInitialDomain [String]
        The mandatory parameter -SourceTenantInitialDomain specifies the initial domain
        (.onmicrosoft.com domain) of the tenant to migrate from (source).
        Alias: Source
        .PARAMETER TargetTenantInitialDomain [String]
        The mandatory parameter -TargetTenantInitialDomain specifies the initial domain
        (.onmicrosoft.com domain) of the tenant to migrate to (target).
        Alias: Target
        .PARAMETER AppRegistrationDisplayName [String]
        The optional parameter -AppRegistrationDisplayName specifies the display name of
        the app registration to create or to use if exists.
        Alias: AppDisplayName
        Defaults to: CrossTenantMigration
        .PARAMETER AppRegistrationIncludeSharePointApiPermission [Switch]
        The optional parameter -AppRegistrationIncludeSharePointApiPermission forces the
        function to add API permissions to the new app registration needed for migration
        of SharePoint Online and/or OneDrive for Business.
        Alias: AppIncludesSharePoint
        Defaults to: $false
        .PARAMETER AppRegistrationClientSecretName [String]
        The optional parameter -AppRegistrationClientSecretName specifies the display name
        of the client secret to create.
        Alias: AppSecretName
        Defaults to: $AppRegistrationDisplayName-Secret
        .PARAMETER AppRegistrationClientSecretValidMonths [Int32]
        The optional parameter -AppRegistrationClientSecretValidMonths specifies the
        validity period of the new client secret in months. Valid values: 3,6,12,18,24
        Alias: AppSecretValidMonths
        Defaults to: 12
        .PARAMETER AppRegistrationUseExisting [Switch]
        The optional parameter -AppRegistrationUseExisting forces the function to use an
        existing app registration (selected by -AppRegistrationName parameter) instead of
        creating a new one.
        Defaults to: $false
        .PARAMETER MigrationEndpointName [String]
        The optional parameter -MigrationEndpointName specifies the display name of the
        migration endpoint in the target tenant.
        Alias: MigrationEndpoint
        Defaults to: EP-$AppRegistrationDisplayName
        .PARAMETER SourceOrganizationRelationshipName [String]
        The optional parameter -SourceOrganizationRelationshipName specifies the display
        name of the organization relationship in the source tenant.
        Alias: SourceOrgRelationship
        Defaults to: ORG-$TargetTenantInitialDomain
        .PARAMETER TargetOrganizationRelationshipName [String]
        The optional parameter -TargetOrganizationRelationshipName specifies the display
        name of the organization relationship in the target tenant.
        Alias: TargetOrgRelationship
        Defaults to: ORG-$SourceTenantInitialDomain
        .PARAMETER MailboxMoveScopeGroups [Array]
        The optional parameter -MailboxMoveScopeGroups specifies the name of one or more
        mail-enabled security groups that shall be processed by the migration. According
        to Microsoft's docu, New-OrganizationRelationship supports multiple scope groups
        (that's why the parameter should be of type 'array'). Nevertheless, multiple scope
        groups as array lead to error (example if 3 values provided):
            ** The given count of areas (3) exceeds the allowed limit of 1 **
        Therefore, only a single value should be provided until this issue is fixed by
        Microsoft. Otherwise the array will be cropped and only the first value is
        processed.
        Alias: ScopeGroups
        Defaults to: SG-$AppRegistrationDisplayName
        .PARAMETER Silent [Switch]
        The optional paramter -Silent forces the function to suppress all informational
        output except error messages.
        Defaults to: $false
        .OUTPUTS
        System.Object
        .COMPONENT
        Microsoft.Graph
        ExchangeOnlineManagement
        .EXAMPLE
        New-CrossTenantMigrationTenantPreparation `
        -SourceTenantInitialDomain 'sourcecompany.onmicrosoft.com' `
        -TargetTenantInitialDomain 'targetcompany.onmicrosoft.com' `
        -AppRegistrationDisplayName 'CrossTenantMigration' `
        -AppRegistrationClientSecretName 'TargetCompany-SourceCompany-Secret' `
        -AppRegistrationClientSecretValidMonths 24 `
        -MigrationEndpointName 'EP-SourceCompany-EXO' `
        -SourceOrganizationRelationshipName 'ORG-TargetCompany' `
        -TargetOrganizationRelationshipName 'ORG-SourceCompany' `
        -MailboxMoveScopeGroups 'SG-CrossTenantMigration-Scope-01','SG-CrossTenantMigration-Scope-02'
        .EXAMPLE
        New-CrossTenantMigrationTenantPreparation `
        -SourceTenantInitialDomain 'sourcecompany.onmicrosoft.com' `
        -TargetTenantInitialDomain 'targetcompany.onmicrosoft.com' `
        -AppRegistrationDisplayName 'CrossTenantMigration' `
        -AppRegistrationUseExisting `
        -AppRegistrationClientSecretName 'TargetCompany-SourceCompany-Secret' `
        -AppRegistrationClientSecretValidMonths 24 `
        -MigrationEndpointName 'EP-SourceCompany-EXO' `
        -SourceOrganizationRelationshipName 'ORG-TargetCompany' `
        -TargetOrganizationRelationshipName 'ORG-SourceCompany' `
        -MailboxMoveScopeGroups 'SG-CrossTenantMigration-Scope-01'
    #>


    [CmdletBinding(PositionalBinding=$false,HelpUri='https://github.com/uplink-systems/powershell-modules/UplinkSystems.Microsoft.Cloud')]
    [Alias('New-CTMTenantPreparation')]

    param (
        [Parameter(Mandatory=$true)] [Alias('Source')]
        [ValidateScript({if ($_.EndsWith('.onmicrosoft.com')) {$true} else {throw "Domain $_ is invalid: not an initial tenant domain"}})]
        [String] $SourceTenantInitialDomain,
        [Parameter(Mandatory=$true)] [Alias('Target')]
        [ValidateScript({if ($_.EndsWith('.onmicrosoft.com')) {$true} else {throw "Domain $_ is invalid: not an initial tenant domain"}})]
        [String] $TargetTenantInitialDomain,
        [Parameter(Mandatory=$false)] [Alias('AppName')]
        [String] $AppRegistrationDisplayName = "CrossTenantMigration",
        [Parameter(Mandatory=$false)] [Alias('AppIncludeSharePoint')]
        [Switch] $AppRegistrationIncludeSharePointApiPermission,
        [Parameter(Mandatory=$false)] [Alias('AppSecretName')]
        [String] $AppRegistrationClientSecretName = "$AppRegistrationDisplayName-Secret",
        [Parameter(Mandatory=$true)] [Alias('AppSecretValidMonths')]
        [ValidateSet(3,6,12,18,24)]
        [Int32] $AppRegistrationClientSecretValidMonths = 12,
        [Parameter(Mandatory=$false)] [Alias('UseExistingApp')]
        [Switch] $AppRegistrationUseExisting,
        [Parameter(Mandatory=$false)] [Alias('MigrationEndpoint')]
        [String] $MigrationEndpointName = "EP-$AppRegistrationDisplayName",
        [Parameter(Mandatory=$false)] [Alias('SourceOrgRelationship')]
        [String] $SourceOrganizationRelationshipName = "ORG-$TargetTenantInitialDomain",
        [Parameter(Mandatory=$false)] [Alias('TargetOrgRelationship')]
        [String] $TargetOrganizationRelationshipName = "ORG-$SourceTenantInitialDomain",
        [Parameter(Mandatory=$false)] [Alias('ScopeGroups')]
        [Array] $MailboxMoveScopeGroups = "SG-$AppRegistrationDisplayName",
        [Switch] $Silent
    )

    # prepare variables
    $Global:SourceTenantId = (Get-EntraTenantId -Domain $SourceTenantInitialDomain)
    $Global:TargetTenantId = (Get-EntraTenantId -Domain $TargetTenantInitialDomain)

    # crop scope groups array to only one value
    if ($MailboxMoveScopeGroups.Count -gt 1) {[Array]$MailboxMoveScopeGroups = $MailboxMoveScopeGroups[0]}

    # disconnect existing sessions
    if (Get-MgContext) {Disconnect-MgGraph | Out-Null}
    if (Get-OrganizationConfig | Select-Object Identity) {Disconnect-ExchangeOnline -Confirm:$false}

    # connect MgGraph session to target tenant
    Connect-MgGraph -Scope 'Application.ReadWrite.All','AppRoleAssignment.ReadWrite.All','DelegatedPermissionGrant.ReadWrite.All','Directory.ReadWrite.All','Domain.Read.All','User.Read.All' -NoWelcome
    # get existing app registration or create a new one if select
    if (-not($AppRegistrationUseExisting)) {
        $Global:MgApplication = New-CrossTenantMigrationAppRegistration -DisplayName $AppRegistrationDisplayName -IncludeSharePoint $AppRegistrationIncludeSharePointApiPermission -Silent $Silent
    }
    else {
        $Global:MgApplication = Get-MgApplication -Filter "DisplayName eq '$AppRegistrationDisplayName'"
    }
    # create a new client secret for the app registration
    $Global:MgApplicationSecret = Add-EntraApplicationCredential -ApplicationName $AppRegistrationDisplayName -SecretName $AppRegistrationClientSecretName -ValidMonths $AppRegistrationClientSecretValidMonths
    Write-Host -Object "`nApplication secret: " -NoNewline; Write-Host -Object "$($MgApplicationSecret.SecretText)`n" -ForegroundColor Yellow
    # disconnect MgGraph session from target tenant
    Disconnect-MgGraph | Out-Null

    # admin consent to the target tenant's app registration in the source tenant
    $AppRegistrationConsentUrl = "https://login.microsoftonline.com/$SourceTenantInitialDomain/adminconsent?client_id=$($MgApplication.AppId)&redirect_uri=https://office.com"
    $AppRegistrationConsentUrl | Set-Clipboard
    Write-Host -Object "Please admin consent to the target tenant app registration in source tenant with the following URL:"
    Write-Host -Object "$AppRegistrationConsentUrl" -ForegroundColor Yellow
    Write-Host -Object "The URL has been copied to clipboard. Paste (or copy the URL above manually) it in a browser and accept with source tenant admin credentials."
    Read-Host -Prompt "Admin consent finished [Y/N]?"

    # connect to target tenant Exchange Online organization
    Connect-ExchangeOnline -Organization $TargetTenantInitialDomain -ShowBanner:$false
    # enable customization if tenant is dehydrated
    $TargetOrganizationConfig = Get-OrganizationConfig | Select-Object isdehydrated
    if ($TargetOrganizationConfig.isdehydrated -eq $true) {Enable-OrganizationCustomization}
    # create migration endpoint
    $Credentials = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $MgApplication.AppId, (ConvertTo-SecureString -String $MgApplicationSecret.SecretText -AsPlainText -Force)
    New-MigrationEndpoint -Name $MigrationEndpointName -ApplicationId $MgApplication.AppId -ExchangeRemoteMove:$true -RemoteServer outlook.office.com -RemoteTenant $SourceTenantInitialDomain -Credentials $Credentials
    # update existing organization relationship or create a new one if none exists
    $TargetOrganizationRelationshipExists = Get-OrganizationRelationship | Where-Object {$_.DomainNames -like $SourceTenantId}
    if ($null -ne $TargetOrganizationRelationshipExists) {
        Set-OrganizationRelationship -Name $TargetOrganizationRelationshipExists.Name -Enabled:$true -MailboxMoveEnabled:$true -MailboxMoveCapability Inbound
    }
    if ($null -eq $OrganizationRelationshipExists) {
        New-OrganizationRelationship -Name $TargetOrganizationRelationshipName -Enabled:$true -MailboxMoveEnabled:$true -MailboxMoveCapability Inbound -DomainNames $SourceTenantId
    }
    # disconnect from target tenant Exchange Online organization
    Disconnect-ExchangeOnline -Confirm:$false

    # connect to source tenant Exchange Online organization
    Connect-ExchangeOnline -Organization $SourceTenantInitialDomain -ShowBanner:$false
    # enable customization if tenant is dehydrated
    $SourceOrganizationConfig = Get-OrganizationConfig | Select-Object isdehydrated
    if ($SourceOrganizationConfig.isdehydrated -eq $true) {Enable-OrganizationCustomization}
    # create scope group(s)
    foreach ($MailboxMoveScopeGroup in $MailboxMoveScopeGroups) {
        if (-not(New-DistributionGroup -Type Security -Name $MailboxMoveScopeGroup)) {
            Write-Information -MessageData "Scope group $MailboxMoveScopeGroup already exists... Skipping creation..." -InformationAction Continue
        }
    }
    # update existing organization relationship or create a new one if none exists
    $SourceOrganizationRelationshipExists = Get-OrganizationRelationship | Where-Object {$_.DomainNames -like $TargetTenantId}
    if ($null -ne $SourceOrganizationRelationshipExists) {
        Set-OrganizationRelationship -Name $SourceOrganizationRelationshipExists.Name -Enabled:$true -MailboxMoveEnabled:$true -MailboxMoveCapability RemoteOutbound -OAuthApplicationId $MgApplication.AppId -MailboxMovePublishedScopes $MailboxMoveScopeGroups
    }
    if ($null -eq $SourceOrganizationRelationshipExists) {
        New-OrganizationRelationship -Name $SourceOrganizationRelationshipName -Enabled:$true -MailboxMoveEnabled:$true -MailboxMoveCapability RemoteOutbound -DomainNames $TargetTenantId -OAuthApplicationId $MgApplication.AppId -MailboxMovePublishedScopes $MailboxMoveScopeGroups
    }
    # disconnect from source tenant Exchange Online organization
    Disconnect-ExchangeOnline -Confirm:$false

}