├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── SECURITY.md ├── SetupCrossTenantRelationshipForResourceTenant.ps1 ├── SetupCrossTenantRelationshipForTargetTenant.ps1 ├── VerifySetup.ps1 ├── VerifySetupDeprecated.ps1 └── v1 Content ├── Cross-tenant mailbox migration (preview).mhtml ├── Cross-tenant mailbox migration (preview).pdf ├── cross-tenant-mailbox-migration.md └── media └── tenant-to-tenant-mailbox-move ├── csv-sample.png ├── invited-by-target-tenant.png ├── permissions-requested-accept.png ├── permissions-requested-dialog.png ├── prepare-tenants-flow.png └── prepare-tenants-flow.svg /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Microsoft Open Source Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 4 | 5 | Resources: 6 | 7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Contributing 3 | 4 | **The scripts and the use of Azure Key Vault have been deprecated and will no longer be supported for use. If you were directed here through an incorrect link or an older link, please refresh the following URL to get the latest steps for setup that do not require these scripts or Azure Key Vault.** 5 | 6 | https://docs.microsoft.com/en-us/microsoft-365/enterprise/cross-tenant-mailbox-migration?view=o365-worldwide 7 | 8 | This project welcomes contributions and suggestions. Most contributions require you to agree to a 9 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us 10 | the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. 11 | 12 | When you submit a pull request, a CLA bot will automatically determine whether you need to provide 13 | a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions 14 | provided by the bot. You will only need to do this once across all repos using our CLA. 15 | 16 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 17 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 18 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 19 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets Microsoft's [Microsoft's definition of a security vulnerability](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)) of a security vulnerability, please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://microsoft.com/msrc/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd). 40 | 41 | 42 | -------------------------------------------------------------------------------- /SetupCrossTenantRelationshipForResourceTenant.ps1: -------------------------------------------------------------------------------- 1 | ################################################################################# 2 | # 3 | # The sample scripts are not supported under any Microsoft standard support 4 | # program or service. The sample scripts are provided AS IS without warranty 5 | # of any kind. Microsoft further disclaims all implied warranties including, without 6 | # limitation, any implied warranties of merchantability or of fitness for a particular 7 | # purpose. The entire risk arising out of the use or performance of the sample scripts 8 | # and documentation remains with you. In no event shall Microsoft, its authors, or 9 | # anyone else involved in the creation, production, or delivery of the scripts be liable 10 | # for any damages whatsoever (including, without limitation, damages for loss of business 11 | # profits, business interruption, loss of business information, or other pecuniary loss) 12 | # arising out of the use of or inability to use the sample scripts or documentation, 13 | # even if Microsoft has been advised of the possibility of such damages. 14 | # 15 | ################################################################################# 16 | 17 | <# .SYNOPSIS 18 | This script can be used by a tenant that wishes to move resources out of their tenant. 19 | For example fabrikam.com would run this script in order for the contoso.com tenant to pull mailboxes from the fabrikam.com tenant. 20 | 21 | This script is intended for the resource tenant in above example fabrikam.com, and it sets up the organization relationship in exchange to authorize the migration. 22 | Following are key properties in organization relationship used here: 23 | - ApplicationId of the azure ad application that resource tenant consents to for mailbox migrations. 24 | - SourceMailboxMovePublishedScopes contains the groups of users that are in scope for migration. Without this no mailboxes can be migrated. 25 | 26 | 27 | .PARAMETER SourceMailboxMovePublishedScopes 28 | SourceMailboxMovePublishedScopes - Identity of the scope used by source tenant admin. 29 | 30 | .PARAMETER ResourceTenantDomain 31 | ResourceTenantDomain - the resource tenant. 32 | 33 | .PARAMETER TargetTenantDomain 34 | TargetTenantDomain - The target tenant. 35 | 36 | .PARAMETER TargetTenantId 37 | TargetTenantId - The target tenant id. 38 | 39 | .EXAMPLE 40 | SetupCrossTenantRelationshipForResourceTenant.ps1 -ResourceTenantDomain fabrikam.onmicrosoft.com -TargetTenantDomain contoso.onmicrosoft.com -TargetTenantId d925e0c6-d4db-40c6-a864-49db24af0460 -SourceMailboxMovePublishedScopes "SecurityGroupName" 41 | #> 42 | 43 | [CmdletBinding(SupportsShouldProcess)] 44 | param 45 | ( 46 | [Parameter(Mandatory = $true, HelpMessage='Setup Options')] 47 | [string[]]$SourceMailboxMovePublishedScopes, 48 | 49 | [Parameter(Mandatory = $true, HelpMessage='Resource tenant domain')] 50 | [ValidateScript({ -not [string]::IsNullOrWhiteSpace($_) })] 51 | [string]$ResourceTenantDomain, 52 | 53 | [Parameter(Mandatory = $true, HelpMessage='Target tenant domain')] 54 | [ValidateScript({ -not [string]::IsNullOrWhiteSpace($_) })] 55 | $TargetTenantDomain, 56 | 57 | [Parameter(Mandatory = $true, HelpMessage='The application id for the azure ad application to be used for mailbox migrations')] 58 | [ValidateScript({ -not [string]::IsNullOrWhiteSpace($_) })] 59 | $ApplicationId, 60 | 61 | [Parameter(Mandatory = $true, HelpMessage='Target tenant id. This is azure ad directory id or external directory object id in exchange online.')] 62 | [ValidateScript({ -not [string]::IsNullOrWhiteSpace($_) })] 63 | $TargetTenantId 64 | ) 65 | 66 | $ErrorActionPreference = 'Stop' 67 | 68 | $ScriptPath = $MyInvocation.MyCommand.Path 69 | $ScriptDir = Split-Path $ScriptPath 70 | 71 | function Main() { 72 | Run-ExchangeSetupForResourceTenant $TargetTenantDomain $TargetTenantId $ResourceTenantDomain $ApplicationId $SourceMailboxMovePublishedScopes 73 | Write-Host "Exchange setup complete." -Foreground Green 74 | } 75 | 76 | function Check-ExchangeOnlinePowershellConnection { 77 | if ($Null -eq (Get-Command New-OrganizationRelationship -ErrorAction SilentlyContinue)) { 78 | Write-Error "Please connect to the Exchange Online Management module or Exchange Online through basic authentication before running this script!"; 79 | } 80 | } 81 | 82 | function Run-ExchangeSetupForResourceTenant([string]$targetTenant, [string]$targetTenantId, [string]$resourceTenantDomain, [string]$appId, [string[]]$sourceMailboxMovePublishedScopes) { 83 | # 1. Verify migration scope. 84 | # 2. Create organization relationship 85 | $orgRel = Get-OrganizationRelationship | ? { $_.DomainNames -contains $targetTenantId } 86 | 87 | if ($orgRel) { 88 | Write-Verbose "Organization relationship already exists with $targetTenantId. Updating it." 89 | $capabilities = @($orgRel.MailboxMoveCapability.Split(",").Trim()) 90 | if (-not $orgRel.MailboxMoveCapability.Contains("RemoteOutbound")) { 91 | Write-Verbose "Adding RemoteOutbound capability to the organization relationship. Existing capabilities: $capabilities" 92 | $capabilities += "RemoteOutbound" 93 | } 94 | 95 | $orgRel | Set-OrganizationRelationship -Enabled:$true -MailboxMoveEnabled:$true -MailboxMoveCapability $capabilities -OAuthApplicationId $appId -MailboxMovePublishedScopes $sourceMailboxMovePublishedScopes 96 | } else { 97 | $randomSuffix = [Random]::new().Next(0, 10000) 98 | $orgRelName = "$($targetTenant.Split('.')[0])_$($resourceTenantDomain.Split('.')[0])_$randomSuffix" 99 | $maxLength = [System.Math]::Min(64, $orgRelName.Length) 100 | $orgRelName = $orgRelName.SubString(0, $maxLength) 101 | 102 | Write-Verbose "Creating organization relationship: $orgRelName in $resourceTenantDomain" 103 | New-OrganizationRelationship ` 104 | -DomainNames $targetTenantId ` 105 | -Enabled:$true ` 106 | -MailboxMoveEnabled:$true ` 107 | -MailboxMoveCapability RemoteOutbound ` 108 | -Name $orgRelName ` 109 | -OAuthApplicationId $appId ` 110 | -MailboxMovePublishedScopes $sourceMailboxMovePublishedScopes 111 | } 112 | } 113 | 114 | function PreValidation() { 115 | Write-Host `n 116 | Write-Host "Welcome to the Cross-tenant mailbox migration preview! Before running this script, please be sure to review the details provided on docs.microsoft.com at the following URL: `nhttps://docs.microsoft.com/en-us/microsoft-365/enterprise/cross-tenant-mailbox-migration" 117 | Write-Host "`nIt is also recommended before running this script to review the script in a script editor or Notepad prior to running."`n 118 | Write-Host "For general feedback and / or questions, please contact crosstenantmigrationpreview@service.microsoft.com.`nThis is not a support alias and should not be used if you are currently experiencing an issue and need immediate assistance."`n 119 | $title = "Confirm: Configure Cross-Tenant mailbox migration preview." 120 | $message = "`nIf you are ready to begin configuring your tenants, select 'Y'.`nIf you need to review any additional details and proceed at a later time, select 'N'.`n`nDo you wish to proceed?" 121 | $yes = New-Object System.Management.Automation.Host.ChoiceDescription "&Yes", "Yes" 122 | $no = New-Object System.Management.Automation.Host.ChoiceDescription "&No", "No" 123 | $options = [System.Management.Automation.Host.ChoiceDescription[]]($yes, $no) 124 | $choice=$host.ui.PromptForChoice($title, $message, $options, 1) 125 | if ($choice -ne 0) { 126 | Exit} 127 | Start-Sleep 2 128 | Write-Host "`nWe are verifying that you are using the latest version of the script."`n 129 | Write-Host "This requires that we download the latest version of the script from GitHub to compare with your local copy." 130 | Write-Host "This file will be stored on your local computer temporarily, as well as overwrite your existing script file if it is out of date." 131 | $title = "Confirm: Allow for download from GitHub." 132 | $message = "`nIf you are ready to begin this step, select 'Y'. `nIf you would prefer to manually download the scripts to make sure you have the latest version, select 'N'" 133 | $yes = New-Object System.Management.Automation.Host.ChoiceDescription "&Yes", "Yes" 134 | $no = New-Object System.Management.Automation.Host.ChoiceDescription "&No", "No" 135 | $options = [System.Management.Automation.Host.ChoiceDescription[]]($yes, $no) 136 | $choice=$host.ui.PromptForChoice($title, $message, $options, 1) 137 | if ($choice -ne 0) { 138 | Exit} 139 | else {Verification} 140 | 141 | } 142 | 143 | function Verification { 144 | Write-Host "`nBeginning verification steps."`n 145 | Check-ExchangeOnlinePowershellConnection 146 | Write-Host "Verifying ability to create a new organization relationship in the tenant." 147 | try { 148 | New-OrganizationRelationship -DomainNames contoso.onmicrosoft.com -Name Contoso -WhatIf -ErrorAction Stop 149 | } 150 | catch { 151 | Write-Output "You need to run the command Enable-OrganizationCustomization before continuing with execution of the script." 152 | Exit 153 | } 154 | Write-Host "`nVerifying that your script is up to date with the latest changes." 155 | Write-Host "`nBeginning download of SetupCrossTenantRelationshipForResourceTenant.ps1 and creation of temporary files." 156 | if ((Test-Path -Path $ScriptDir\XTenantTemp) -eq $true) { 157 | Remove-Item -Path $ScriptDir\XTenantTemp\ -Recurse -Force | Out-Null 158 | } 159 | New-Item -Path $ScriptDir -Name XTenantTemp -ItemType Directory | Out-Null 160 | Invoke-WebRequest -Uri https://aka.ms/ResourceTenant -Outfile $ScriptDir\XTenantTemp\SetupCrossTenantRelationshipForResourceTenant.ps1 161 | if ((Get-FileHash $ScriptDir\SetupCrossTenantRelationshipForResourceTenant.ps1).hash -eq (Get-FileHash $ScriptDir\XTenantTemp\SetupCrossTenantRelationshipForResourceTenant.ps1).hash) { 162 | Write-Host "`nYou are using the latest version of the script. Removing temporary files and proceeding with setup." 163 | Start-Sleep 1 164 | Remove-Item -Path $ScriptDir\XTenantTemp\ -Recurse -Force | Out-Null 165 | } 166 | elseif ((Get-FileHash $ScriptDir\SetupCrossTenantRelationshipForResourceTenant.ps1).hash -ne (Get-FileHash $ScriptDir\XTenantTemp\SetupCrossTenantRelationshipForResourceTenant.ps1).hash) { 167 | Write-Host "`nYou are not using the latest version of the script."`n 168 | Start-Sleep 1 169 | Write-Host "`nReplacing the local copy of SetupCrossTenantRelationshipForResourceTenant.ps1 and cleaning up temporary files..." 170 | Start-Sleep 1 171 | Copy-Item $ScriptDir\XTenantTemp\SetupCrossTenantRelationshipForResourceTenant.ps1 -Destination $ScriptDir | Out-Null 172 | Start-Sleep 1 173 | Remove-Item -Path $ScriptDir\XTenantTemp\ -Recurse -Force | Out-Null 174 | Write-Host "Update completed. You will need to run the script again." 175 | Start-Sleep 1 176 | Exit 177 | } 178 | } 179 | 180 | PreValidation 181 | Main 182 | -------------------------------------------------------------------------------- /SetupCrossTenantRelationshipForTargetTenant.ps1: -------------------------------------------------------------------------------- 1 | ################################################################################# 2 | # 3 | # The sample scripts are not supported under any Microsoft standard support 4 | # program or service. The sample scripts are provided AS IS without warranty 5 | # of any kind. Microsoft further disclaims all implied warranties including, without 6 | # limitation, any implied warranties of merchantability or of fitness for a particular 7 | # purpose. The entire risk arising out of the use or performance of the sample scripts 8 | # and documentation remains with you. In no event shall Microsoft, its authors, or 9 | # anyone else involved in the creation, production, or delivery of the scripts be liable 10 | # for any damages whatsoever (including, without limitation, damages for loss of business 11 | # profits, business interruption, loss of business information, or other pecuniary loss) 12 | # arising out of the use of or inability to use the sample scripts or documentation, 13 | # even if Microsoft has been advised of the possibility of such damages. 14 | # 15 | ################################################################################# 16 | 17 | <# .SYNOPSIS 18 | This script can be used by a tenant that wishes to pull resources out of another tenant. 19 | For example contoso.com would run this script in order to pull mailboxes from fabrikam.com tenant. 20 | 21 | This script is intended for the target tenant and would setup the following using the SubscriptionId specified or the default subscription: 22 | 1. Create a resource group or use the one specified as parameter 23 | 2. Create a key vault in the above resource group specified as a parameter 24 | 3. Setup above key vault's access policy to grant exchange access to secrets and certificates. 25 | 4. Request a self-signed certificate to be put in the key vault. 26 | 5. Retrieve the public part of certificate from key vault 27 | 6. Create an AAD application and setup its permissions for MSGraph and exchange 28 | 7. Set the secret for above application as the certificate in 4. 29 | 8. Wait for the tenant admin to consent to the application permissions 30 | 9. Once confirmed, send an email using initiation manager to the tenant admin of resource tenant. 31 | 10. Create a migration endpoint in exchange with the ApplicationId, Pointer to application secret in KeyVault and RemoteTenant 32 | 11. Create an organization relationship with resource tenant authorizing migration. 33 | 34 | .PARAMETER SubscriptionId 35 | SubscriptionId - the subscription to use for key vault. 36 | 37 | .PARAMETER ResourceTenantAdminEmail 38 | ResourceTenantAdminEmail - the resource tenant admin email. 39 | 40 | .PARAMETER ResourceGroup 41 | ResourceGroup - the resource group name. 42 | 43 | .PARAMETER KeyVaultName 44 | KeyVaultName - the key vault name. 45 | 46 | .PARAMETER AzureResourceLocation 47 | AzureResourceLocation - the Display Name of the Azure Resource Group & Key Vault location 48 | 49 | .PARAMETER KeyVaultAuditStorageResourceGroup 50 | KeyVaultAuditStorageResourceGroup - the key vault audit storage resource group 51 | 52 | .PARAMETER KeyVaultAuditStorageAccountName 53 | KeyVaultAuditStorageAccountName - the key vault audit storage account name 54 | 55 | .PARAMETER KeyVaultAuditStorageAccountLocation 56 | KeyVaultAuditStorageAccountLocation - the key vault audit storage account location 57 | 58 | .PARAMETER KeyVaultAuditStorageAccountSKU 59 | KeyVaultAuditStorageAccountSKU - the key vault audit storage account sku 60 | 61 | .PARAMETER CertificateName 62 | CertificateName - the name of certificate in key vault 63 | 64 | .PARAMETER CertificateSubject 65 | CertificateSubject - the subject of certificate in key vault 66 | 67 | .PARAMETER AzureAppPermissions 68 | AzureAppPermissions - fine grained control over the permissions to be given to the application. 69 | 70 | .PARAMETER UseAppAndCertGeneratedForSendingInvitation 71 | UseAppAndCertGeneratedForSendingInvitation - download the private key of generated certificate from key vault to be used for sending invitation. 72 | 73 | .PARAMETER ResourceTenantDomain 74 | ResourceTenantDomain - the resource tenant. 75 | 76 | .PARAMETER TargetTenantDomain 77 | TargetTenantDomain - The target tenant. 78 | 79 | .PARAMETER MigrationEndpointMaxConcurrentMigrations 80 | MigrationEndpointMaxConcurrentMigrations - migration endpoint's MaxConcurrentMigrations 81 | 82 | .PARAMETER ResourceTenantId 83 | ResourceTenantId - The resource tenant id. 84 | 85 | .PARAMETER Government 86 | Government - Use if the tenants are in the Microsoft Cloud for US Government. 87 | 88 | .PARAMETER DoD 89 | Dod - Use if the tenants are DoD customers in the Microsoft Cloud for US Government. 90 | 91 | .EXAMPLE 92 | SetupCrossTenantRelationshipForTargetTenant.ps1 -ResourceTenantDomain fabrikam.onmicrosoft.com -TargetTenantDomain contoso.onmicrosoft.com -ResourceTenantAdminEmail admin@contoso.onmicrosoft.com -ResourceGroup "TESTPSRG" -KeyVaultName "TestPSKV" -CertificateSubject "CN=TESTCERTSUBJ" -AzureAppPermissions Exchange, MSGraph -UseAppAndCertGeneratedForSendingInvitation -KeyVaultAuditStorageAccountName "KeyVaultLogsStorageAcnt" -KeyVaultAuditStorageResourceGroup TestResGrp0 -KeyVaultAuditStorageAccountName testauditname0 -KeyVaultAuditStorageAccountLocation westus -KeyVaultAuditStorageAccountSKU Standard_GRS -MigrationEndpointMaxConcurrentMigrations 20 -ExistingApplicationId d7404497-1e2f-4b58-bdd5-93e82dad91a4 -AzureResourceLocation "West US" 93 | 94 | .EXAMPLE 95 | SetupCrossTenantRelationshipForTargetTenant.ps1 -ResourceTenantDomain fabrikam.onmicrosoft.com -TargetTenantDomain contoso.onmicrosoft.com -ResourceTenantId 96 | #> 97 | 98 | [CmdletBinding(SupportsShouldProcess)] 99 | param 100 | ( 101 | [Parameter(Mandatory = $true, HelpMessage='SubscriptionId for key vault', ParameterSetName = 'TargetSetupAll')] 102 | [Parameter(Mandatory = $true, HelpMessage='SubscriptionId for key vault', ParameterSetName = 'TargetSetupAzure')] 103 | [ValidateScript({ -not [string]::IsNullOrWhiteSpace($_) })] 104 | [string]$SubscriptionId, 105 | 106 | [Parameter(Mandatory = $true, HelpMessage='Resource tenant admin email', ParameterSetName = 'TargetSetupAll')] 107 | [Parameter(Mandatory = $true, HelpMessage='Resource tenant admin email', ParameterSetName = 'TargetSetupAzure')] 108 | [ValidateScript({ -not [string]::IsNullOrWhiteSpace($_) })] 109 | [string]$ResourceTenantAdminEmail, 110 | 111 | [Parameter(Mandatory = $true, HelpMessage='Resource group for key vault', ParameterSetName = 'TargetSetupAll')] 112 | [Parameter(Mandatory = $true, HelpMessage='Resource group for key vault', ParameterSetName = 'TargetSetupAzure')] 113 | [ValidateScript({ -not [string]::IsNullOrWhiteSpace($_) })] 114 | [string]$ResourceGroup, 115 | 116 | [Parameter(Mandatory = $true, HelpMessage='KeyVault name', ParameterSetName = 'TargetSetupAll')] 117 | [Parameter(Mandatory = $true, HelpMessage='KeyVault name', ParameterSetName = 'TargetSetupAzure')] 118 | [ValidateScript({ -not [string]::IsNullOrWhiteSpace($_) })] 119 | [string]$KeyVaultName, 120 | 121 | [Parameter(Mandatory = $true, HelpMessage='Azure resource location', ParameterSetName = 'TargetSetupAll')] 122 | [Parameter(Mandatory = $true, HelpMessage='Azure resource location', ParameterSetName = 'TargetSetupAzure')] 123 | [string]$AzureResourceLocation, 124 | 125 | [Parameter(Mandatory = $false, HelpMessage='Resource group for storage account used for key vault audit logs', ParameterSetName = 'TargetSetupAll')] 126 | [Parameter(Mandatory = $false, HelpMessage='Resource group for storage account used for key vault audit logs', ParameterSetName = 'TargetSetupAzure')] 127 | [string]$KeyVaultAuditStorageResourceGroup, 128 | 129 | [Parameter(Mandatory = $false, HelpMessage='Storage account name for storing key vault audit logs', ParameterSetName = 'TargetSetupAll')] 130 | [Parameter(Mandatory = $false, HelpMessage='Storage account name for storing key vault audit logs', ParameterSetName = 'TargetSetupAzure')] 131 | [ValidateScript({ 132 | if ($_ -cmatch "^[a-z0-9]{3,24}$") 133 | { 134 | $true 135 | } 136 | else 137 | { 138 | throw [System.Management.Automation.ValidationMetadataException] "Storage account names must be between 3 and 24 characters in length and may contain numbers and lowercase letters only." 139 | } 140 | })] 141 | [string]$KeyVaultAuditStorageAccountName, 142 | 143 | [Parameter(Mandatory = $false, HelpMessage='Storage account location', ParameterSetName = 'TargetSetupAll')] 144 | [Parameter(Mandatory = $false, HelpMessage='Storage account location', ParameterSetName = 'TargetSetupAzure')] 145 | [string]$KeyVaultAuditStorageAccountLocation, 146 | 147 | [Parameter(Mandatory = $false, HelpMessage='Storage account SKU', ParameterSetName = 'TargetSetupAll')] 148 | [Parameter(Mandatory = $false, HelpMessage='Storage account SKU', ParameterSetName = 'TargetSetupAzure')] 149 | [string]$KeyVaultAuditStorageAccountSKU, 150 | 151 | [Parameter(HelpMessage='Certificate name to use', ParameterSetName = 'TargetSetupAll')] 152 | [Parameter(HelpMessage='Certificate name to use', ParameterSetName = 'TargetSetupAzure')] 153 | [string]$CertificateName, 154 | 155 | [Parameter(HelpMessage='Certificate subject to use', ParameterSetName = 'TargetSetupAll')] 156 | [Parameter(HelpMessage='Certificate subject to use', ParameterSetName = 'TargetSetupAzure')] 157 | [ValidateScript({$_.StartsWith("CN=") })] 158 | [string]$CertificateSubject, 159 | 160 | [Parameter(HelpMessage='Application permissions', ParameterSetName = 'TargetSetupAll')] 161 | [Parameter(HelpMessage='Application permissions', ParameterSetName = 'TargetSetupAzure')] 162 | $AzureAppPermissions = 'All', 163 | 164 | [Parameter(HelpMessage='Use the certificate generated for azure application when sending invitation', ParameterSetName = 'TargetSetupAll')] 165 | [Parameter(HelpMessage='Use the certificate generated for azure application when sending invitation', ParameterSetName = 'TargetSetupAzure')] 166 | [Switch]$UseAppAndCertGeneratedForSendingInvitation, 167 | 168 | [Parameter(Mandatory = $true, HelpMessage='Resource tenant domain')] 169 | [ValidateScript({ -not [string]::IsNullOrWhiteSpace($_) })] 170 | [string]$ResourceTenantDomain, 171 | 172 | [Parameter(Mandatory = $true, HelpMessage='Target tenant domain')] 173 | [ValidateScript({ -not [string]::IsNullOrWhiteSpace($_) })] 174 | $TargetTenantDomain, 175 | 176 | [Parameter(Mandatory = $false, HelpMessage='Migration endpoint MaxConcurrentMigrations')] 177 | [int]$MigrationEndpointMaxConcurrentMigrations, 178 | 179 | [Parameter(Mandatory = $true, HelpMessage='Target tenant id. This is azure ad directory id or external directory object id in exchange online.', ParameterSetName = 'TargetSetupAll')] 180 | [Parameter(Mandatory = $true, HelpMessage='Target tenant id. This is azure ad directory id or external directory object id in exchange online.', ParameterSetName = 'TargetSetupExchange')] 181 | [ValidateScript({ -not [string]::IsNullOrWhiteSpace($_) })] 182 | $ResourceTenantId, 183 | 184 | [Parameter(HelpMessage='Existing Application Id. If existing application Id is present and can be found, new application will not be created.', ParameterSetName = 'TargetSetupAll')] 185 | [Parameter(HelpMessage='Existing Application Id. If existing application Id is present and can be found, new application will not be created.', ParameterSetName = 'TargetSetupAzure')] 186 | [guid]$ExistingApplicationId = [guid]::Empty, 187 | 188 | [Parameter(Mandatory=$false, HelpMessage='Use this switch if you are connecting to a tenant in the US Government Cloud')] 189 | [Switch] 190 | $Government, 191 | 192 | [Parameter(Mandatory=$false, HelpMessage='Use this switch if you are connecting to a tenant in the US Government Cloud - Dod')] 193 | [Switch] 194 | $Dod 195 | ) 196 | 197 | $ErrorActionPreference = 'Stop' 198 | 199 | $ScriptPath = $MyInvocation.MyCommand.Path 200 | $ScriptDir = Split-Path $ScriptPath 201 | $MS_GRAPH_APP_ID = "00000003-0000-0000-c000-000000000000" 202 | $MS_GRAPH_APP_ROLE = "User.Invite.All" 203 | $EXO_APP_ID = "00000002-0000-0ff1-ce00-000000000000" 204 | $EXO_APP_ROLE = "Mailbox.Migration" 205 | $REPLY_URL = "https://office.com" 206 | $FIRSTPARTY_POWERSHELL_CLIENTID = "1950a258-227b-4e31-a9cf-717495945fc2" 207 | $FIRSTPARTY_POWERSHELL_CLIENT_REDIRECT_URI = 'https://login.microsoftonline.com/organizations/oauth2/nativeclient' -as [Uri] 208 | 209 | function Main() { 210 | 211 | $AzureAppPermissions = ([ApplicationPermissions]$AzureAppPermissions) 212 | if ($PSCmdlet.ParameterSetName -eq 'TargetSetupAll' -or $PSCmdlet.ParameterSetName -eq 'TargetSetupAzure') { 213 | Import-AzureModules 214 | if (-not $AzureAppPermissions.HasFlag([ApplicationPermissions]::MSGraph) -and $UseAppAndCertGeneratedForSendingInvitation) { 215 | Write-Error "Cannot use application for sending invitation as it does not have permissions on MSGraph" 216 | } 217 | 218 | if ($Government -eq $true -or $Dod -eq $true) { 219 | $azureADAccount = Connect-AzureAD -AzureEnvironmentName AzureUSGovernment 220 | } else { 221 | $azureAdAccount = Connect-AzureAD 222 | } 223 | Write-Verbose "Connected to AzureAD - $($azureADAccount | Out-String)" 224 | if ($Government -eq $true -or $Dod -eq $true) { 225 | $azAccount = Connect-AzAccount -Tenant $azureADAccount.Tenant.ToString() -Environment AzureUSGovernment 226 | } else { 227 | $azAccount = Connect-AzAccount -Tenant $azureADAccount.Tenant.ToString() 228 | } 229 | Write-Verbose "Connected to Az Account - $($azAccount | Out-String)" 230 | 231 | Write-Host "Setting up key vault in the $TargetTenantDomain tenant" 232 | $subscriptions = Get-AzSubscription 233 | Write-Verbose "SubscriptionId - $SubscriptionId was provided. Searching for it in $($subscriptions | Out-String)" 234 | $subscription = $subscriptions | ? { $_.SubscriptionId -eq $SubscriptionId} 235 | if (-not $subscription) { 236 | Write-Error "Subscription with id $SubscriptionId was not found." 237 | } 238 | 239 | Write-Verbose "Found subscription - $($SubscriptionId | Out-String)" 240 | Set-AzContext -Subscription $SubscriptionId 241 | 242 | ## Grab the EXO & MSGraph APP SPN ## 243 | $spns = @() 244 | $msGraphSpn = Get-AzureADServicePrincipal -Filter "AppId eq '$MS_GRAPH_APP_ID'" 245 | $exoAppSpn = Get-AzureADServicePrincipal -Filter "AppId eq '$EXO_APP_ID'" 246 | $spns += $msGraphSpn 247 | $spns += $exoAppSpn 248 | Write-Verbose "Found exchange service principal in $TargetTenantDomain - $($exoAppSpn | Out-String)" 249 | 250 | $certificatePublicKey, $certificatePrivateKey = Create-KeyVaultAndGenerateCertificate ` 251 | $TargetTenantDomain ` 252 | $ResourceTenantDomain ` 253 | $ResourceGroup ` 254 | $KeyVaultName ` 255 | $AzureResourceLocation ` 256 | $CertificateName ` 257 | $CertificateSubject ` 258 | $exoAppSpn.ObjectId ` 259 | $UseAppAndCertGeneratedForSendingInvitation ` 260 | $KeyVaultAuditStorageResourceGroup ` 261 | $KeyVaultAuditStorageAccountName ` 262 | $KeyVaultAuditStorageAccountLocation ` 263 | $KeyVaultAuditStorageAccountSKU ` 264 | $ExistingApplicationId 265 | 266 | Write-Verbose "Creating an application in $TargetTenantDomain" 267 | if (-not $AzureAppPermissions.HasFlag([ApplicationPermissions]::MSGraph)) { 268 | Write-Warning "MSGraph permission was not specified, however, an app needs at least one permission on ADGraph in order for admin to consent to it via the consent url. This app may only be consented from the azure portal." 269 | } 270 | 271 | $appOwnerTenantId, $appCreated = Create-Application $TargetTenantDomain $ResourceTenantDomain ($certificatePublicKey.Certificate) $spns ([ApplicationPermissions]$AzureAppPermissions) $ExistingApplicationId 272 | $global:AppId = $appCreated.AppId 273 | $appReplyUrl = $appCreated.ReplyUrls[0] 274 | $global:CertificateId = $certificatePublicKey.Id 275 | Write-Host "Application details to be registered in organization relationship: ApplicationId: [ $AppId ]. KeyVault secret Id: [ $CertificateId ]. These values are available in variables `$AppId and `$CertificateId respectively" -Foreground Green 276 | Write-Verbose "Sending the consent URI for this app to $ResourceTenantAdminEmail." 277 | Read-Host "Please consent to the app for $TargetTenantDomain before sending invitation to $ResourceTenantAdminEmail" 278 | Send-AdminConsentUri $TargetTenantDomain $ResourceTenantDomain $ResourceTenantAdminEmail $AppId $certificatePrivateKey $appReplyUrl $appCreated.DisplayName 279 | } 280 | 281 | if ($PSCmdlet.ParameterSetName -eq 'TargetSetupAll' -or $PSCmdlet.ParameterSetName -eq 'TargetSetupExchange') { 282 | $AppId = Ensure-VariableIsPopulated "AppId" "Please enter the application id for the azure ad application to be used for mailbox migrations" 283 | $CertificateId = Ensure-VariableIsPopulated "CertificateId" "Please enter the key vault url for the migration app's secret" 284 | Run-ExchangeSetupForTargetTenant $TargetTenantDomain $ResourceTenantDomain $ResourceTenantId $AppId $CertificateId $MigrationEndpointMaxConcurrentMigrations 285 | Write-Host "Exchange setup complete. Migration endpoint details are available in `$MigrationEndpoint variable" -Foreground Green 286 | } 287 | } 288 | 289 | function Check-ExchangeOnlinePowershellConnection { 290 | if ($Null -eq (Get-Command New-OrganizationRelationship -ErrorAction SilentlyContinue)) { 291 | Write-Error "Please connect to the Exchange Online Management module or Exchange Online through basic authentication before running this script!"; 292 | } 293 | } 294 | 295 | function Check-AzurePowershellConnection { 296 | if ($Null -eq (Get-AzLocation -ErrorAction SilentlyContinue | ?{$_.DisplayName -eq $AzureResourceLocation}) -and $Government -eq $true -or (Get-AzLocation -ErrorAction SilentlyContinue | ?{$_.DisplayName -eq $AzureResourceLocation}) -and $Dod) { 297 | Connect-AzAccount -Environment AzureUSGovernment 298 | } elseif ($Null -eq (Get-AzLocation -ErrorAction SilentlyContinue | ?{$_.DisplayName -eq $AzureResourceLocation})) { 299 | Connect-AzAccount 300 | } if ($Null -eq (Get-AzLocation -ErrorAction SilentlyContinue | ?{$_.DisplayName -eq $AzureResourceLocation})) { 301 | Write-Error "A valid Azure location was not specified, please run Get-AzLocation to determine a valid location." 302 | } 303 | } 304 | 305 | function Import-AzureModules() { 306 | $desiredAzureModules = @{ 307 | "AzureAD" = [Version]"2.0.2.4"; 308 | "Az.Monitor" = [Version]"1.2.0"; 309 | "Az.KeyVault" = [Version]"1.2.0"; 310 | "Az.Accounts" = [Version]"1.5.2"; 311 | "Az.Resources" = [Version]"1.3.1"; 312 | "Az.Storage" = [Version]"1.9.0"; 313 | } 314 | 315 | $moduleMissingErrors = @() 316 | $desiredAzureModules.Keys | % { 317 | $desiredVersion = [Version]($desiredAzureModules[$_]) 318 | $desiredAzModule = (Get-Module $_ -ListAvailable -Verbose:$false | ? { $_.Version -ge $desiredVersion}) 319 | if (-not $desiredAzModule) { 320 | $moduleMissingErrors += "Powershell module: [$_] minimum version [$($desiredAzureModules[$_])] is required for running this script. Please install this module using: Install-Module $_ -AllowClobber" 321 | } 322 | } 323 | 324 | if ($moduleMissingErrors) { 325 | Write-Error "Missing modules - `r`n$([string]::Join("`r`n", $moduleMissingErrors))" 326 | } 327 | 328 | Import-Module AzureAD -Verbose:$false | Out-Null 329 | $desiredAzureModules.Keys | Import-Module -verbose:$false | Out-Null 330 | } 331 | 332 | function Ensure-VariableIsPopulated([string]$variableName, [string]$message) { 333 | $val = Get-Variable $variableName -ErrorAction Ignore 334 | if (-not $val) { 335 | $enteredVal = Read-Host $message 336 | if (-not $enteredVal) { 337 | Write-Error "Entered value was not valid" 338 | } 339 | 340 | $enteredVal 341 | } 342 | 343 | $val.Value 344 | } 345 | 346 | function Create-KeyVaultAndGenerateCertificate([string]$targetTenant, ` 347 | [string]$resourceTenantDomain, ` 348 | [string]$resourceGrpName, ` 349 | [string]$kvName, ` 350 | [string]$arLocation, ` 351 | [string]$certName, ` 352 | [string]$certSubj, ` 353 | [string]$exoAppObjectId, ` 354 | [bool]$retrieveCertPrivateKey, ` 355 | [string]$auditStorageAcntRG, ` 356 | [string]$auditStorageAcntName, ` 357 | [string]$auditStorageAcntLocation, ` 358 | [string]$auditStorageAcntSKU, ` 359 | [guid]$existingApplicationId) { 360 | if ([string]::IsNullOrWhiteSpace($certName)) { 361 | $randomPrefix = [Random]::new().Next(0, 10000) 362 | $certName = $randomPrefix.ToString() + "TenantFriendingAppSecret" 363 | } 364 | 365 | $resGrp = $null 366 | try { 367 | $resGrp = Get-AzResourceGroup -Name $resourceGrpName 368 | if ($resGrp) { 369 | Write-Verbose "Resource group $resourceGrpName already exists." 370 | } 371 | } catch { 372 | Write-Verbose "Resource group $resourceGrpName not found, this will be created." 373 | } 374 | 375 | if (-not $resGrp) { 376 | Write-Verbose "Creating resource group - $resourceGrpName" 377 | $resGrp = New-AzResourceGroup -Name $resourceGrpName -Location $arLocation 378 | Write-Host "Resource Group $resourceGrpName successfully created" -Foreground Green 379 | } 380 | 381 | $kv = $null 382 | try { 383 | $kv = Get-AzKeyVault -VaultName $kvName -ResourceGroupName $resourceGrpName 384 | } catch { 385 | Write-Verbose "KeyVault $kvName not found, this will be created." 386 | } 387 | 388 | if ($kv) { 389 | Write-Verbose "Keyvault $kvName already exists." 390 | } else { 391 | Write-Verbose "Creating KeyVault $kvName" 392 | $kv = New-AzKeyVault -Name $kvName -Location $arLocation -ResourceGroupName $resourceGrpName 393 | Write-Host "KeyVault $kvName successfully created" -Foreground Green 394 | } 395 | 396 | $storageAcnt = $null 397 | if ($auditStorageAcntRG -and $auditStorageAcntName) { 398 | Write-Verbose "Setting up auditing for key vault $kvName" 399 | 400 | $storageResGrp = Get-AzStorageAccount -ResourceGroupName $auditStorageAcntRG -Name $auditStorageAcntName -ErrorAction SilentlyContinue 401 | if ($storageResGrp -eq $null) 402 | { 403 | Write-Verbose "Resource group '$auditStorageAcntRG' not found... creating resource group in '$auditStorageAcntLocation'" 404 | $storageResGrp = New-AzResourceGroup -Name $auditStorageAcntRG -Location $auditStorageAcntLocation 405 | } 406 | 407 | $storageAcnt = Get-AzStorageAccount -ResourceGroupName $auditStorageAcntRG -Name $auditStorageAcntName -ErrorAction SilentlyContinue 408 | if ($storageAcnt -eq $null) 409 | { 410 | Write-Verbose "Az storage account '$auditStorageAcntName' not found... creating storage account with Location '$auditStorageAcntLocation', SKU '$auditStorageAcntSKU'" 411 | $storageAcnt = New-AzStorageAccount -ResourceGroupName $auditStorageAcntRG -AccountName $auditStorageAcntName -Location $auditStorageAcntLocation -SkuName $auditStorageAcntSKU 412 | } 413 | 414 | Set-AzDiagnosticSetting -ResourceId $kv.ResourceId -StorageAccountId $storageAcnt.Id -Enabled $true -Category AuditEvent | Out-Null 415 | Write-Host "Auditing setup successfully for $kvName" -Foreground Green 416 | } 417 | 418 | Write-Verbose "Setting up access for key vault $kvName" 419 | Set-AzKeyVaultAccessPolicy -ResourceId $kv.ResourceId -ObjectId $exoAppObjectId -PermissionsToSecrets get,list -PermissionsToCertificates get,list | Out-Null 420 | Write-Host "Exchange app given access to KeyVault $kvName" -Foreground Green 421 | try { 422 | $cert = Get-AzKeyVaultCertificate -VaultName $kvName -Name $certName 423 | if ($cert.Certificate) { 424 | Write-Verbose "Certificate $certName already exists in $kvName" 425 | if ($retrieveCertPrivateKey -eq $true) { 426 | Write-Verbose "Retrieving certificate private key" 427 | $certPrivateKey = Get-AzKeyVaultSecret -VaultName $kvName -Name $certName 428 | } 429 | 430 | return $cert, $certPrivateKey 431 | } 432 | } catch { 433 | Write-Verbose "Certificate not found, a new request will be generated." 434 | } 435 | 436 | if ( [string]::IsNullOrWhiteSpace($certSubj)) { 437 | $certSubj = "CN=" + $targetTenant + "_" + $resourceTenantDomain + "_" + ([Random]::new().Next(0, 10000)).ToString() 438 | Write-Verbose "Cert subject not provided, generated subject - $certSubj" 439 | } 440 | 441 | $policy = New-AzKeyVaultCertificatePolicy -SubjectName $certSubj -IssuerName Self -ValidityInMonths 12 442 | $certReq = Add-AzKeyVaultCertificate -VaultName $kvName -Name $certName -CertificatePolicy $policy 443 | Write-Host "Self signed certificate requested in key vault - $kvName. Certificate name - $certName" -Foreground Green 444 | $tries = 5 445 | $certPrivateKey = $null 446 | while ($tries -gt 0) { 447 | try { 448 | Write-Verbose "Looking for certificate $certName. Attempt - $(6 - $tries)" 449 | $cert = Get-AzKeyVaultCertificate -VaultName $kvName -Name $certName 450 | if ($cert.Certificate) { 451 | Write-Verbose "Certificate found - $($cert | Out-String)" 452 | if ($retrieveCertPrivateKey -eq $true) { 453 | $certPrivateKey = Get-AzKeyVaultSecret -VaultName $kvName -Name $certName 454 | if ($certPrivateKey) { 455 | Write-Verbose "Certificate private key also found" 456 | break; 457 | } else { 458 | if ($tries -lt 0) { 459 | Write-Error "Certificate private key not found after retries." 460 | } 461 | 462 | Write-Verbose "Certificate public key is present, however, its private key is not available, waiting 5 secs and looking again." 463 | } 464 | } 465 | } else { 466 | if ($tries -lt 0) { 467 | Write-Error "Certificate not found after retries." 468 | } 469 | 470 | Write-Verbose "Certificate not found, waiting 5 secs and looking again." 471 | sleep 5 472 | } 473 | } catch { 474 | if ($tries -lt 0) { 475 | Write-Error "Certificate not found after retries." 476 | } 477 | 478 | sleep 60 479 | } 480 | 481 | $tries-- 482 | } 483 | 484 | Write-Verbose "Returning cert - $($cert.Certificate | Out-String)" 485 | Write-Host "Certificate $certName successfully created" -Foreground Green 486 | $cert, $certPrivateKey 487 | } 488 | 489 | function Create-Application([string]$targetTenantDomain, [string]$resourceTenantDomain, $certificate, $spns, $azAppPermissions, [guid]$ExistingApplicationId) { 490 | if ([guid]::Empty -ne $ExistingApplicationId) { 491 | $existingApp = Get-AzureADApplication -Filter "AppId eq '$ExistingApplicationId'" 492 | if ($Null -ne $existingApp) { 493 | Write-Warning "Existing application '$ExistingApplicationId' found. Skipping new application creation." 494 | return (Get-AzureADTenantDetail).ObjectId, $existingApp 495 | } 496 | } 497 | 498 | #### Collect all the permissions first #### 499 | $appPermissions = @() 500 | $msGraphSpn = $null 501 | 502 | if ($azAppPermissions.HasFlag([ApplicationPermissions]::MSGraph)) { 503 | ## Calculate permission on MSGraph ## 504 | $msGraphSpn = $spns | ? { $_.AppId -eq $MS_GRAPH_APP_ID } 505 | if (-not $msGraphSpn) { 506 | Write-Error "Tenant does not have access to MSGraph" 507 | } 508 | 509 | $msGraphAppPermission = $msGraphSpn.AppRoles | ? { $_.Value -eq $MS_GRAPH_APP_ROLE } 510 | $reqGraph = New-Object -TypeName "Microsoft.Open.AzureAD.Model.RequiredResourceAccess" 511 | $reqGraph.ResourceAppId = $msGraphSpn.AppId 512 | $reqGraph.ResourceAccess = New-Object -TypeName "Microsoft.Open.AzureAD.Model.ResourceAccess" -ArgumentList $msGraphAppPermission.Id,"Role" 513 | $appPermissions += $reqGraph 514 | } 515 | 516 | if ($azAppPermissions.HasFlag([ApplicationPermissions]::Exchange)) { 517 | ## Calculate permission on EXO ## 518 | $exoAppSpn = $spns | ? { $_.AppId -eq $EXO_APP_ID } 519 | if (-not $exoAppSpn) { 520 | Write-Error "Tenant does not have Exchange enabled" 521 | } 522 | 523 | $exoAppPermission = $exoAppSpn.AppRoles | ? { $_.Value -eq $EXO_APP_ROLE } 524 | $reqExo = New-Object -TypeName "Microsoft.Open.AzureAD.Model.RequiredResourceAccess" 525 | $reqExo.ResourceAppId = $exoAppSpn.AppId 526 | $reqExo.ResourceAccess = New-Object -TypeName "Microsoft.Open.AzureAD.Model.ResourceAccess" -ArgumentList $exoAppPermission.Id,"Role" 527 | $appPermissions += $reqExo 528 | } 529 | 530 | #### Create the app with all the permissions #### 531 | $appOwnerTenantId = (Get-AzureADTenantDetail).ObjectId 532 | $randomSuffix = [Random]::new().Next(0, 10000) 533 | $appName = "$($targetTenantDomain.Split('.')[0])_Friends_$($resourceTenantDomain.Split('.')[0])_$randomSuffix" 534 | $appCreationParameters = @{ 535 | "AvailableToOtherTenants" = $true; 536 | "DisplayName" = $appName; 537 | "Homepage" = $REPLY_URL; 538 | "ReplyUrls" = $REPLY_URL; 539 | "RequiredResourceAccess" = $appPermissions 540 | } 541 | 542 | $appCreated = New-AzureADApplication @appCreationParameters 543 | 544 | $base64CertHash = [System.Convert]::ToBase64String($certificate.GetCertHash()) 545 | $base64CertVal = [System.Convert]::ToBase64String($certificate.GetRawCertData()) 546 | $appCertPwd = New-AzureADApplicationKeyCredential -ObjectId $appCreated.ObjectId -CustomKeyIdentifier $base64CertHash -Value $base64CertVal -StartDate ([DateTime]::Now) -EndDate ([DateTime]::Now).AddDays(363) -Type AsymmetricX509Cert -Usage Verify 547 | $spn = New-AzureADServicePrincipal -AppId $appCreated.AppId -AccountEnabled $true -DisplayName $appCreated.DisplayName 548 | $permissions = "" 549 | if ($azAppPermissions.HasFlag([ApplicationPermissions]::MSGraph)) { 550 | $permissions += "MSGraph - $MS_GRAPH_APP_ROLE. " 551 | } 552 | 553 | if ($azAppPermissions.HasFlag([ApplicationPermissions]::Exchange)) { 554 | $permissions += "Exchange - $EXO_APP_ROLE" 555 | } 556 | 557 | Write-Host "Application $appName created successfully in $targetTenantDomain tenant with following permissions. $permissions" -Foreground Green 558 | Write-Host "Admin consent URI for $targetTenantDomain tenant admin is -" -Foreground Yellow 559 | if ($Government -eq $true -or $DoD -eq $true) { 560 | Write-Host ("https://login.microsoftonline.us/{0}/adminconsent?client_id={1}&redirect_uri={2}" -f $targetTenantDomain, $appCreated.AppId, $appCreated.ReplyUrls[0]) 561 | } else { 562 | Write-Host ("https://login.microsoftonline.com/{0}/adminconsent?client_id={1}&redirect_uri={2}" -f $targetTenantDomain, $appCreated.AppId, $appCreated.ReplyUrls[0]) 563 | } 564 | 565 | Write-Host "Admin consent URI for $resourceTenantDomain tenant admin is -" -Foreground Yellow 566 | if ($Government -eq $true -or $DoD -eq $true) { 567 | Write-Host ("https://login.microsoftonline.us/{0}/adminconsent?client_id={1}&redirect_uri={2}" -f $resourceTenantDomain, $appCreated.AppId, $appCreated.ReplyUrls[0]) 568 | } else { 569 | Write-Host ("https://login.microsoftonline.com/{0}/adminconsent?client_id={1}&redirect_uri={2}" -f $resourceTenantDomain, $appCreated.AppId, $appCreated.ReplyUrls[0]) 570 | } 571 | 572 | return $appOwnerTenantId, $appCreated 573 | } 574 | 575 | function Get-AppOnlyToken([string]$authContextTenant, [string]$appId, [string]$resourceUri, $appSecretCert) { 576 | if ($Government -eq $true -or $DoD -eq $true) { 577 | $authority = "https://login.microsoftonline.us/$authContextTenant/oauth2/token" 578 | } else { 579 | $authority = "https://login.microsoftonline.com/$authContextTenant/oauth2/token" 580 | } 581 | 582 | $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority 583 | $ssPtr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($appSecretCert.SecretValue) 584 | 585 | try { 586 | $secretValueText = [System.Runtime.InteropServices.Marshal]::PtrToStringBSTR($ssPtr) 587 | }finally { 588 | [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($ssPtr) 589 | } 590 | 591 | $certBytes = [System.Convert]::FromBase64String($secretValueText) 592 | $clientCreds = new-object Microsoft.IdentityModel.Clients.ActiveDirectory.ClientAssertionCertificate -ArgumentList $appId, ([System.Security.Cryptography.X509Certificates.X509Certificate2]::new($certBytes)) 593 | Write-Verbose "Acquiring token resourceAppIdURI $resourceUri appSecret $appSecretCert" 594 | return $authContext.AcquireTokenAsync($resourceUri, $clientCreds).Result 595 | } 596 | 597 | function Get-AccessTokenWithUserPrompt([string]$authContextTenant, [string]$resourceUri) { 598 | if ($Government -eq $true -or $DoD -eq $true) { 599 | $authority = "https://login.microsoftonline.us/common/oauth2/token" 600 | } else { 601 | $authority = "https://login.microsoftonline.com/common/oauth2/token" 602 | } 603 | 604 | $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority 605 | Write-Verbose "Acquiring token resourceAppIdURI $resourceUri" 606 | $platformParams = New-Object Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters -ArgumentList ([Microsoft.IdentityModel.Clients.ActiveDirectory.PromptBehavior]::Always) 607 | return $authContext.AcquireTokenAsync($resourceUri, $FIRSTPARTY_POWERSHELL_CLIENTID, $FIRSTPARTY_POWERSHELL_CLIENT_REDIRECT_URI, $platformParams).GetAwaiter().GetResult() 608 | } 609 | 610 | function Send-AdminConsentUri([string]$invitingTenant, [string]$resourceTenantDomain, [string]$resourceTenantDomainAdminEmail, [string]$appId, $appSecretCert, [string]$appReplyUrl, [string]$appName) { 611 | $authRes = $null 612 | if ($Government -eq $true) { 613 | $msGraphResourceUri = "https://graph.microsoft.us" 614 | } elseif ($DoD -eq $true) { 615 | $msGraphResourceUri = "https://dod-graph.microsoft.us" 616 | } else { 617 | $msGraphResourceUri = "https://graph.microsoft.com" 618 | } 619 | Write-Verbose "Preparing invitation. Waiting for 10 secs before requesting token for the consented application to give time for replication." 620 | sleep 10 621 | if ($appSecretCert) { 622 | $authRes = Get-AppOnlyToken $invitingTenant $appId $msGraphResourceUri $appSecretCert 623 | } else { 624 | $authRes = Get-AccessTokenWithUserPrompt $invitingTenant $msGraphResourceUri $appId $appReplyUrl 625 | } 626 | 627 | if (-not $authRes) { 628 | Write-Error "Could not retrieve a token for invitation manager api call" 629 | } 630 | 631 | if ($Government -eq $true) { 632 | $invitationBody = @{ 633 | invitedUserEmailAddress = $resourceTenantDomainAdminEmail 634 | inviteRedirectUrl = ("https://login.microsoftonline.us/{0}/adminconsent?client_id={1}&redirect_uri={2}" -f $resourceTenantDomain, $appId, $appReplyUrl) 635 | sendInvitationMessage = $true 636 | invitedUserMessageInfo = @{ 637 | customizedMessageBody = "Organization [$invitingTenant] wishes to pull mailboxes from your organization using [$appName] application. ` 638 | If you recognize this application please click below to provide your consent. ` 639 | To authorize this application to be used for office 365 mailbox migration, please add its application id [$appId] to your organization relationship with [$invitingTenant] in the OAuthApplicationId property.` 640 | If the 'Accept Invitation' button does not properly work, the URL will state application [$appName] cannot be accessed at this time after clicking on the button.` 641 | Please copy the link in the email and paste it directly into the browser." 642 | } 643 | } 644 | } else { 645 | $invitationBody = @{ 646 | invitedUserEmailAddress = $resourceTenantDomainAdminEmail 647 | inviteRedirectUrl = ("https://login.microsoftonline.com/{0}/adminconsent?client_id={1}&redirect_uri={2}" -f $resourceTenantDomain, $appId, $appReplyUrl) 648 | sendInvitationMessage = $true 649 | invitedUserMessageInfo = @{ 650 | customizedMessageBody = "Organization [$invitingTenant] wishes to pull mailboxes from your organization using [$appName] application. ` 651 | If you recognize this application please click below to provide your consent. ` 652 | To authorize this application to be used for office 365 mailbox migration, please add its application id [$appId] to your organization relationship with [$invitingTenant] in the OAuthApplicationId property.` 653 | If the 'Accept Invitation' button does not properly work, the URL will state application [$appName] cannot be accessed at this time after clicking on the button.` 654 | Please copy the link in the email and paste it directly into the browser." 655 | } 656 | } 657 | 658 | $invitationBodyJson = $invitationBody | ConvertTo-Json 659 | $headers = New-Object "System.Collections.Generic.Dictionary[[String],[String]]" 660 | $headers.Add("Authorization", $authRes.CreateAuthorizationHeader()) 661 | Write-Verbose "Sending invitation" 662 | 663 | if ($Government -eq $true) { 664 | $resp = Invoke-RestMethod -Method POST -Uri "https://graph.microsoft.us/v1.0/invitations" -Body $invitationBodyJson -ContentType 'application/json' -Headers $headers 665 | } elseif ($DoD -eq $true) { 666 | $resp = Invoke-RestMethod -Method POST -Uri "https://dod-graph.microsoft.us/v1.0/invitations" -Body $invitationBodyJson -ContentType 'application/json' -Headers $headers 667 | } else { 668 | $resp = Invoke-RestMethod -Method POST -Uri "https://graph.microsoft.com/v1.0/invitations" -Body $invitationBodyJson -ContentType 'application/json' -Headers $headers 669 | } 670 | 671 | if ($resp -and $resp.invitedUserEmailAddress) { 672 | Write-Host "Successfully sent invitation to $($resp.invitedUserEmailAddress)" -Foreground Green 673 | } 674 | } 675 | } 676 | 677 | function Run-ExchangeSetupForTargetTenant([string]$targetTenant, [string]$resourceTenantDomain, [string]$resourceTenantId, [string]$appId, [string]$appSecretKeyVaultUrl, [int]$migEndpointMaxConcurrentMigrations) { 678 | # 1. Create/Update organization relationship. 679 | # 2. Create migration endpoint. 680 | 681 | Write-Host "Setting up exchange components on target tenant: $targetTenant" 682 | if (-not (Get-Command Get-OrganizationRelationship -ErrorAction Ignore)) { 683 | Write-Error "We could not find exchange powershell cmdlet. Please re-establish the session and rerun this script." 684 | } 685 | 686 | $orgRel = Get-OrganizationRelationship | ? { $_.DomainNames -contains $resourceTenantId } 687 | if ($orgRel) { 688 | Write-Verbose "Organization relationship already exists with $resourceTenantId. Updating it." 689 | $capabilities = @($orgRel.MailboxMoveCapability.Split(",").Trim()) 690 | if (-not $orgRel.MailboxMoveCapability.Contains("Inbound")) { 691 | Write-Verbose "Adding Inbound capability to the organization relationship. Existing capabilities: $capabilities" 692 | $capabilities += "Inbound" 693 | } 694 | 695 | $orgRel | Set-OrganizationRelationship -Enabled:$true -MailboxMoveEnabled:$true -MailboxMoveCapability $capabilities 696 | $orgRelName = $orgRel.Name 697 | } else { 698 | $randomSuffix = [Random]::new().Next(0, 10000) 699 | $orgRelName = "$($targetTenant.Split('.')[0])_$($resourceTenantDomain.Split('.')[0])_$randomSuffix" 700 | $maxLength = [System.Math]::Min(64, $orgRelName.Length) 701 | $orgRelName = $orgRelName.SubString(0, $maxLength) 702 | 703 | Write-Verbose "Creating organization relationship: $orgRelName in $targetTenant. DomainName: $resourceTenantId" 704 | New-OrganizationRelationship ` 705 | -DomainNames $resourceTenantId ` 706 | -Enabled:$true ` 707 | -MailboxMoveEnabled:$true ` 708 | -MailboxMoveCapability Inbound ` 709 | -Name $orgRelName 710 | } 711 | 712 | $migEndpoint = Get-MigrationEndpoint -Identity $orgRelName -ErrorAction SilentlyContinue 713 | if ($migEndpoint) 714 | { 715 | Write-Verbose "Remove existing migration endpoint $orgRelName" 716 | Remove-MigrationEndpoint -Identity $orgRelName 717 | } 718 | 719 | Write-Verbose "Creating migration endpoint $orgRelName with remote tenant: $resourceTenantDomain, appId: $appId, appSecret: $appSecretKeyVaultUrl" 720 | 721 | if (-not $MigrationEndpointMaxConcurrentMigrations -and $Government -eq $true) 722 | { 723 | $global:MigrationEndpoint = New-MigrationEndpoint ` 724 | -Name $orgRelName ` 725 | -RemoteTenant $resourceTenantDomain ` 726 | -RemoteServer "outlook.office365.us" ` 727 | -ApplicationId $appId ` 728 | -AppSecretKeyVaultUrl $appSecretKeyVaultUrl ` 729 | -ExchangeRemoteMove:$true 730 | } elseif ($Government -eq $true) { 731 | $global:MigrationEndpoint = New-MigrationEndpoint ` 732 | -Name $orgRelName ` 733 | -RemoteTenant $resourceTenantDomain ` 734 | -RemoteServer "outlook.office365.us" ` 735 | -ApplicationId $appId ` 736 | -AppSecretKeyVaultUrl $appSecretKeyVaultUrl ` 737 | -MaxConcurrentMigrations $MigrationEndpointMaxConcurrentMigrations ` 738 | -ExchangeRemoteMove:$true 739 | } elseif (-not $MigrationEndpointMaxConcurrentMigrations -and $DoD -eq $true) { 740 | $global:MigrationEndpoint = New-MigrationEndpoint ` 741 | -Name $orgRelName ` 742 | -RemoteTenant $resourceTenantDomain ` 743 | -RemoteServer "dod-outlook.office365.us" ` 744 | -ApplicationId $appId ` 745 | -AppSecretKeyVaultUrl $appSecretKeyVaultUrl ` 746 | -ExchangeRemoteMove:$true 747 | } elseif ($DoD -eq $true) { 748 | $global:MigrationEndpoint = New-MigrationEndpoint ` 749 | -Name $orgRelName ` 750 | -RemoteTenant $resourceTenantDomain ` 751 | -RemoteServer "dod-outlook.office365.us" ` 752 | -ApplicationId $appId ` 753 | -AppSecretKeyVaultUrl $appSecretKeyVaultUrl ` 754 | -MaxConcurrentMigrations $MigrationEndpointMaxConcurrentMigrations ` 755 | -ExchangeRemoteMove:$true 756 | } elseif (-not $MigrationEndpointMaxConcurrentMigrations) { 757 | $global:MigrationEndpoint = New-MigrationEndpoint ` 758 | -Name $orgRelName ` 759 | -RemoteTenant $resourceTenantDomain ` 760 | -RemoteServer "outlook.office.com" ` 761 | -ApplicationId $appId ` 762 | -AppSecretKeyVaultUrl $appSecretKeyVaultUrl ` 763 | -ExchangeRemoteMove:$true 764 | } else { 765 | $global:MigrationEndpoint = New-MigrationEndpoint ` 766 | -Name $orgRelName ` 767 | -RemoteTenant $resourceTenantDomain ` 768 | -RemoteServer "outlook.office.com" ` 769 | -ApplicationId $appId ` 770 | -AppSecretKeyVaultUrl $appSecretKeyVaultUrl ` 771 | -MaxConcurrentMigrations $MigrationEndpointMaxConcurrentMigrations ` 772 | -ExchangeRemoteMove:$true 773 | } 774 | 775 | if ($Null -ne $Error[0] -and $Error[0].Exception.ErrorRecord.FullyQualifiedErrorId.Contains("MaximumConcurrentMigrationLimitExceededException")) 776 | { 777 | Write-Error "Failed to create migration endpoint, please adjust MaxConcurrentMigrations for existing migration endpoints then re-run setup script with -MigrationEndpointMaxConcurrentMigrations option" 778 | } 779 | elseif (-not $MigrationEndpoint) 780 | { 781 | Write-Error "Failed to create migration endpoint, please contact crosstenantmigrationpreview@service.microsoft.com" 782 | } 783 | else 784 | { 785 | Write-Host "MigrationEndpoint created in $targetTenant for source $resourceTenantDomain" -Foreground Green 786 | $MigrationEndpoint 787 | } 788 | } 789 | 790 | 791 | $enumExists = $null 792 | try { 793 | $enumExists = [ApplicationPermissions] | Get-Member 794 | } catch { } 795 | 796 | if (-not $enumExists) { 797 | Add-Type -TypeDefinition @" 798 | using System; 799 | 800 | [Flags] 801 | public enum ApplicationPermissions 802 | { 803 | Exchange = 1, 804 | MSGraph = 2, 805 | All = Exchange | MSGraph 806 | } 807 | "@ 808 | } 809 | 810 | function PreValidation() { 811 | Write-Host `n 812 | Write-Host "Welcome to the Cross-tenant mailbox migration preview! Before running this script, please be sure to review the details provided on docs.microsoft.com at the following URL: `nhttps://docs.microsoft.com/en-us/microsoft-365/enterprise/cross-tenant-mailbox-migration" 813 | Write-Host "`nIt is also recommended before running this script to review the script in a script editor or Notepad prior to running."`n 814 | Write-Host "For general feedback and / or questions, please contact crosstenantmigrationpreview@service.microsoft.com.`nThis is not a support alias and should not be used if you are currently experiencing an issue and need immediate assistance."`n 815 | $title = "Confirm: Configure Cross-Tenant mailbox migration preview." 816 | $message = "`nIf you are ready to begin configuring your tenants, select 'Y'.`nIf you need to review any additional details and proceed at a later time, select 'N'.`n`nDo you wish to proceed?" 817 | $yes = New-Object System.Management.Automation.Host.ChoiceDescription "&Yes", "Yes" 818 | $no = New-Object System.Management.Automation.Host.ChoiceDescription "&No", "No" 819 | $options = [System.Management.Automation.Host.ChoiceDescription[]]($yes, $no) 820 | $choice=$host.ui.PromptForChoice($title, $message, $options, 1) 821 | if ($choice -ne 0) { 822 | Exit} 823 | Start-Sleep 2 824 | Write-Host "`nWe are verifying that you are using the latest version of the script."`n 825 | Write-Host "This requires that we download the latest version of the script from GitHub to compare with your local copy." 826 | Write-Host "This file will be stored on your local computer temporarily, as well as overwrite your existing script file if it is out of date." 827 | $title = "Confirm: Allow for download from GitHub." 828 | $message = "`nIf you are ready to begin this step, select 'Y'. `nIf you would prefer to manually download the scripts to make sure you have the latest version, select 'N'" 829 | $yes = New-Object System.Management.Automation.Host.ChoiceDescription "&Yes", "Yes" 830 | $no = New-Object System.Management.Automation.Host.ChoiceDescription "&No", "No" 831 | $options = [System.Management.Automation.Host.ChoiceDescription[]]($yes, $no) 832 | $choice=$host.ui.PromptForChoice($title, $message, $options, 1) 833 | if ($choice -ne 0) { 834 | Exit} 835 | else {Verification} 836 | } 837 | 838 | function Verification { 839 | Write-Host "`nBeginning verification steps."`n 840 | Check-ExchangeOnlinePowershellConnection 841 | Write-Host "Verifying ability to create a new organization relationship in the tenant." 842 | try { 843 | New-OrganizationRelationship -DomainNames contoso.onmicrosoft.com -Name Contoso -WhatIf -ErrorAction Stop 844 | } 845 | catch { 846 | Write-Output "You need to run the command Enable-OrganizationCustomization before continuing with execution of the script." 847 | Exit 848 | } 849 | Write-Host "`nVerifying that your script is up to date with the latest changes." 850 | Write-Host "`nBeginning download of SetupCrossTenantRelationshipForTargetTenant.ps1 and creation of temporary files." 851 | if ((Test-Path -Path $ScriptDir\XTenantTemp) -eq $true) { 852 | Remove-Item -Path $ScriptDir\XTenantTemp\ -Recurse -Force | Out-Null 853 | } 854 | Write-Host "`nVerifying that a valid location was specified for Azure`n" 855 | Check-AzurePowershellConnection 856 | New-Item -Path $ScriptDir -Name XTenantTemp -ItemType Directory | Out-Null 857 | Invoke-WebRequest -UseBasicParsing -Uri https://aka.ms/TargetTenant -Outfile $ScriptDir\XTenantTemp\SetupCrossTenantRelationshipForTargetTenant.ps1 858 | if ((Get-FileHash $ScriptDir\SetupCrossTenantRelationshipForTargetTenant.ps1).hash -eq (Get-FileHash $ScriptDir\XTenantTemp\SetupCrossTenantRelationshipForTargetTenant.ps1).hash) { 859 | Write-Host "`nYou are using the latest version of the script. Removing temporary files and proceeding with setup." 860 | Start-Sleep 1 861 | Remove-Item -Path $ScriptDir\XTenantTemp\ -Recurse -Force | Out-Null 862 | } 863 | elseif ((Get-FileHash $ScriptDir\SetupCrossTenantRelationshipForTargetTenant.ps1).hash -ne (Get-FileHash $ScriptDir\XTenantTemp\SetupCrossTenantRelationshipForTargetTenant.ps1).hash) { 864 | Write-Host "`nYou are not using the latest version of the script."`n 865 | Start-Sleep 1 866 | Write-Host "`nReplacing the local copy of SetupCrossTenantRelationshipForTargetTenant.ps1 and cleaning up temporary files..." 867 | Start-Sleep 1 868 | Copy-Item $ScriptDir\XTenantTemp\SetupCrossTenantRelationshipForTargetTenant.ps1 -Destination $ScriptDir | Out-Null 869 | Start-Sleep 1 870 | Remove-Item -Path $ScriptDir\XTenantTemp\ -Recurse -Force | Out-Null 871 | Write-Host "Update completed. You will need to run the script again." 872 | Start-Sleep 1 873 | Exit 874 | } 875 | } 876 | PreValidation 877 | Main 878 | 879 | <# 880 | Set-OrganizationRelationship -Identity \ -OAuthApplicationId 484a8384-979a-4cc9-8791-8e6bb34f76d4 881 | Set-OrganizationRelationship -Identity -OAuthApplicationId 484a8384-979a-4cc9-8791-8e6bb34f76d4 882 | Set-MigrationEndpoint -Identity 75f7afc6-417a-4fbe-801b-654f6b8f38e3 -Organization -ApplicationId 484a8384-979a-4cc9-8791-8e6bb34f76d4 -AppSecretKeyVaultUrl -SkipVerification 883 | New-MoveRequest -Remote -RemoteTenant -TargetDeliveryDomain -SourceEndpoint 75f7afc6-417a-4fbe-801b-654f6b8f38e3 -whatif 884 | #> 885 | <# 886 | function Verify-ApplicationLocalTenant ([bool]$localTenant, [string]$appId, [string]$targetTenant, [string]$appReplyUrl, [string]$friendTenant) { 887 | if ($localTenant -eq $false -and $Government -eq $true -or $localTenant -eq $false -and $Dod -eq $true) { 888 | Write-Host "Log into $friendTenant" 889 | Connect-AzureAD -AzureEnvironmentName AzureUSGovernment 890 | } else { 891 | Write-Host "Log into $friendTenant" 892 | Connect-AzureAD 893 | } 894 | 895 | $consentDomain = "" 896 | if ($localTenant -eq $true) { 897 | $consentDomain = $targetTenant 898 | } else { 899 | $consentDomain = $friendTenant 900 | } 901 | 902 | $spn = Get-AzureADServicePrincipal -All $true | ? { $_.AppId -eq $appId } 903 | if (!$spn) { 904 | Write-Error "SPN of the app was not created in $consentDomain tenant" 905 | return 906 | } 907 | 908 | # Check MSGraph and EXO has incoming app roles assignment from the tenant friending app 909 | # 1. collect spns of MSGraph and EXO applications 910 | $spns = Get-AzureADServicePrincipal -All $true | ? { $_.AppId -in @($MS_GRAPH_APP_ID, $EXO_APP_ID) } 911 | if (!$spns) { 912 | Write-Error "Internal Error: SPNs of MSGraph or EXO not found." 913 | return 914 | } 915 | 916 | $spnExists = $true 917 | $spns | % { 918 | # Get SPN of an App 919 | # https://graph.microsoft.com/beta/tgttenant.onmicrosoft.com/servicePrincipals?$filter=appId eq '851174ff-ddd3-4bfe-b5fe-c7e5af95143c' 920 | # Get application roles assigned from SPN 921 | # https://graph.microsoft.com/beta/tgttenant.onmicrosoft.com/servicePrincipals/f05a1a01-a082-46b5-bd81-1bc66e13e408/appRoleAssignedTo 922 | # If admin consented then there is an app role assignment from App -> MSGraph/EXO 923 | $appRoleAssignments = Get-AzureADServiceAppRoleAssignment -ObjectId $_.ObjectId -All $true | ? { $_.PrincipalId -eq $spn.ObjectId } 924 | if (!$appRoleAssignments -and $spnExists -eq $true) { 925 | $spnExists = $false 926 | Write-Error "The app: $appId is not consented by tenant admin of $consentDomain. Please consent using the following link:" 927 | "https://login.microsoftonline.com/{0}/adminconsent?client_id={1}&redirect_uri={2}" -f $consentDomain, $appId, $appReplyUrl 928 | return 929 | } 930 | } 931 | 932 | if ($spnExists) { 933 | Write-Host "Application $appId is setup correctly in $consentDomain tenant" -Foreground Green 934 | } 935 | } 936 | 937 | function Remove-AppRoleAssignment ([string]$appId, [string]$appIdToRemovePermissionOn) { 938 | # App: $appId has permission on $appIdToRemovePermissionOn 939 | # First getting spn of appId 940 | $spn = Get-AzureADServicePrincipal -All $true | ? { $_.AppId -eq $appId } 941 | if (!$spn) { 942 | Write-Error "SPN of the app was not created in $consentDomain tenant" 943 | return 944 | } 945 | 946 | # Get spn of app which $appId has permission on, this would be either MSGraph or EXO application 947 | $spnIdToRemovePermissionOn = Get-AzureADServicePrincipal -All $true | ? { $_.AppId -eq $appIdToRemovePermissionOn } 948 | if (!$spnIdToRemovePermissionOn) { 949 | Write-Error "Internal Error: SPNs of MSGraph or EXO not found." 950 | return 951 | } 952 | 953 | $appRoleAssignments = Get-AzureADServiceAppRoleAssignment -ObjectId $spnIdToRemovePermissionOn.ObjectId -All $true | ? { $_.PrincipalId -eq $spn.ObjectId } 954 | if (!$appRoleAssignments) { 955 | Write-Error "The app: $appId does not have any permission on $appIdToRemovePermissionOn" 956 | return 957 | } 958 | 959 | # Remove the app role. 960 | Remove-AzureADServiceAppRoleAssignment -ObjectId $appRoleAssignments.PrincipalId -AppRoleAssignmentId $appRoleAssignments.ObjectId 961 | } 962 | 963 | function Get-AdministrativeUnits ($authRes) { 964 | $headers = New-Object "System.Collections.Generic.Dictionary[[String],[String]]" 965 | $headers.Add("Authorization", $authRes.CreateAuthorizationHeader()) 966 | Invoke-RestMethod -Method GET -Uri "https://graph.microsoft.com/beta/administrativeUnits" -ContentType 'application/json' -Headers $headers 967 | } 968 | 969 | function Create-AdministrativeUnit ($authRes) { 970 | $headers = New-Object "System.Collections.Generic.Dictionary[[String],[String]]" 971 | $headers.Add("Authorization", $authRes.CreateAuthorizationHeader()) 972 | $AuCreationBody = @{ 973 | displayName = "Mergers AU" 974 | description = "Admin unit for M&A" 975 | } 976 | 977 | $AuCreationBodyJson = $AuCreationBody | ConvertTo-Json 978 | Invoke-RestMethod -Method POST -Uri "https://graph.microsoft.com/beta/administrativeUnits" -ContentType 'application/json' -Headers $headers -Body $AuCreationBodyJson 979 | }#> 980 | -------------------------------------------------------------------------------- /VerifySetup.ps1: -------------------------------------------------------------------------------- 1 | ################################################################################# 2 | # 3 | # The sample scripts are not supported under any Microsoft standard support 4 | # program or service. The sample scripts are provided AS IS without warranty 5 | # of any kind. Microsoft further disclaims all implied warranties including, without 6 | # limitation, any implied warranties of merchantability or of fitness for a particular 7 | # purpose. The entire risk arising out of the use or performance of the sample scripts 8 | # and documentation remains with you. In no event shall Microsoft, its authors, or 9 | # anyone else involved in the creation, production, or delivery of the scripts be liable 10 | # for any damages whatsoever (including, without limitation, damages for loss of business 11 | # profits, business interruption, loss of business information, or other pecuniary loss) 12 | # arising out of the use of or inability to use the sample scripts or documentation, 13 | # even if Microsoft has been advised of the possibility of such damages. 14 | # 15 | ################################################################################# 16 | 17 | <# .SYNOPSIS 18 | This script can be used by a tenant that wishes to validate the setup required for cross-tenant mailbox migration. 19 | 20 | This script performs the following checks when run with -Context Target parameter: 21 | 1. Validates the following on the AAD application: 22 | a. Is registered in the target tenant directory 23 | b. Is setup with right permissions on MSGraph and Exchange 24 | c. Is consented by an administrator 25 | 2. Validates the following in OrganizationRelationship: 26 | a. Has a relationship with Source tenant 27 | b. The move direction is correct. 28 | 3. Validates the following on Migration Endpoint: 29 | a. ApplicationId is correct. 30 | b. RemoteTenantId is correct. 31 | 32 | .PARAMETER PartnerTenantId 33 | PartnerTenantId - the tenant id of the partner tenant. 34 | 35 | .PARAMETER PartnerTenantDomain 36 | PartnerTenantDomain - the tenant domain of the partner tenant. 37 | 38 | .PARAMETER ApplicationId 39 | ApplicationId - the application setup for mailbox migration. 40 | 41 | .EXAMPLE - TargetTenant 42 | $report = VerifySetup.ps1 -PartnerTenantId -ApplicationId -PartnerTenantDomain -Verbose 43 | 44 | .EXAMPLE - TargetTenant 45 | $report = VerifySetup.ps1 -PartnerTenantId -ApplicationId -PartnerTenantDomain -Verbose 46 | 47 | .EXAMPLE - SourceTenant 48 | $report = VerifySetup.ps1 -PartnerTenantId -ApplicationId 49 | #> 50 | 51 | param 52 | ( 53 | [Parameter(Mandatory = $true, HelpMessage='Partner tenant id', ParameterSetName = 'VerifyTarget')] 54 | [Parameter(Mandatory = $true, HelpMessage='Partner tenant id', ParameterSetName = 'VerifySource')] 55 | [ValidateScript({ -not [string]::IsNullOrWhiteSpace($_) })] 56 | [string]$PartnerTenantId, 57 | 58 | 59 | 60 | [Parameter(Mandatory = $true, HelpMessage='AAD ApplicationId', ParameterSetName = 'VerifyTarget')] 61 | [Parameter(Mandatory = $true, HelpMessage='Partner tenant id', ParameterSetName = 'VerifySource')] 62 | [ValidateScript({ -not [string]::IsNullOrWhiteSpace($_) })] 63 | [string]$ApplicationId, 64 | 65 | 66 | 67 | [Parameter(Mandatory = $true, HelpMessage='PartnerTenantDomain', ParameterSetName = 'VerifyTarget')] 68 | [ValidateScript({ -not [string]::IsNullOrWhiteSpace($_) })] 69 | [string]$PartnerTenantDomain 70 | ) 71 | 72 | $ErrorActionPreference = 'Stop' 73 | 74 | $MS_GRAPH_APP_ID = "00000003-0000-0000-c000-000000000000" 75 | $EXO_APP_ID = "00000002-0000-0ff1-ce00-000000000000" 76 | $EXO_APP_ROLE = "Mailbox.Migration" 77 | 78 | function Main() { 79 | $report = @{} 80 | Check-ExchangeOnlinePowershellConnection 81 | $isTargetTenant = $PSCmdlet.ParameterSetName -eq 'VerifyTarget' 82 | $azureADAccount = Connect-AzureAD 83 | Write-Verbose "Connected to AzureAD - $($azureADAccount | Out-String)" 84 | $azAccount = Connect-AzAccount -Tenant $azureADAccount.Tenant.ToString() 85 | Write-Verbose "Connected to Az Account - $($azAccount | Out-String)" 86 | $currentTenantId = $azureADAccount.TenantId.Guid 87 | 88 | Write-Verbose "Verifying Application; AppId: [$ApplicationId] Current tenant: [$currentTenantId] Partner tenant: [$PartnerTenantId] IsTargetTenant: [$isTargetTenant]" 89 | $errors, $warnings = Verify-Application $ApplicationId $currentTenantId $PartnerTenantId $isTargetTenant 90 | $report["Application"] = @{ "Errors" = $errors; "Warnings" = $warnings } 91 | Write-Host "`r`n" 92 | Print-Result "Verifying AAD Application" $errors $warnings 93 | 94 | Write-Verbose "Verifying OrganizationRelationship; AppId: [$ApplicationId] Partner tenant: [$PartnerTenantId] IsTargetTenant: [$isTargetTenant]" 95 | $errors = Verify-OrganizationRelationship $PartnerTenantId $ApplicationId $isTargetTenant 96 | Print-Result "Verifying OrganizationRelationship" $errors 97 | $report["OrganizationRelationship"] = @{ "Errors" = $errors } 98 | 99 | if ($isTargetTenant -eq $true) { 100 | Write-Verbose "Verifying MigrationEndpoint; AppId: [$ApplicationId] Partner tenant: [$PartnerTenantDomain]" 101 | $errors = Verify-MigrationEndpoint $PartnerTenantDomain $ApplicationId 102 | Print-Result "Verifying MigrationEndpoint" $errors 103 | $report["MigrationEndpoint"] = @{ "Errors" = $errors } 104 | } 105 | 106 | Write-Verbose ($report | ConvertTo-Json) 107 | $report 108 | } 109 | 110 | function Print-Result([string]$opName, $errors, $warnings) { 111 | Write-Host "[$opName].............." -NoNewLine 112 | if (!$errors -and !$warnings) { 113 | Write-Host "[Passed]" -NoNewLine -ForeGroundColor Green 114 | Write-Host "`r`n" 115 | return 116 | } 117 | 118 | if ($errors) { 119 | Write-Host "[Failed]" -ForeGroundColor Red 120 | Write-Host ($errors -join "`n") -ForeGroundColor Red 121 | } 122 | 123 | if ($warnings) { 124 | if (!$errors) { 125 | Write-Host "[Warnings]" -ForeGroundColor Yellow 126 | } 127 | 128 | Write-Host ($warnings -join "`n") -ForeGroundColor Yellow 129 | } 130 | 131 | Write-Host "`r`n`r`n" 132 | } 133 | 134 | function Verify-Application ([string]$appId, [string]$currentTenantId, [string]$partnerTenantId, [bool]$isTargetTenant) { 135 | $warnings = @() 136 | $errors = @() 137 | $spn = Get-AzureADServicePrincipal -Filter "AppId eq '$appId'" 138 | if (!$spn) { 139 | $errors += "App [$appId] is not registered in [$currentTenantId] tenant" 140 | return $errors 141 | } 142 | 143 | if ($isTargetTenant -eq $true) { 144 | if ($spn.AppOwnerTenantId -ne $currentTenantId) { 145 | $error += "App [$appId] was found in the [$currentTenantId] tenant but is not owned by it. Since this is target tenant, the app used for migration must be owned by target tenant." 146 | } 147 | } elseif ($spn.AppOwnerTenantId -ne $partnerTenantId) { 148 | $error += "App [$appId] was found in the [$currentTenantId] tenant but is not owned by $partnerTenantId. Please use an application owned by target tenant for mailbox migrations." 149 | } 150 | 151 | # Check MSGraph and EXO has incoming app roles assignment from the tenant friending app 152 | # 1. collect spns of MSGraph and EXO applications 153 | $msGraphSpn = Get-AzureADServicePrincipal -Filter "AppId eq '$MS_GRAPH_APP_ID'" 154 | $exoSpn = Get-AzureADServicePrincipal -Filter "AppId eq '$EXO_APP_ID'" 155 | if (!$msGraphSpn -or !$exoSpn) { 156 | $errors += "Internal Error: SPNs of MSGraph or EXO not found." 157 | return $errors 158 | } 159 | 160 | # Get the permission objects from Exo and MsGraph 161 | $exoMailboxMigrationPermissions = $exoSpn.AppRoles | ? { $_.Value -eq $EXO_APP_ROLE } 162 | $msGraphDirectoryPermissions = $msGraphSpn.AppRoles | ? { $_.Value -eq $MS_GRAPH_APP_ROLE } 163 | 164 | # Get the permission objects of the permissions assigned to the application 165 | $exoPermissionForApp = Get-AzureADServiceAppRoleAssignment -ObjectId $exoSpn.ObjectId -All $true | ? { $_.PrincipalId -eq $spn.ObjectId } 166 | $msGraphPermissionForApp = Get-AzureADServiceAppRoleAssignment -ObjectId $msGraphSpn.ObjectId -All $true | ? { $_.PrincipalId -eq $spn.ObjectId } 167 | 168 | if (!$exoPermissionForApp -or ($exoPermissionForApp.Id -ne $exoMailboxMigrationPermissions.Id)) { 169 | $errors += "App [$appId] does not have [$EXO_APP_ROLE] permission on Exchange setup or the permission is not consented by an Administrator" 170 | } 171 | 172 | $errors, $warnings 173 | } 174 | 175 | function Verify-OrganizationRelationship([string]$partnerTenantId, [string]$appId, [bool]$isTargetTenant) { 176 | $errors = @() 177 | $orgRel = Get-OrganizationRelationship | ? { $_.DomainNames -contains $partnerTenantId } 178 | if (!$orgRel) { 179 | $errors += "Organization relationship does not exist with [$partnerTenantId]" 180 | return $errors 181 | } 182 | 183 | if ($isTargetTenant -eq $true) { 184 | if (!$orgRel.MailboxMoveEnabled) { 185 | $errors += "MailboxMove is not enabled in Organization relationship with [$partnerTenantId]" 186 | } 187 | 188 | if ($orgRel.MailboxMoveCapability -ne 'Inbound') { 189 | $errors += "MailboxMoveCapability is invalid in Organization relationship with [$partnerTenantId]. It should be [Inbound] found [$($orgRel.MailboxMoveCapability)]" 190 | } 191 | 192 | if ($errors) { 193 | return $errors 194 | } 195 | } else { 196 | if (!$orgRel.MailboxMoveEnabled) { 197 | $errors += "MailboxMove is not enabled in Organization relationship with [$partnerTenantId]" 198 | } 199 | 200 | if ($orgRel.MailboxMoveCapability -ne 'RemoteOutbound') { 201 | $errors += "MailboxMoveCapability is invalid in Organization relationship with [$partnerTenantId]. It should be [RemoteOutbound] found [$($orgRel.MailboxMoveCapability)]" 202 | } 203 | 204 | if ($orgRel.OAuthApplicationId -ne $appId) { 205 | $errors += "Mailbox Migration ApplicationId is not whitelisted in the Organization Relationship with [$partnerTenantId]. Expected [$appId] found [$($orgRel.ApplicationId)]" 206 | } 207 | 208 | if (!$orgRel.MailboxMovePublishedScopes) { 209 | $errors += "Source tenant needs to specify MailboxMovePublishedScopes to allow migration" 210 | } 211 | } 212 | 213 | return $errors 214 | } 215 | 216 | function Verify-MigrationEndpoint([string]$partnerTenantDomain, [string]$appId) { 217 | $errors = @() 218 | $migEp = Get-MigrationEndpoint | ? { $_.ApplicationId -eq $appId } 219 | if (!$migEp) { 220 | $errors += "Migration Endpoint containing [$appId] not found." 221 | return $errors 222 | } 223 | 224 | if ($migEp.RemoteTenant -ne $partnerTenantDomain) { 225 | $errors += "RemoteTenant does not match in Migration Endpoint. Expected [$partnerTenantDomain] found [$($migEp.RemoteTenant)]" 226 | } 227 | 228 | if ($migEp.ApplicationId -ne $appId) { 229 | $errors += "ApplicationId does not match in Migration Endpoint. Expected [$appId] found [$($migEp.ApplicationId)]" 230 | } 231 | 232 | if (!$migEp.IsRemote) { 233 | $errors += "IsRemote does not match in Migration Endpoint. Expected [true] found [$($migEp.IsRemote)]" 234 | } 235 | 236 | return $errors 237 | } 238 | 239 | function Check-ExchangeOnlinePowershellConnection { 240 | if ($Null -eq (Get-Command New-OrganizationRelationship -ErrorAction SilentlyContinue)) { 241 | Write-Error "Please connect to the Exchange Online Management module or Exchange Online through basic authentication before running this script!"; 242 | } 243 | } 244 | 245 | Main -------------------------------------------------------------------------------- /VerifySetupDeprecated.ps1: -------------------------------------------------------------------------------- 1 | ################################################################################# 2 | # 3 | # The sample scripts are not supported under any Microsoft standard support 4 | # program or service. The sample scripts are provided AS IS without warranty 5 | # of any kind. Microsoft further disclaims all implied warranties including, without 6 | # limitation, any implied warranties of merchantability or of fitness for a particular 7 | # purpose. The entire risk arising out of the use or performance of the sample scripts 8 | # and documentation remains with you. In no event shall Microsoft, its authors, or 9 | # anyone else involved in the creation, production, or delivery of the scripts be liable 10 | # for any damages whatsoever (including, without limitation, damages for loss of business 11 | # profits, business interruption, loss of business information, or other pecuniary loss) 12 | # arising out of the use of or inability to use the sample scripts or documentation, 13 | # even if Microsoft has been advised of the possibility of such damages. 14 | # 15 | ################################################################################# 16 | 17 | <# .SYNOPSIS 18 | This script can be used by a tenant that wishes to validate the setup required for cross-tenant mailbox migration. 19 | 20 | This script performs the following checks when run with -Context Target parameter: 21 | 1. Validates the following on the AAD application: 22 | a. Is registered in the target tenant directory 23 | b. Is setup with right permissions on MSGraph and Exchange 24 | c. Is consented by an administrator 25 | 2. Validates the following in KeyVault: 26 | a. The KeyVault url is correct 27 | b. Exchange first party application has READ permissions on the secret 28 | 3. Validates the following in OrganizationRelationship: 29 | a. Has a relationship with Source tenant 30 | b. The move direction is correct. 31 | 4. Validates the following on Migration Endpoint: 32 | a. ApplicationId is correct. 33 | b. ApplicationKeyVaultUrl is correct. 34 | c. RemoteTenantId is correct. 35 | 36 | .PARAMETER PartnerTenantId 37 | PartnerTenantId - the tenant id of the partner tenant. 38 | 39 | .PARAMETER PartnerTenantDomain 40 | PartnerTenantDomain - the tenant domain of the partner tenant. 41 | 42 | .PARAMETER ApplicationId 43 | ApplicationId - the application setup for mailbox migration. 44 | 45 | .PARAMETER ApplicationKeyVaultUrl 46 | ApplicationKeyVaultUrl - the keyvault url for application secret. 47 | 48 | .EXAMPLE - TargetTenant 49 | $report = VerifySetup.ps1 -PartnerTenantId -ApplicationId -ApplicationKeyVaultUrl -PartnerTenantDomain -Verbose 50 | 51 | .EXAMPLE - TargetTenant 52 | $report = VerifySetup.ps1 -PartnerTenantId -ApplicationId -ApplicationKeyVaultUrl -PartnerTenantDomain -SubscriptionId -Verbose 53 | 54 | .EXAMPLE - SourceTenant 55 | $report = VerifySetup.ps1 -PartnerTenantId -ApplicationId 56 | #> 57 | 58 | param 59 | ( 60 | [Parameter(Mandatory = $true, HelpMessage='Partner tenant id', ParameterSetName = 'VerifyTarget')] 61 | [Parameter(Mandatory = $true, HelpMessage='Partner tenant id', ParameterSetName = 'VerifySource')] 62 | [ValidateScript({ -not [string]::IsNullOrWhiteSpace($_) })] 63 | [string]$PartnerTenantId, 64 | 65 | 66 | 67 | [Parameter(Mandatory = $true, HelpMessage='AAD ApplicationId', ParameterSetName = 'VerifyTarget')] 68 | [Parameter(Mandatory = $true, HelpMessage='Partner tenant id', ParameterSetName = 'VerifySource')] 69 | [ValidateScript({ -not [string]::IsNullOrWhiteSpace($_) })] 70 | [string]$ApplicationId, 71 | 72 | 73 | 74 | [Parameter(Mandatory = $true, HelpMessage='PartnerTenantDomain', ParameterSetName = 'VerifyTarget')] 75 | [ValidateScript({ -not [string]::IsNullOrWhiteSpace($_) })] 76 | [string]$PartnerTenantDomain, 77 | 78 | 79 | 80 | [Parameter(Mandatory = $true, HelpMessage='App secret key vault url', ParameterSetName = 'VerifyTarget')] 81 | [ValidateScript({ 82 | if ($_ -cmatch "^https://[a-zA-Z_0-9\-]+\.vault\.azure.net(:443){0,1}/certificates/[a-zA-Z_0-9\-]+/[a-zA-Z_0-9]+$") 83 | { 84 | $true 85 | } 86 | elseif ($_ -cmatch "^https://[a-zA-Z_0-9]+\.vault\.azure.us(:443){0,1}/certificates/[a-zA-Z_0-9]+/[a-zA-Z_0-9]+$") { 87 | $true 88 | } 89 | else 90 | { 91 | throw [System.Management.Automation.ValidationMetadataException] "Please make sure key vault url matches format specified here: https://docs.microsoft.com/en-us/azure/key-vault/general/about-keys-secrets-certificates#vault-name-and-object-name" 92 | } 93 | })] 94 | [string]$ApplicationKeyVaultUrl, 95 | 96 | [Parameter(Mandatory = $false, HelpMessage='SubscriptionId for key vault', ParameterSetName = 'VerifyTarget')] 97 | [Parameter(Mandatory = $false, HelpMessage='SubscriptionId for key vault', ParameterSetName = 'VerifySource')] 98 | [ValidateScript({-not [string]::IsNullOrWhiteSpace($_)})] 99 | [string]$SubscriptionId 100 | ) 101 | 102 | $ErrorActionPreference = 'Stop' 103 | 104 | $MS_GRAPH_APP_ID = "00000003-0000-0000-c000-000000000000" 105 | $MS_GRAPH_APP_ROLE = "User.Invite.All" 106 | $EXO_APP_ID = "00000002-0000-0ff1-ce00-000000000000" 107 | $EXO_APP_ROLE = "Mailbox.Migration" 108 | 109 | function Main() { 110 | $report = @{} 111 | Check-ExchangeOnlinePowershellConnection 112 | $isTargetTenant = $PSCmdlet.ParameterSetName -eq 'VerifyTarget' 113 | $azureADAccount = Connect-AzureAD 114 | Write-Verbose "Connected to AzureAD - $($azureADAccount | Out-String)" 115 | $azAccount = Connect-AzAccount -Tenant $azureADAccount.Tenant.ToString() 116 | Write-Verbose "Connected to Az Account - $($azAccount | Out-String)" 117 | $currentTenantId = $azureADAccount.TenantId.Guid 118 | if($isTargetTenant -eq $true) 119 | { 120 | Check-AzSubscription 121 | } 122 | Write-Verbose "Verifying Application; AppId: [$ApplicationId] Current tenant: [$currentTenantId] Partner tenant: [$PartnerTenantId] IsTargetTenant: [$isTargetTenant]" 123 | $errors, $warnings = Verify-Application $ApplicationId $currentTenantId $PartnerTenantId $isTargetTenant 124 | $report["Application"] = @{ "Errors" = $errors; "Warnings" = $warnings } 125 | Write-Host "`r`n" 126 | Print-Result "Verifying AAD Application" $errors $warnings 127 | if ($isTargetTenant -eq $true) { 128 | Write-Verbose "Verifying KeyVault; AppId: [$ApplicationId] ApplicationKeyVaultUrl: [$ApplicationKeyVaultUrl]" 129 | $errors = Verify-KeyVault $ApplicationId $ApplicationKeyVaultUrl 130 | Print-Result "Verifying KeyVault" $errors 131 | $report["KeyVault"] = @{ "Errors" = $errors } 132 | } 133 | 134 | Write-Verbose "Verifying OrganizationRelationship; AppId: [$ApplicationId] Partner tenant: [$PartnerTenantId] IsTargetTenant: [$isTargetTenant]" 135 | $errors = Verify-OrganizationRelationship $PartnerTenantId $ApplicationId $isTargetTenant 136 | Print-Result "Verifying OrganizationRelationship" $errors 137 | $report["OrganizationRelationship"] = @{ "Errors" = $errors } 138 | 139 | if ($isTargetTenant -eq $true) { 140 | Write-Verbose "Verifying MigrationEndpoint; AppId: [$ApplicationId] Partner tenant: [$PartnerTenantDomain] ApplicationKeyVaultUrl: [$ApplicationKeyVaultUrl]" 141 | $errors = Verify-MigrationEndpoint $PartnerTenantDomain $ApplicationId $ApplicationKeyVaultUrl 142 | Print-Result "Verifying MigrationEndpoint" $errors 143 | $report["MigrationEndpoint"] = @{ "Errors" = $errors } 144 | } 145 | 146 | Write-Verbose ($report | ConvertTo-Json) 147 | $report 148 | } 149 | 150 | function Print-Result([string]$opName, $errors, $warnings) { 151 | Write-Host "[$opName].............." -NoNewLine 152 | if (!$errors -and !$warnings) { 153 | Write-Host "[Passed]" -NoNewLine -ForeGroundColor Green 154 | Write-Host "`r`n" 155 | return 156 | } 157 | 158 | if ($errors) { 159 | Write-Host "[Failed]" -ForeGroundColor Red 160 | Write-Host ($errors -join "`n") -ForeGroundColor Red 161 | } 162 | 163 | if ($warnings) { 164 | if (!$errors) { 165 | Write-Host "[Warnings]" -ForeGroundColor Yellow 166 | } 167 | 168 | Write-Host ($warnings -join "`n") -ForeGroundColor Yellow 169 | } 170 | 171 | Write-Host "`r`n`r`n" 172 | } 173 | 174 | function Verify-Application ([string]$appId, [string]$currentTenantId, [string]$partnerTenantId, [bool]$isTargetTenant) { 175 | $warnings = @() 176 | $errors = @() 177 | $spn = Get-AzureADServicePrincipal -Filter "AppId eq '$appId'" 178 | if (!$spn) { 179 | $errors += "App [$appId] is not registered in [$currentTenantId] tenant" 180 | return $errors 181 | } 182 | 183 | if ($isTargetTenant -eq $true) { 184 | if ($spn.AppOwnerTenantId -ne $currentTenantId) { 185 | $error += "App [$appId] was found in the [$currentTenantId] tenant but is not owned by it. Since this is target tenant, the app used for migration must be owned by target tenant." 186 | } 187 | } elseif ($spn.AppOwnerTenantId -ne $partnerTenantId) { 188 | $error += "App [$appId] was found in the [$currentTenantId] tenant but is not owned by $partnerTenantId. Please use an application owned by target tenant for mailbox migrations." 189 | } 190 | 191 | # Check MSGraph and EXO has incoming app roles assignment from the tenant friending app 192 | # 1. collect spns of MSGraph and EXO applications 193 | $msGraphSpn = Get-AzureADServicePrincipal -Filter "AppId eq '$MS_GRAPH_APP_ID'" 194 | $exoSpn = Get-AzureADServicePrincipal -Filter "AppId eq '$EXO_APP_ID'" 195 | if (!$msGraphSpn -or !$exoSpn) { 196 | $errors += "Internal Error: SPNs of MSGraph or EXO not found." 197 | return $errors 198 | } 199 | 200 | # Get the permission objects from Exo and MsGraph 201 | $exoMailboxMigrationPermissions = $exoSpn.AppRoles | ? { $_.Value -eq $EXO_APP_ROLE } 202 | $msGraphDirectoryPermissions = $msGraphSpn.AppRoles | ? { $_.Value -eq $MS_GRAPH_APP_ROLE } 203 | 204 | # Get the permission objects of the permissions assigned to the application 205 | $exoPermissionForApp = Get-AzureADServiceAppRoleAssignment -ObjectId $exoSpn.ObjectId -All $true | ? { $_.PrincipalId -eq $spn.ObjectId } 206 | $msGraphPermissionForApp = Get-AzureADServiceAppRoleAssignment -ObjectId $msGraphSpn.ObjectId -All $true | ? { $_.PrincipalId -eq $spn.ObjectId } 207 | 208 | if (!$exoPermissionForApp -or ($exoPermissionForApp.Id -ne $exoMailboxMigrationPermissions.Id)) { 209 | $errors += "App [$appId] does not have [$EXO_APP_ROLE] permission on Exchange setup or the permission is not consented by an Administrator" 210 | } 211 | 212 | if (!$msGraphPermissionForApp -or ($msGraphPermissionForApp.Id -ne $msGraphDirectoryPermissions.Id)) { 213 | $warnings += "App [$appId] does not have [$MS_GRAPH_APP_ROLE] permission on MSGraph setup or the permission is not consented by an Administrator" 214 | } 215 | 216 | $errors, $warnings 217 | } 218 | 219 | function Verify-KeyVault([string]$appId, [string]$appKvUrl) { 220 | $errors = @() 221 | try { 222 | $uri = [System.Uri]::new($appKvUrl) 223 | $kvName = $uri.Host.Split(".")[0] 224 | $kv = Get-AzKeyVault -VaultName $kvName 225 | if (!$kv) { 226 | $errors += "KeyVault: $kvName not found" 227 | return $errors 228 | } 229 | 230 | $exoSpn = Get-AzureADServicePrincipal -Filter "AppId eq '$EXO_APP_ID'" 231 | $exoAccessPolicy = $kv.AccessPolicies | ? { $_.ObjectId -eq $exoSpn.ObjectId } 232 | if (!$exoAccessPolicy) { 233 | $errors += "Exchange does not have any permissions on the KeyVault [$kvName]" 234 | return $errors 235 | } 236 | 237 | $certStorePermissions = $exoAccessPolicy.PermissionsToCertificates.ToLower() 238 | $secretStorePermissions = $exoAccessPolicy.PermissionsToSecrets.ToLower() 239 | "get", "list" | % { if (!$certStorePermissions.Contains($_)) {$errors += "Exchange does not have [$_] permission on KeyVault [$kvName]'s Certificate container"}} 240 | "get", "list" | % { if (!$secretStorePermissions.Contains($_)) {$errors += "Exchange does not have [$_] permission on KeyVault [$kvName]'s Secrets container"}} 241 | } catch { 242 | $errors += $_.Message 243 | } 244 | 245 | $errors 246 | } 247 | 248 | function Verify-OrganizationRelationship([string]$partnerTenantId, [string]$appId, [bool]$isTargetTenant) { 249 | $errors = @() 250 | $orgRel = Get-OrganizationRelationship | ? { $_.DomainNames -contains $partnerTenantId } 251 | if (!$orgRel) { 252 | $errors += "Organization relationship does not exist with [$partnerTenantId]" 253 | return $errors 254 | } 255 | 256 | if ($isTargetTenant -eq $true) { 257 | if (!$orgRel.MailboxMoveEnabled) { 258 | $errors += "MailboxMove is not enabled in Organization relationship with [$partnerTenantId]" 259 | } 260 | 261 | if ($orgRel.MailboxMoveCapability -ne 'Inbound') { 262 | $errors += "MailboxMoveCapability is invalid in Organization relationship with [$partnerTenantId]. It should be [Inbound] found [$($orgRel.MailboxMoveCapability)]" 263 | } 264 | 265 | if ($errors) { 266 | return $errors 267 | } 268 | } else { 269 | if (!$orgRel.MailboxMoveEnabled) { 270 | $errors += "MailboxMove is not enabled in Organization relationship with [$partnerTenantId]" 271 | } 272 | 273 | if ($orgRel.MailboxMoveCapability -ne 'RemoteOutbound') { 274 | $errors += "MailboxMoveCapability is invalid in Organization relationship with [$partnerTenantId]. It should be [RemoteOutbound] found [$($orgRel.MailboxMoveCapability)]" 275 | } 276 | 277 | if ($orgRel.OAuthApplicationId -ne $appId) { 278 | $errors += "Mailbox Migration ApplicationId is not whitelisted in the Organization Relationship with [$partnerTenantId]. Expected [$appId] found [$($orgRel.ApplicationId)]" 279 | } 280 | 281 | if (!$orgRel.MailboxMovePublishedScopes) { 282 | $errors += "Source tenant needs to specify MailboxMovePublishedScopes to allow migration" 283 | } 284 | } 285 | 286 | return $errors 287 | } 288 | 289 | function Verify-MigrationEndpoint([string]$partnerTenantDomain, [string]$appId, [string]$appKvUrl) { 290 | $errors = @() 291 | $migEp = Get-MigrationEndpoint | ? { $_.ApplicationId -eq $appId } 292 | if (!$migEp) { 293 | $errors += "Migration Endpoint containing [$appId] not found." 294 | return $errors 295 | } 296 | 297 | if ($migEp.RemoteTenant -ne $partnerTenantDomain) { 298 | $errors += "RemoteTenant does not match in Migration Endpoint. Expected [$partnerTenantDomain] found [$($migEp.RemoteTenant)]" 299 | } 300 | 301 | if ($migEp.ApplicationId -ne $appId) { 302 | $errors += "ApplicationId does not match in Migration Endpoint. Expected [$appId] found [$($migEp.ApplicationId)]" 303 | } 304 | 305 | if ($migEp.AppSecretKeyVaultUrl -ne $appKvUrl) { 306 | $errors += "AppSecretKeyVaultUrl does not match in Migration Endpoint. Expected [$appKvUrl] found [$($migEp.AppSecretKeyVaultUrl)]" 307 | } 308 | 309 | if (!$migEp.IsRemote) { 310 | $errors += "IsRemote does not match in Migration Endpoint. Expected [true] found [$($migEp.IsRemote)]" 311 | } 312 | 313 | return $errors 314 | } 315 | 316 | function Check-ExchangeOnlinePowershellConnection { 317 | if ($Null -eq (Get-Command New-OrganizationRelationship -ErrorAction SilentlyContinue)) { 318 | Write-Error "Please connect to the Exchange Online Management module or Exchange Online through basic authentication before running this script!"; 319 | } 320 | } 321 | 322 | function Check-AzSubscription { 323 | if (!$SubscriptionId) 324 | { 325 | $subscriptions = Get-AzSubscription 326 | 327 | if ($subscriptions.Count -gt 1) { 328 | Write-Error "Multipule Azure subscriptions were found for this tenant. Please rerun the script and use the -SubscriptionId parameter with the correct subscription" 329 | } 330 | elseif (!$subscriptions) { 331 | Write-Error "No valid Azure subscriptions were found for this tenant." 332 | } 333 | 334 | Set-AzContext -Subscription $subscriptions.SubscriptionId 335 | } 336 | elseif ($SubscriptionID) 337 | { 338 | Write-Verbose "SubscriptionId - $SubscriptionId was provided." 339 | Set-AzContext -Subscription $SubscriptionId 340 | } 341 | } 342 | 343 | Main -------------------------------------------------------------------------------- /v1 Content/Cross-tenant mailbox migration (preview).pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/cross-tenant/1a75ccd1a367b7ad0a116607813f80751c1e1ffd/v1 Content/Cross-tenant mailbox migration (preview).pdf -------------------------------------------------------------------------------- /v1 Content/cross-tenant-mailbox-migration.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Cross-tenant mailbox migration 3 | description: How to move mailboxes between Microsoft 365 or Office 365 tenants. 4 | ms.author: kvice 5 | author: kelleyvice-msft 6 | manager: Laurawi 7 | ms.prod: microsoft-365-enterprise 8 | ms.topic: article 9 | f1.keywords: 10 | - NOCSH 11 | ms.date: 09/21/2020 12 | ms.reviewer: georgiah 13 | ms.custom: 14 | - it-pro 15 | - admindeeplinkMAC 16 | ms.collection: 17 | - M365-subscription-management 18 | --- 19 | 20 | # Cross-tenant mailbox migration (preview) 21 | 22 | Previously, when an Exchange Online tenant needed to move mailboxes to another tenant in the same Exchange Online service, they would have to completely offboard them to on-premises and then onboard them to a new tenant. With the new cross-tenant mailbox migration feature, tenant administrators in both source and target tenants can move mailboxes between the tenants with minimal infrastructure dependencies in their on-premises systems. This removes the need to off-board and onboard mailboxes. 23 | 24 | Commonly, during mergers or divestitures, you need the ability to move users and content into a new tenant. When the target tenant administrator executes the move, it’s called a Pull move, similar to on-premises to cloud onboarding migrations. 25 | 26 | Cross-tenant Exchange mailbox moves are fully self-serviced by tenant administrators, using well known interfaces that can be scripted into the larger workflows needed to transition users to their new organization. Administrators can use the `New-MigrationBatch` cmdlet, available through the Move Mailboxes management role, to execute cross-tenant moves. The move process includes tenant authorization checks during mailbox synchronization and finalization. 27 | 28 | Users migrating must be present in the target tenant Exchange Online system as MailUsers, marked with specific attributes to enable the cross-tenant moves. The system will fail moves for users that are not properly set up in the target tenant. 29 | 30 | When the moves are complete, the source system mailbox is converted to MailUser and the targetAddress (shown as ExternalEmailAddress in Exchange) is stamped with the routing address to the destination tenant. This process leaves the legacy MailUser in the source tenant, and allows for a period of co-existence and mail routing. When business processes allow, the source tenant may remove the source MailUser or convert them to a mail contact. 31 | 32 | Cross-tenant Exchange mailbox migrations are supported for tenants in hybrid or cloud only, or any combination of the two. 33 | 34 | This article describes the process for cross-tenant mailbox moves and provides guidance on how to prepare source and target tenants for the content move. 35 | 36 | ## Preparing source and target tenants 37 | 38 | The Cross-tenant Exchange mailbox migration feature requires authorization and scoping for cross-tenant migrations. Using the Azure Enterprise application and Key Vault storage solutions, tenant admins are now empowered to manage both authorization and scoping of Exchange Online mailbox migrations from one tenant to another. Cross-tenant mailbox moves supports an invitation and consent model to establish an Azure Active Directory (Azure AD) application used for authentication between a tenant pair. Additional components such as an organization relationship and a migration endpoint are also required. 39 | 40 | This section does not include the specific steps required to prepare the MailUser user objects in the target directory, nor does it include the sample command to submit a migration batch. Please see [Prepare target user objects for migration](#prepare-target-user-objects-for-migration) for this information. 41 | 42 | ## Prerequisites 43 | 44 | The cross-tenant mailbox move feature requires [Azure Key Vault](/azure/key-vault/basic-concepts) to establish a tenant pair-specific Azure application to securely store and access the certificate/secret used to authenticate and authorize mailbox migration from one tenant to the other, removing any requirements to share certificates/secrets between tenants. 45 | 46 | Before starting, be sure you have the necessary permissions to run the deployment scripts in order to configure Azure Key Vault, Move Mailbox application, EXO Migration Endpoint, and the EXO Organization Relationship. Typically, **Azure AD DC admin**, or **Global admin** has permission to perform all configuration steps. 47 | 48 | Additionally, mail-enabled security groups in the source tenant are required prior to running setup. These groups are used to scope the list of mailboxes that can move from source (or sometimes referred to as resource) tenant to the target tenant. This allows the source tenant admin to restrict or scope the specific set of mailboxes that need to be moved, preventing unintended users from being migrated. Nested groups are not supported. 49 | 50 | You will also need to communicate with your trusted partner company (with whom you will be moving mailboxes) to obtain their Microsoft 365 tenant ID. This tenant ID is used in the Organization Relationship `DomainName` field. 51 | 52 | To obtain the tenant ID of a subscription, sign in to the Microsoft 365 admin center and go to [https://aad.portal.azure.com/#blade/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/Properties](https://aad.portal.azure.com/#blade/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/Properties). Click the copy icon for the Tenant ID property to copy it to the clipboard. 53 | 54 | Here is how the process works. 55 | 56 | :::image type="content" source="./media/tenant-to-tenant-mailbox-move/prepare-tenants-flow.png" alt-text="Tenant preparation for mailbox migration."::: 57 | 58 | [See a larger version of this image](https://github.com/MicrosoftDocs/microsoft-365-docs/raw/public/microsoft-365/media/tenant-to-tenant-mailbox-move/prepare-tenants-flow.png). 59 | 60 | 63 | 64 | ### Prepare tenants 65 | 66 | At a high level, the following configuration actions take place when executing the setup scripts. 67 | 68 | Prepare the target tenant: 69 | 70 | 1. If an existing Azure Resource Group is not provided, a new one is created (SCRIPT). 71 | 2. If an existing Key Vault is not provided, a new one is created (SCRIPT). 72 | 3. A new Access Policy is created for the Office 365 Exchange Online Mailbox Migration application (SCRIPT). 73 | 4. A new certificate is created (or existing one, if specified) to hold the secret to the Migration application (SCRIPT). 74 | 5. A new Azure AD application is created (SCRIPT). 75 | 6. The certificate/secret is uploaded to the migration application (SCRIPT). 76 | 7. Mailbox migration permissions are assigned to the application (SCRIPT). 77 | 8. The deployment script pauses until target admin consents to their own application (SCRIPT). 78 | 9. The target tenant admin consents to the permissions given to the application (MANUAL). 79 | 10. An organization relationship is created to the target tenant (SCRIPT). 80 | 11. A migration endpoint is created to pull mailboxes to the target tenant (SCRIPT). 81 | 82 | Prepare the source tenant: 83 | 84 | 1. The source tenant admin accepts consent to Mailbox Migration application invitation from the Target tenant (MANUAL). 85 | 2. The source tenant admin creates a mail-enabled security group in their tenant to contain the list of mailboxes allowed to be moved by the migration application (MANUAL). 86 | 3. An organization relationship is created to the target tenant specifying the mailbox migration application should be used for OAuth verification to accept the move request (SCRIPT). 87 | 88 | #### Step-by-step instructions for the target tenant admin 89 | 90 | 1. Download the SetupCrossTenantRelationshipForTargetTenant.ps1 script for the target tenant setup from the [GitHub repository](https://github.com/microsoft/cross-tenant/releases/tag/Preview). 91 | 2. Save the script (SetupCrossTenantRelationshipForTargetTenant.ps1) to the computer from which you will be executing the script. 92 | 3. Create a Remote PowerShell connection to the Exchange Online target tenant. Again, make sure you have the necessary permissions to run the deployment scripts in order to configure the Azure Key Vault storage and certificate, Move Mailbox application, EXO Migration Endpoint, and the EXO Organization Relationship. 93 | 4. Change the file folder directory to the script location or verify the script is currently saved to the location currently in your Remote PowerShell session. 94 | 5. Run the script with the following parameters and values. 95 | 96 | |Parameter|Value|Required or Optional 97 | |---|---|---| 98 | |-TargetTenantDomain|Target tenant domain, such as fabrikam\.onmicrosoft.com.|Required| 99 | |-ResourceTenantDomain|Source tenant domain, such as contoso\.onmicrosoft.com.|Required| 100 | |-ResourceTenantAdminEmail|Source tenant admin’s email address. This is the source tenant admin who will be consenting to the use of the mailbox migration application sent from the target admin. This is the admin who will receive the email invite for the application.|Required| 101 | |-ResourceTenantId|Source tenant organization ID (GUID).|Required| 102 | |-SubscriptionId|The Azure subscription to use for creating resources.|Required| 103 | |-ResourceGroup|Azure resource group name that contains or will contain the Key Vault.|Required| 104 | |-KeyVaultName|Azure Key Vault instance that will store your mailbox migration application certificate/secret.|Required| 105 | |-CertificateName|Certificate name when generating or searching for certificate in key vault.|Required| 106 | |-CertificateSubject|Azure Key Vault certificate subject name, such as CN=contoso_fabrikam.|Required| 107 | |-AzureResourceLocation|The location of the Azure resource group and key vault.|Required| 108 | |-ExistingApplicationId|Mail migration application to use if one was already created.|Optional| 109 | |-AzureAppPermissions|The permissions required to be given to the mailbox migration application, such as Exchange or MSGraph (Exchange for moving mailboxes, MSGraph for using this application to send a consent link invitation to resource tenant).|Required| 110 | |-UseAppAndCertGeneratedForSendingInvitation|Parameter for using the application created for migration to be used for sending consent link invitation to source tenant admin. If not present this will prompt for the target admin’s credentials to connect to Azure invitation manager and send the invitation as target admin.|Optional| 111 | |-KeyVaultAuditStorageAccountName|The storage account where Key Vault’s audit logs would be stored.|Optional| 112 | |-KeyVaultAuditStorageResourceGroup|The resource group that contains the storage account for storing Key Vault audit logs.|Optional| 113 | |||| 114 | 115 | > [!NOTE] 116 | > Please ensure you have installed the Azure AD PowerShell module prior to running the scripts. Please refer to [here](/powershell/azure/install-az-ps) for installation steps 117 | 118 | 6. The script will pause and ask you to accept or consent to the Exchange mailbox migration application that was created during this process. Here is an example. 119 | 120 | ```powershell 121 | PS C:\PowerShell\> # Note: the below User.Invite.All permission is optional, and will only be used to retrieve access token to send invitation email to source tenant 122 | PS C:\PowerShell\> .\SetupCrossTenantRelationshipForTargetTenant.ps1 -ResourceTenantDomain contoso.onmicrosoft.com -ResourceTenantAdminEmail admin@contoso.onmicrosoft.com -TargetTenantDomain fabrikam.onmicrosoft.com -ResourceTenantId ksagjid39-ede2-4d2c-98ae-874709325b00 -SubscriptionId e4ssd05d-a327-49ss-849a-sd0932439023 -ResourceGroup "Cross-TenantMoves" -KeyVaultName "Cross-TenantMovesVault" -CertificateName "Contoso-Fabrikam-cert" -CertificateSubject "CN=Contoso_Fabrikam" -AzureResourceLocation "Brazil Southeast" -AzureAppPermissions Exchange, MSGraph -UseAppAndCertGeneratedForSendingInvitation -KeyVaultAuditStorageAccountName "t2tstorageaccount" -KeyVaultAuditStorageResourceGroup "Demo" 123 | 124 | cmdlet Get-Credential at command pipeline position 1 125 | Supply values for the following parameters: 126 | Credential 127 | Setting up key vault in the fabrikam.onmicrosoft.com tenant 128 | 129 | Name Account SubscriptionName Environment TenantId 130 | ---- ------- ---------------- ----------- -------- 131 | Pay-As-You-Go (ewe23423-a3327-34232-343... Admin@fabrikam... Pay-As-You-Go AzureCloud dsad938432-dd8e-s9034-bf9a-83984293n43 132 | Auditing setup successfully for Cross-TenantMovesVault 133 | Exchange application given access to KeyVault Cross-TenantMovesVault 134 | Application fabrikam_Friends_contoso_2520 created successfully in fabrikam.onmicrosoft.com tenant with following permissions. MSGraph - User.Invite.All. Exchange - Mailbox.Migration 135 | Admin consent URI for fabrikam.onmicrosoft.com tenant admin is - 136 | https://login.microsoftonline.com/fabrikam.onmicrosoft.com/adminconsent?client_id=6fea6ere-0dwe-404d-ad35-c71a15cers5c&redirect_uri=https://office.com 137 | Admin consent URI for contoso.onmicrosoft.com tenant admin is - 138 | https://login.microsoftonline.com/contoso.onmicrosoft.com/adminconsent?client_id=6fea6ssd-0753-404d-wer5-c71a154d675c&redirect_uri=https://office.com 139 | Application details to be registered in organization relationship: ApplicationId: [ 6fes8en4-sjo3-406d-ad35-sldkfjiew993 ]. KeyVault secret Id: [ https://cross-tenantmovesvault.vault.azure.net:443/certificates/Contoso-Fabrikam-cert/ksdfj843nt8476h84c288c5a3fb8ec5fdb08 ]. These values are available in variables $AppId and $CertificateId respectively 140 | Please consent to the application for fabrikam.onmicrosoft.com before sending invitation to admin@contoso.onmicrosoft.com: 141 | ``` 142 | 143 | 7. A URL will be displayed in the Remote PowerShell session. Copy the link provided for your tenant consent and paste it into a Web browser. 144 | 145 | 8. Sign in with your **Azure AD DC admin**, or **Global admin** credentials. When the following screen is presented, select **Accept**. 146 | 147 | :::image type="content" source="./media/tenant-to-tenant-mailbox-move/permissions-requested-dialog.png" alt-text="Accept permissions dialog box."::: 148 | 149 | 9. Switch back to the Remote PowerShell session and hit Enter to proceed. 150 | 151 | 10. The script will configure the remaining setup objects. Here is an example. 152 | 153 | ```powershell 154 | Successfully sent invitation to admin@contoso.onmicrosoft.com 155 | Setting up exchange components on target tenant: fabrikam.onmicrosoft.com 156 | MigrationEndpoint created in fabrikam.onmicrosoft.com for target contoso.onmicrosoft.com 157 | Exchange setup complete. Migration endpoint details are available in $MigrationEndpoint variable 158 | ``` 159 | 160 | The target admin setup is now complete! 161 | 162 | #### Step-by-step instructions for the source tenant admin 163 | 164 | 1. Sign in with your Global Admin credentials. Sign in to your mailbox as the -ResourceTenantAdminEmail specified by the target admin during their setup. Find the email invitation from the target tenant, and then select the **Get Started** button. 165 | 166 | :::image type="content" source="./media/tenant-to-tenant-mailbox-move/invited-by-target-tenant.png" alt-text="You've been invited dialog box"::: 167 | 168 | 2. Select **Accept** to accept the invitation. 169 | 170 | :::image type="content" source="./media/tenant-to-tenant-mailbox-move/permissions-requested-accept.png" alt-text="Dialog box to accept permissions."::: 171 | 172 | > [!NOTE] 173 | > If you do not get this email or cannot find it, the target tenant admin was provided a direct URL that can be given to you to accept the invitation. The URL should in the in the transcript of the target tenant admin's Remote PowerShell session. 174 | 175 | 3. In either the Microsoft 365 admin center or a Remote PowerShell session, create one or more mail-enabled security groups to control the list of mailboxes allowed by the target tenant to pull (move) from the source tenant to the target tenant. You do not need to populate this group in advance, but at least one group must be provided to run the setup steps (script). Nest groups are not supported. 176 | 177 | 4. Download the SetupCrossTenantRelationshipForResourceTenant.ps1 script for the source tenant setup from the GitHub repository here: [https://github.com/microsoft/cross-tenant/releases/tag/Preview](https://github.com/microsoft/cross-tenant/releases/tag/Preview). 178 | 179 | 5. Create a Remote PowerShell connection to the source tenant with your Exchange Administrator permissions. **Azure AD DC admin**, or **Global admin** permissions are not required to configure the source tenant, only the target tenant because of the Azure application creation process. 180 | 181 | 6. Change directory to the script location or verify that the script is currently saved to the location currently in your Remote PowerShell session. 182 | 183 | 7. Run the script with the following required parameters and values. 184 | 185 | |Parameter|Value| 186 | |---|---| 187 | |-SourceMailboxMovePublishedScopes|Mail-enabled security group created by source tenant for the identities/mailboxes that are in scope for migration.| 188 | |-ResourceTenantDomain|Source tenant domain name, such as contoso\.onmicrosoft.com.| 189 | |-ApplicationId|Azure application ID (GUID) of the application used for migration. Application ID available via your Azure portal (Azure AD, Enterprise Applications, app name, application ID) or included in your invitation email.| 190 | |-TargetTenantDomain|Target tenant domain name, such as fabrikam\.onmicrosoft.com.| 191 | |-TargetTenantId|Tenant ID of the target tenant. For example, the Azure AD tenant ID of contoso\.onmicrosoft.com tenant.| 192 | ||| 193 | 194 | Here is an example. 195 | 196 | ```powershell 197 | SetupCrossTenantRelationshipForResourceTenant.ps1 -SourceMailboxMovePublishedScopes "MigScope","MyGroup" -ResourceTenantDomain contoso.onmicrosoft.com -TargetTenantDomain fabrikam.onmicrosoft.com -ApplicationId sdf5e87sa-0753-dd88-ad35-c71a15cs8e44c -TargetTenantId 4sdkfo933-3904-sd93-bf9a-sdi39402834 198 | Exchange setup complete. 199 | ``` 200 | 201 | The source admin setup is now complete! 202 | 203 | ### Verify setup 204 | 205 | Verify that the organization relationships in both source and target tenants and migration endpoint in the target were created successfully. 206 | 207 | #### Target tenant 208 | 209 | ##### Organization relationship 210 | 211 | Verify that the organization relationship object was created and configured with this command. 212 | 213 | ```powershell 214 | Get-OrganizationRelationship | fl name, DomainNames, MailboxMoveEnabled, MailboxMoveCapability 215 | ``` 216 | Here is an example: 217 | 218 | ```powershell 219 | PS C:\PowerShell\> Get-OrganizationRelationship fabrikam_contoso_1178 | fl name, DomainNames, MailboxMoveEnabled, MailboxMoveCapability 220 | 221 | Name : fabrikam_contoso_1123 222 | DomainNames : {sd0933me9f-9304-s903-s093-s093mfi903m4} 223 | MailboxMoveEnabled : True 224 | MailboxMoveCapability : Inbound 225 | ``` 226 | 227 | ##### Migration endpoint 228 | 229 | Verify that the migration endpoint object was created and configured with this command. 230 | 231 | ```powershell 232 | Get-MigrationEndpoint " | fl Identity, RemoteTenant, ApplicationId, AppSecretKeyVaultUrl 233 | ``` 234 | 235 | Here is an example. 236 | 237 | ```powershell 238 | PS C:\PowerShell\> Get-MigrationEndpoint fabrikam_contoso_1123 | fl Identity, RemoteTenant, ApplicationId, AppSecretKeyVaultUrl 239 | 240 | 241 | Identity : fabrikam_contoso_1123 242 | RemoteTenant : contoso.onmicrosoft.com 243 | ApplicationId : s93mf93-das9-dq24-dq234-dada9033904m 244 | AppSecretKeyVaultUrl : https://cross-tenantmyvaultformoves.vault.azure.net:443/certificates/Contoso-Fabrikam-cert/ae79348mx94384c288c5a3dfsioepw308 245 | ``` 246 | 247 | #### Source tenant 248 | 249 | ##### Organization relationship 250 | 251 | Verify that the organization relationship object was created and configured with this command. 252 | 253 | ```powershell 254 | Get-OrganizationRelationship | fl name, MailboxMoveEnabled, MailboxMoveCapability, MailboxMovePublishedScopes, OAuthApplicationId 255 | ``` 256 | 257 | Here is an example. 258 | 259 | ```powershell 260 | PS C:\PowerShell\> Get-OrganizationRelationship | fl name, MailboxMoveEnabled, MailboxMoveCapability, MailboxMovePublishedScopes, OAuthApplicationId 261 | 262 | 263 | Name : fabrikam_contoso_001 264 | MailboxMoveEnabled : True 265 | MailboxMoveCapability : RemoteOutbound 266 | MailboxMovePublishedScopes : {MigScope} 267 | OAuthApplicationId : sd9890342-3243-3242-fe3w2-fsdade93m0 268 | ``` 269 | 270 | #### Verify Setup Script 271 | 272 | If you receive any errors during the configuration of the source or target tenants, you can run the VerifySetup.ps1 script located [on GitHub](https://github.com/microsoft/cross-tenant/releases/tag/Preview) and review the output. 273 | 274 | Here's an example of running VerifySetup.ps1 on the target tenant: 275 | 276 | ```powershell 277 | VerifySetup.ps1 -PartnerTenantId -ApplicationId -ApplicationKeyVaultUrl -PartnerTenantDomain -Verbose 278 | ``` 279 | 280 | Here's an example of VerifySetup.ps1 on the source tenant: 281 | 282 | ```powershell 283 | VerifySetup.ps1 -PartnerTenantId -ApplicationId 284 | ``` 285 | 286 | ### Move mailboxes back to the original source 287 | 288 | If a mailbox move back to the original source tenant is required, the same set of steps and scripts will need to be run in both new source and new target tenants. The existing Organization Relationship object will be updated or appended, not recreated. 289 | 290 | ## Prepare target user objects for migration 291 | 292 | Users migrating must be present in the target tenant and Exchange Online system (as MailUsers) marked with specific attributes to enable the cross-tenant moves. The system will fail moves for users that are not properly set up in the target tenant. The following section details the MailUser object requirements for the target tenant. 293 | 294 | ### Prerequisites 295 | 296 | You must ensure the following objects and attributes are set in the target organization. 297 | 298 | 1. For any mailbox moving from a source organization, you must provision a MailUser object in the Target organization: 299 | 300 | - The Target MailUser must have these attributes from the source mailbox or assigned with the new User object: 301 | - ExchangeGUID (direct flow from source to target) – The mailbox GUID must match. The move process will not proceed if this is not present on target object. 302 | - ArchiveGUID (direct flow from source to target) – The archive GUID must match. The move process will not proceed if this is not present on the target object. (This is only required if the source mailbox is Archive enabled). 303 | - LegacyExchangeDN (flow as proxyAddress, “x500:\”) – The LegacyExchangeDN must be present on target MailUser as x500: proxyAddress. In addition, you also need to copy all x500 addresses from the source mailbox to the target mail user. The move processes will not proceed if these are not present on the target object. 304 | - UserPrincipalName – UPN will align to the user’s NEW identity or target company (for example, user@northwindtraders.onmicrosoft.com). 305 | - Primary SMTPAddress – Primary SMTP address will align to the user’s NEW company (for example, user@northwind.com). 306 | - TargetAddress/ExternalEmailAddress – MailUser will reference the user’s current mailbox hosted in source tenant (for example user@contoso.onmicrosoft.com). When assigning this value, verify that you have/are also assigning PrimarySMTPAddress or this value will set the PrimarySMTPAddress which will cause move failures. 307 | - You cannot add legacy smtp proxy addresses from source mailbox to target MailUser. For example, you cannot maintain contoso.com on the MEU in fabrikam.onmicrosoft.com tenant objects). Domains are associated with one Azure AD or Exchange Online tenant only. 308 | 309 | Example **target** MailUser object: 310 | 311 | |Attribute|Value| 312 | |---|---| 313 | |Alias|LaraN| 314 | |RecipientType|MailUser| 315 | |RecipientTypeDetails|MailUser| 316 | |UserPrincipalName|LaraN@northwintraders.onmicrosoft.com| 317 | |PrimarySmtpAddress|Lara.Newton@northwind.com| 318 | |ExternalEmailAddress|SMTP:LaraN@contoso.onmicrosoft.com| 319 | |ExchangeGuid|1ec059c7-8396-4d0b-af4e-d6bd4c12a8d8| 320 | |LegacyExchangeDN|/o=First Organization/ou=Exchange Administrative Group| 321 | ||(FYDIBOHF23SPDLT)/cn=Recipients/cn=74e5385fce4b46d19006876949855035Lara| 322 | |EmailAddresses|x500:/o=First Organization/ou=Exchange Administrative Group (FYDIBOHF23SPDLT)/cn=Recipients/cn=d11ec1a2cacd4f81858c8190| 323 | ||7273f1f9-Lara| 324 | ||smtp:LaraN@northwindtraders.onmicrosoft.com| 325 | ||SMTP:Lara.Newton@northwind.com| 326 | ||| 327 | 328 | Example **source** Mailbox object: 329 | 330 | |Attribute|Value| 331 | |---|---| 332 | |Alias|LaraN| 333 | |RecipientType|UserMailbox| 334 | |RecipientTypeDetails|UserMailbox| 335 | |UserPrincipalName|LaraN@contoso.onmicrosoft.com| 336 | |PrimarySmtpAddress|Lara.Newton@contoso.com| 337 | |ExchangeGuid|1ec059c7-8396-4d0b-af4e-d6bd4c12a8d8| 338 | |LegacyExchangeDN|/o=First Organization/ou=Exchange Administrative Group| 339 | ||(FYDIBOHF23SPDLT)/cn=Recipients/cn=d11ec1a2cacd4f81858c81907273f1f9Lara| 340 | |EmailAddresses|smtp:LaraN@contoso.onmicrosoft.com 341 | ||SMTP:Lara.Newton@contoso.com| 342 | ||| 343 | 344 | - Additional attributes may be included in Exchange hybrid write back already. If not, they should be included. 345 | - msExchBlockedSendersHash – Writes back online safe and blocked sender data from clients to on-premises Active Directory. 346 | - msExchSafeRecipientsHash – Writes back online safe and blocked sender data from clients to on-premises Active Directory. 347 | - msExchSafeSendersHash – Writes back online safe and blocked sender data from clients to on-premises Active Directory. 348 | 349 | 2. If the source mailbox is on LitigationHold and the source mailbox Recoverable Items size is greater than our database default (30 GB), moves will not proceed since the target quota is less than the source mailbox size. You can update the target MailUser object to transition the ELC mailbox flags from the source environment to the target, which triggers the target system to expand the quota of the MailUser to 100 GB, thus allowing the move to the target. These instructions will work only for hybrid identity running Azure AD Connect, as the commands to stamp the ELC flags are not exposed to tenant administrators. 350 | 351 | > [!NOTE] 352 | > SAMPLE – AS IS, NO WARRANTY 353 | > 354 | > This script assumes a connection to both source mailbox (to get source values) and the target on-premises Active Directory (to stamp the ADUser object). If source has litigation or single item recovery enabled, set this on the destination account. This will increase the dumpster size of destination account to 100 GB. 355 | 356 | ```powershell 357 | $ELCValue = 0 358 | if ($source.LitigationHoldEnabled) {$ELCValue = $ELCValue + 8} if ($source.SingleItemRecoveryEnabled) {$ELCValue = $ELCValue + 16} if ($ELCValue -gt 0) {Set-ADUser -Server $domainController -Identity $destination.SamAccountName -Replace @{msExchELCMailboxFlags=$ELCValue}} 359 | ``` 360 | 361 | 3. Non-hybrid target tenants can modify the quota on the Recoverable Items folder for the MailUsers prior to migration by running the following command to enable Litigation Hold on the MailUser object and increasing the quota to 100 GB: `Set-MailUser -EnableLitigationHoldForMigration`. Note this will not work for tenants in hybrid. 362 | 363 | 4. Users in the target organization must be licensed with appropriate Exchange Online subscriptions applicable for the organization. You may apply a license in advance of a mailbox move but ONLY once the target MailUser is properly set up with ExchangeGUID and proxy addresses. Applying a license before the ExchangeGUID is applied will result in a new mailbox provisioned in target organization. 364 | 365 | > [!NOTE] 366 | > When you apply a license on a Mailbox or MailUser object, all SMTP type proxyAddresses are scrubbed to ensure only verified domains are included in the Exchange EmailAddresses array. 367 | 368 | 5. You must ensure that the target MailUser has no previous ExchangeGuid that does not match the Source ExchangeGuid. This might occur if the target MEU was previously licensed for Exchange Online and provisioned a mailbox. If the target MailUser was previously licensed for or had an ExchangeGuid that does not match the Source ExchangeGuid, you need to perform a cleanup of the cloud MEU. For these cloud MEUs, you can run `Set-User -PermanentlyClearPreviousMailboxInfo`. 369 | 370 | > [!CAUTION] 371 | > This process is irreversible. If the object has a softDeleted mailbox, it cannot be restored after this point. Once cleared, however, you can sync the correct ExchangeGuid to the target object and MRS will connect the source mailbox to the newly created target mailbox. (Reference EHLO blog on the new parameter.) 372 | 373 | Find objects that were previously mailboxes using this command. 374 | 375 | ```powershell 376 | Get-User | select Name, *recipient* | ft -AutoSize 377 | ``` 378 | 379 | Here is an example. 380 | 381 | ```powershell 382 | PS demo> get-user John@northwindtraders.com |select name, *recipient*| ft -AutoSize 383 | 384 | Name PreviousRecipientTypeDetails RecipientType RecipientTypeDetails 385 | ---- ---------------------------- ------------- -------------------- 386 | John UserMailbox MailUser MailUser 387 | ``` 388 | 389 | Clear the soft-deleted mailbox using this command. 390 | 391 | ```powershell 392 | Set-User -PermanentlyClearPreviousMailboxInfo 393 | ``` 394 | 395 | Here is an example. 396 | 397 | ```powershell 398 | PS demo> Set-User John@northwindtraders.com -PermanentlyClearPreviousMailboxInfo Confirm 399 | Are you sure you want to perform this action? 400 | Delete all existing information about user “John@northwindtraders.com"?. This operation will clear existing values from Previous home MDB and Previous Mailbox GUID of the user. After deletion, reconnecting to the previous mailbox that existed in the cloud will not be possible and any content it had will be unrecoverable PERMANENTLY. 401 | Do you want to continue? 402 | [Y] Yes [A] Yes to All [N] No [L] No to All [?] Help (default is "Y"): Y 403 | ``` 404 | 405 | ## Perform mailbox migrations 406 | 407 | Cross-tenant Exchange mailbox migrations are submitted as migration batches initiated from the target tenant. This is similar to the way that on-boarding migration batches work when migrating from Exchange on-premises to Microsoft 365. 408 | 409 | ### Create Migration batches 410 | 411 | Here is an example migration batch cmdlet for kicking off moves. 412 | 413 | ```powershell 414 | New-MigrationBatch -Name T2Tbatch-testforignitedemo -SourceEndpoint target_source_7977 -CSVData ([System.IO.File]::ReadAllBytes('users.csv')) -Autostart -TargetDeliveryDomain targetformoves.onmicrosoft.com -AutoComplete 415 | 416 | Identity Status Type TotalCount 417 | -------- ------ ---- ---------- 418 | T2Tbatch-testforignitedemo Syncing ExchangeRemoteMove 1 419 | 420 | ``` 421 | 422 | > [!NOTE] 423 | > The email address in the CSV file must be the one specified in the target tenant, not the source tenant. 424 | 425 | Migration batch submission is also supported from the new Exchange Admin Center when selecting the cross-tenant option. 426 | 427 | #### Update on-premises MailUsers 428 | 429 | Once the mailbox moves from source to target, you should ensure that the on-premises mail users, both Source and target, are updated with the new targetAddress. In the examples, the targetDeliveryDomain used in the move is **contoso.onmicrosoft.com**. Update the mail users with this targetAddress. 430 | 431 | ## Frequently asked questions 432 | 433 | **Do we need to update RemoteMailboxes in source on-premises after the move?** 434 | 435 | Yes, you should update the targetAddress (RemoteRoutingAddress/ExternalEmailAddress) of the source on-premises users when the source tenant mailbox moves to target tenant. While mail routing can follow the referrals across multiple mail users with different targetAddresses, Free/Busy lookups for mail users MUST target the location of the mailbox user. Free/Busy lookups will not chase multiple redirects. 436 | 437 | **Do Teams meetings migrate cross-tenant?** 438 | 439 | The meetings will move however the Teams meeting URL does not update when items migrate cross-tenant. Since the URL will be invalid in the target tenant you will need to remove and recreate the Teams meetings. 440 | 441 | **Does the Teams chat folder content migrate cross-tenant?** 442 | 443 | No, the Teams chat folder content does not migrate cross-tenant. 444 | 445 | **How can I see just moves that are cross-tenant moves, not my onboarding and off-boarding moves?** 446 | 447 | Use the `-flags` parameter. Here is an example. 448 | 449 | ```powershell 450 | Get-MoveRequest -Flags "CrossTenant" 451 | ``` 452 | 453 | **Can you provide example scripts for copying attributes used in testing?** 454 | 455 | > [!NOTE] 456 | > SAMPLE – AS IS, NO WARRANTY
This script assumes a connection to both source mailbox (to get source values) and the target on-premises Active Directory Domain Services (to stamp the ADUser object). If source has litigation or single item recovery enabled, set this on the destination account. This will increase the dumpster size of destination account to 100 GB. 457 | 458 | ```powershell 459 | #Dumps out the test mailboxes from SourceTenant 460 | #Note, the filter applied on Get-Mailbox is for an attribute set on CustomAttribute1 = "ProjectKermit" 461 | #These are the ‘target’ users to be moved to the Northwind org tenant ################################################################# 462 | $outFileUsers = "$home\desktop\userstomigrate.txt" 463 | $outFileUsersXML = "$home\desktop\userstomigrate.xml" 464 | #output the test objects 465 | Get-Mailbox -Filter "CustomAttribute1 -like 'ProjectKermit'" -ResultSize Unlimited | Select-Object -ExpandProperty Alias | Out-File $outFileUsers 466 | $mailboxes = Get-Content $outFileUsers 467 | $mailboxes | ForEach-Object {Get-Mailbox $_} | Select-Object PrimarySMTPAddress,Alias,SamAccountName,FirstName,LastName,DisplayName,Name,ExchangeGuid,ArchiveGuid,LegacyExchangeDn,EmailAddresses | Export-Clixml $outFileUsersXML 468 | 469 | ################################################################# 470 | #Copy the file $outfile to the desktop of the target on-premises 471 | #then run the below to create MEU in Target 472 | ################################################################# 473 | $mailboxes = Import-Clixml $home\desktop\userstomigrate.xml 474 | 475 | foreach ($m in $mailboxes) { 476 | $organization = "@contoso.onmicrosoft.com" 477 | $mosi = $m.Alias+$organization 478 | $Password = [System.Web.Security.Membership]::GeneratePassword(16,4) | ConvertTo-SecureString -AsPlainText -Force 479 | $x500 = "x500:" +$m.LegacyExchangeDn 480 | $tmpUser = New-MailUser -MicrosoftOnlineServicesID $mosi -PrimarySmtpAddress $mosi -ExternalEmailAddress $m.PrimarySmtpAddress -FirstName $m.FirstName -LastName $m.LastName -Name $m.Name -DisplayName $m.DisplayName -Alias $m.Alias -Password $Password 481 | $tmpUser | Set-MailUser -EmailAddresses @{add=$x500} -ExchangeGuid $m.ExchangeGuid -ArchiveGuid $m.ArchiveGuid -CustomAttribute1 "ProjectKermit" 482 | $tmpx500 = $m.EmailAddresses | ?{$_ -match "x500"} 483 | $tmpx500 | %{Set-MailUser $m.Alias -EmailAddresses @{add="$_"}} 484 | } 485 | 486 | ################################################################# 487 | # On AADSync machine, run AADSync 488 | ################################################################# 489 | Start-ADSyncSyncCycle 490 | 491 | #AADSync and FWDSync will create the target MEUs in the Target tenant 492 | ``` 493 | 494 | **How do we access Outlook on Day 1 after the use mailbox is moved?** 495 | 496 | Since only one tenant can own a domain, the former primary SMTPAddress will not be associated to the user in the target tenant when the mailbox move completes; only those domains associated with the new tenant. Outlook uses the users new UPN to authenticate to the service and the Outlook profile expects to find the legacy primary SMTPAddress to match the mailbox in the target system. Since the legacy address is not in the target System the outlook profile will not connect to find the newly moved mailbox. 497 | 498 | For this initial deployment, users will need to rebuild their profile with their new UPN, primary SMTP address and re-sync OST content. 499 | 500 | > [!NOTE] 501 | > Plan accordingly as you batch your users for completion. You need to account for network utilization and capacity when Outlook client profiles are created and subsequent OST and OAB files are downloaded to clients. 502 | 503 | **What Exchange RBAC roles do I need to be member of to set up or complete a cross-tenant move?** 504 | 505 | There a matrix of roles based on assumption of delegated duties when executing a mailbox move. Currently, two roles are required: 506 | 507 | - The first role is for a one-time setup task that establishes the authorization of moving content into or out of your tenant/organizational boundary. As moving data out of your organizational control is a critical concern for all companies, we opted with the highest assigned role of Organization Administrator (OrgAdmin). This role must alter or setup a new OrganizationRelationship that defines the -MailboxMoveCapability with the remote organization. Only the OrgAdmin can alter the MailboxMoveCapability setting, while other attributes on the OrganizationRelationhip can be managed by the Federated Sharing administrator. 508 | 509 | - The role of executing the actual move commands can be delegated to a lower-level function. The role of Move Mailboxes is assigned the capability of moving mailboxes in or out of the organization by using the `-RemoteTenant` parameter. 510 | 511 | **How do we target which SMTP address is selected for targetAddress (TargetDeliveryDomain) on the converted mailbox (to MailUser conversion)?** 512 | 513 | Exchange mailbox moves using MRS craft the targetAddress on the original source mailbox when converting to a MailUser by matching an email address (proxyAddress) on the target object. The process takes the -TargetDeliveryDomain value passed into the move command, then checks for a matching proxy for that domain on the target side. When we find a match, the matching proxyAddress is used to set the ExternalEmailAddress (targetAddress) on the converted mailbox (now MailUser) object. 514 | 515 | **How do mailbox permissions transition?** 516 | 517 | Mailbox permissions include Send on Behalf of and Mailbox Access: 518 | 519 | - Send On Behalf Of (AD:publicDelegates) stores the DN of recipients with access to a user’s mailbox as a delegate. This value is stored in Active Directory and currently does not move as part of the mailbox transition. If the source mailbox has publicDelegates set, you will need to restamp the publicDelegates on the target Mailbox once the MEU to Mailbox conversion completes in the target environment by running `Set-Mailbox -GrantSendOnBehalfTo `. 520 | 521 | - Mailbox Permissions that are stored in the mailbox will move with the mailbox when both the principal and the delegate are moved to the target system. For example, the user TestUser_7 is granted FullAccess to the mailbox TestUser_8 in the tenant SourceCompany.onmicrosoft.com. After the mailbox move completes to TargetCompany.onmicrosoft.com, the same permissions are set up in the target directory. Examples using *Get-MailboxPermission* for TestUser_7 in both source and target tenants are shown below. Exchange cmdlets are prefixed with source and target accordingly. 522 | 523 | Here's an example of the output of the mailbox permission before a move. 524 | 525 | ```powershell 526 | PS C:\PowerShell\> Get-SourceMailboxPermission testuser_7 |ft -AutoSize User, AccessRights, IsInherited, Deny 527 | User AccessRights IsInherited Deny 528 | ---- ------------ ----------- ---- 529 | NT AUTHORITY\SELF {FullAccess, ReadPermission} False False 530 | TestUser_8@SourceCompany.onmicrosoft.com {FullAccess} False False.... 531 | ``` 532 | 533 | Here's an example of the output of the mailbox permission after the move. 534 | 535 | ```powershell 536 | PS C:\PowerShell\> Get-TargetMailboxPermission testuser_7 | ft -AutoSize User, AccessRights, IsInherited, Deny 537 | User AccessRights IsInherited Deny 538 | ---- ------------ ----------- ---- 539 | NT AUTHORITY\SELF {FullAccess, ReadPermission} False FalseTestUser_8@TargetCompany.onmicrosoft.com {FullAccess} False False 540 | ``` 541 | 542 | > [!NOTE] 543 | > Cross-tenant mailbox and calendar permissions are NOT supported. You must organize principals and delegates into consolidated move batches so that these connected mailboxes are transitioned at the same time from the source tenant. 544 | 545 | **What X500 proxy should be added to the target MailUser proxy addresses to enable migration?** 546 | 547 | The cross-tenant mailbox migration requires that the LegacyExchangeDN value of the source mailbox object to be stamped as an x500 email address on the target MailUser object. 548 | 549 | Example: 550 | 551 | ```powershell 552 | LegacyExchangeDN value on source mailbox is: 553 | /o=First Organization/ou=Exchange Administrative Group(FYDIBOHF23SPDLT)/cn=Recipients/cn=d11ec1a2cacd4f81858c81907273f1f9Lara 554 | 555 | so the x500 email address to be added to target MailUser object would be: 556 | x500:/o=First Organization/ou=Exchange Administrative Group (FYDIBOHF23SPDLT)/cn=Recipients/cn=d11ec1a2cacd4f81858c81907273f1f9-Lara 557 | ``` 558 | 559 | > [!NOTE] 560 | > In addition to this X500 proxy, you will need to copy all X500 proxies from the mailbox in the source to the mailbox in the target. 561 | 562 | **Where do I start troubleshooting if moves do not work?** 563 | 564 | Start by running the VerifySetup.ps1 script located [on GitHub](https://github.com/microsoft/cross-tenant/releases/tag/Preview) and review the output. 565 | 566 | Here's an example of running VerifySetup.ps1 on the target tenant: 567 | 568 | ```powershell 569 | VerifySetup.ps1 -PartnerTenantId -ApplicationId -ApplicationKeyVaultUrl -PartnerTenantDomain -Verbose 570 | ``` 571 | 572 | Here's an eExample of running VerifySetup.ps1 on the source tenant: 573 | 574 | ```powershell 575 | VerifySetup.ps1 -PartnerTenantId -ApplicationId 576 | ``` 577 | 578 | **Can the source and target tenant utilize the same domain name?** 579 | 580 | No. The source and target tenant domain names must be unique. For example, a source domain of contoso.com and the target domain of fourthcoffee.com. 581 | 582 | **Will shared mailboxes move and still work?** 583 | 584 | Yes, however we only keep the store permissions as described in these articles: 585 | 586 | - [Microsoft Docs | Manage permissions for recipients in Exchange Online](/exchange/recipients-in-exchange-online/manage-permissions-for-recipients) 587 | 588 | - [Microsoft Support | How to grant Exchange and Outlook mailbox permissions in Office 365 dedicated](https://support.microsoft.com/topic/how-to-grant-exchange-and-outlook-mailbox-permissions-in-office-365-dedicated-bac01b2c-08ff-2eac-e1c8-6dd01cf77287) 589 | 590 | **Is Azure Key Vault required and when are transactions made?** 591 | 592 | Yes, an Azure subscription is required to use Key Vault to store the certificate to authorize migration. Unlike onboarding migrations which use username & password to authenticate to the source, cross-tenant mailbox migrations use OAuth and this certificate as the secret/credential. Access to the Key Vault must be maintained throughout all mailbox migrations as it is accessed once at the beginning and once end of migration, as well as once every 24 hours during incremental sync times. You can review AKV costing details [here](https://azure.microsoft.com/pricing/details/key-vault/). 593 | 594 | **Do you have any recommendations for batches?** 595 | 596 | Do not exceed 2000 mailboxes per batch. We strongly recommend submitting batches two weeks prior to the cut-over date as there is no impact to the end users during sync. If you need guidance for mailboxes quantities over 50,000 you can reach out to the Engineering Feedback Distribution List at crosstenantmigrationpreview@service.microsoft.com. 597 | 598 | **What if I use Service encryption with Customer Key?** 599 | 600 | The mailbox will be decrypted prior to moving. Ensure Customer Key is configured in the target tenant if it is still required. See [here](../compliance/customer-key-overview.md) for more information. 601 | 602 | **What is the estimated migration time?** 603 | 604 | To help you plan your migration, the table present [here](/exchange/mailbox-migration/office-365-migration-best-practices#estimated-migration-times) shows the guidelines about when to expect bulk mailbox migrations or individual migrations to complete. These estimates are based on a data analysis of previous customer migrations. Because every environment is unique, your exact migration velocity may vary. 605 | 606 | Do remember that this feature is currently in preview and the SLA and any applicable Service Levels do not apply to any performance or availability issues during the preview status of this feature. 607 | 608 | ## Known issues 609 | 610 | - **Issue: Auto Expanded archives cannot be migrated.** The cross-tenant migration feature support migrations of the primary mailbox and archive mailbox for a specific user. If the user in the source however has an auto expanded archive – meaning more than one archive mailbox, the feature is unable to migrate the additional archives and should fail. 611 | 612 | - **Issue: Cloud MailUsers with non-owned smtp proxyAddress block MRS moves background.** When creating target tenant MailUser objects, you must ensure that all SMTP proxy addresses belong to the target tenant organization. If an SMTP proxyAddress exists on the target mail user that does not belong to the local tenant, the conversion of the MailUser to Mailbox is prevented. This is due to our assurance that mailbox objects can only send mail from domains for which the tenant is authoritative (domains claimed by the tenant): 613 | 614 | - When you sync users from on-premises using Azure AD Connect, you provision on-premises MailUser objects with ExternalEmailAddress pointing to the source tenant where the mailbox exists (laran@contoso.onmicrosoft.com) and you stamp the PrimarySMTPAddress as a domain that resides in the target tenant (Lara.Newton@northwind.com). These values sync down to the tenant and an appropriate mail user is provisioned and ready for migration. An example object is shown here. 615 | 616 | ```powershell 617 | target/AADSynced user] PS C> Get-MailUser laran | select ExternalEmailAddress, EmailAddresses 618 | ExternalEmailAddress EmailAddresses 619 | -------------------- -------------- 620 | SMTP:laran@contoso.onmicrosoft.com {SMTP:lara.newton@northwind.com} 621 | ``` 622 | 623 | > [!NOTE] 624 | > The *contoso.onmicrosoft.com* address is *not* present in the EmailAddresses / proxyAddresses array. 625 | 626 | - **Issue: MailUser objects with “external” primary SMTP addresses are modified / reset to “internal” company claimed domains** 627 | 628 | MailUser objects are pointers to non-local mailboxes. In the case for cross-tenant mailbox migrations, we use MailUser objects to represent either the source mailbox (from the target organization’s perspective) or target mailbox (from the source organization’s perspective). The MailUsers will have an ExternalEmailAddress (targetAddress) that points to the smtp address of the actual mailbox (ProxyTest@fabrikam.onmicrosoft.com) and primarySMTP address that represents the displayed SMTP address of the mailbox user in the directory. Some organizations choose to display the primary SMTP address as an external SMTP address, not as an address owned/verified by the local tenant (such as fabrikam.com rather than as contoso.com). However, once an Exchange service plan object is applied to the MailUser via licensing operations, the primary SMTP address is modified to show as a domain verified by the local organization (contoso.com). There are two potential reasons: 629 | 630 | - When any Exchange service plan is applied to a MailUser, the Azure AD process starts to enforce proxy scrubbing to ensure that the local organization is not able to send mail out, spoof, or mail from another tenant. Any SMTP address on a recipient object with these service plans will be removed if the address is not verified by the local organization. As is the case in the example, the Fabikam.com domain is NOT verified by the contoso.onmicrosoft.com tenant, so the scrubbing removes that fabrikam.com domain. If you wish to persist these external domain on MailUser, either before the migration or after migration, you need to alter your migration processes to strip licenses after the move completes or before the move to ensure that the users have the expected external branding applied. You will need to ensure that the mailbox object is properly licensed to not affect mail service.

An example script to remove the service plans on a MailUser in the Contoso.onmicrosoft.com tenant is shown here. 631 | 632 | ```powershell 633 | $LO = New-MsolLicenseOptions -AccountSkuId "contoso:ENTERPRISEPREMIUM" DisabledPlans 634 | "LOCKBOX_ENTERPRISE","EXCHANGE_S_ENTERPRISE","INFORMATION_BARRIERS","MIP_S_CLP2"," 635 | MIP_S_CLP1","MYANALYTICS_P2","EXCHANGE_ANALYTICS","EQUIVIO_ANALYTICS","THREAT_INTE 636 | LLIGENCE","PAM_ENTERPRISE","PREMIUM_ENCRYPTION" 637 | Set-MsolUserLicense -UserPrincipalName proxytest@contoso.com LicenseOptions $lo 638 | ``` 639 | 640 | Results in the set of ServicePlans assigned are shown here. 641 | 642 | ```powershell 643 | (Get-MsolUser -UserPrincipalName proxytest@contoso.com).licenses |select 644 | -ExpandProperty servicestatus |sort ProvisioningStatus -Descending 645 | ServicePlan ProvisioningStatus 646 | ----------- ------------------ 647 | ATP_ENTERPRISE PendingProvisioning 648 | MICROSOFT_SEARCH PendingProvisioning 649 | INTUNE_O365 PendingActivation 650 | PAM_ENTERPRISE Disabled 651 | EXCHANGE_ANALYTICS Disabled 652 | EQUIVIO_ANALYTICS Disabled 653 | THREAT_INTELLIGENCE Disabled 654 | LOCKBOX_ENTERPRISE Disabled 655 | PREMIUM_ENCRYPTION Disabled 656 | EXCHANGE_S_ENTERPRISE Disabled 657 | INFORMATION_BARRIERS Disabled 658 | MYANALYTICS_P2 Disabled 659 | MIP_S_CLP1 Disabled 660 | MIP_S_CLP2 Disabled 661 | ADALLOM_S_O365 PendingInput 662 | RMS_S_ENTERPRISE Success 663 | YAMMER_ENTERPRISE Success 664 | PROJECTWORKMANAGEMENT Success 665 | BI_AZURE_P2 Success 666 | WHITEBOARD_PLAN3 Success 667 | SHAREPOINTENTERPRISE Success 668 | SHAREPOINTWAC Success 669 | KAIZALA_STANDALONE Success 670 | OFFICESUBSCRIPTION Success 671 | MCOSTANDARD Success 672 | Deskless Success 673 | STREAM_O365_E5 Success 674 | FLOW_O365_P3 Success 675 | POWERAPPS_O365_P3 Success 676 | TEAMS1 Success 677 | MCOEV Success 678 | MCOMEETADV Success 679 | BPOS_S_TODO_3 Success 680 | FORMS_PLAN_E5 Success 681 | SWAY Success 682 | ``` 683 | 684 | The user’s PrimarySMTPAddress is no longer scrubbed. The fabrikam.com domain is not owned by the contoso.onmicrosoft.com tenant and will persist as the primary SMTP address shown in the directory. 685 | 686 | Here is an example. 687 | 688 | ```powershell 689 | get-recipient proxytest | ft -a userprin*, primary*, external* 690 | PrimarySmtpAddress ExternalDirectoryObjectId ExternalEmailAddress 691 | ------------------ ------------------------- -------------------- 692 | proxytest@fabrikam.com e2513482-1d5b-4066-936a-cbc7f8f6f817 SMTP:proxytest@fabrikam.com 693 | ``` 694 | 695 | - When msExchRemoteRecipientType is set to 8 (DeprovisionMailbox), for on-premises MailUsers that are migrated to the target tenant, the proxy scrubbing logic in Azure will remove nonowned domains and reset the primarySMTP to an owned domain. By clearing msExchRemoteRecipientType in the on-premises MailUser, the proxy scrub logic no longer applies. 696 | 697 | Below is the full set of possible Service Plans that include Exchange Online. 698 | 699 | |Name| 700 | |---| 701 | |Advanced eDiscovery Storage (500GB)| 702 | |Customer Lockbox| 703 | |Data Loss Prevention| 704 | |Exchange Enterprise CAL Services (EOP, DLP)| 705 | |Exchange Essentials| 706 | |Exchange Foundation| 707 | |Exchange Online (P1)| 708 | |Exchange Online (Plan 1)| 709 | |Exchange Online (Plan 2)| 710 | |Exchange Online Archiving for Exchange Online| 711 | |Exchange Online Archiving for Exchange Server| 712 | |Exchange Online Inactive User Add-on| 713 | |Exchange Online Kiosk| 714 | |Exchange Online Multi-Geo| 715 | |Exchange Online Plan 1| 716 | |Exchange Online POP| 717 | |Exchange Online Protection| 718 | |Information Barriers| 719 | |Information Protection for Office 365 - Premium| 720 | |Information Protection for Office 365 - Standard| 721 | |Insights by MyAnalytics| 722 | |Microsoft 365 Advanced Auditing| 723 | |Microsoft Bookings| 724 | |Microsoft Business Center| 725 | |Microsoft MyAnalytics (Full)| 726 | |Office 365 Advanced eDiscovery| 727 | |Microsoft Defender for Office 365 (Plan 1)| 728 | |Microsoft Defender for Office 365 (Plan 2)| 729 | |Office 365 Privileged Access Management| 730 | |Premium Encryption in Office 365| 731 | || 732 | -------------------------------------------------------------------------------- /v1 Content/media/tenant-to-tenant-mailbox-move/csv-sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/cross-tenant/1a75ccd1a367b7ad0a116607813f80751c1e1ffd/v1 Content/media/tenant-to-tenant-mailbox-move/csv-sample.png -------------------------------------------------------------------------------- /v1 Content/media/tenant-to-tenant-mailbox-move/invited-by-target-tenant.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/cross-tenant/1a75ccd1a367b7ad0a116607813f80751c1e1ffd/v1 Content/media/tenant-to-tenant-mailbox-move/invited-by-target-tenant.png -------------------------------------------------------------------------------- /v1 Content/media/tenant-to-tenant-mailbox-move/permissions-requested-accept.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/cross-tenant/1a75ccd1a367b7ad0a116607813f80751c1e1ffd/v1 Content/media/tenant-to-tenant-mailbox-move/permissions-requested-accept.png -------------------------------------------------------------------------------- /v1 Content/media/tenant-to-tenant-mailbox-move/permissions-requested-dialog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/cross-tenant/1a75ccd1a367b7ad0a116607813f80751c1e1ffd/v1 Content/media/tenant-to-tenant-mailbox-move/permissions-requested-dialog.png -------------------------------------------------------------------------------- /v1 Content/media/tenant-to-tenant-mailbox-move/prepare-tenants-flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/cross-tenant/1a75ccd1a367b7ad0a116607813f80751c1e1ffd/v1 Content/media/tenant-to-tenant-mailbox-move/prepare-tenants-flow.png -------------------------------------------------------------------------------- /v1 Content/media/tenant-to-tenant-mailbox-move/prepare-tenants-flow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 55 | 56 | 57 | 59 | 60 | 61 | 62 | 63 | 64 | 66 | 67 | 68 | 69 | 70 | Page-1 71 | 72 | 73 | 74 | 75 | Sheet.2010 76 | Mailboxes are pull from source to target 77 | 78 | 79 | 80 | Mailboxes are pull from source to target 82 | 83 | Triangle 84 | 85 | 86 | 87 | 88 | 89 | 90 | Rectangle.14 91 | EXO 92 | 93 | 94 | 95 | 96 | 97 | 98 | EXO 99 | 100 | Rectangle.22 101 | Tenant Relationship App 102 | 103 | 104 | 105 | 106 | 107 | 108 | Tenant Relationship App 109 | 110 | Dynamic connector.23 111 | 112 | 113 | 114 | Sheet.29 115 | Source Tenant 116 | 117 | 118 | 119 | Source Tenant 121 | 122 | Triangle.30 123 | 124 | 125 | 126 | 127 | 128 | 129 | Rectangle.34 130 | EXO 131 | 132 | 133 | 134 | 135 | 136 | 137 | EXO 138 | 139 | Rectangle.38 140 | Tenant Relationship App 141 | 142 | 143 | 144 | 145 | 146 | 147 | Tenant Relationship App 148 | 149 | Dynamic connector.39 150 | 151 | 152 | 153 | Sheet.41 154 | EXO exposes Mailbox.Migration permission to TenantFriending A... 155 | 156 | 157 | 158 | EXO exposes Mailbox.Migration permission to TenantFriending App. 161 | 162 | Rectangle.42 163 | MSGraph 164 | 165 | 166 | 167 | 168 | 169 | 170 | MSGraph 171 | 172 | Dynamic connector.43 173 | 174 | 175 | 176 | Sheet.44 177 | Tenant Relationship App is assigned: - Mailbox.Migration perm... 178 | 179 | 180 | 181 | Tenant Relationship App is assigned: - Mailbox.Migration permission on exchange. (Required)- Directory.ReadWrite.All app permission on MSGraph (Optional) 184 | 185 | Sheet.45 186 | Target Tenant 187 | 188 | 189 | 190 | Target Tenant 192 | 193 | Sheet.50 194 | Source tenant admin consents to TenantRelationship app. 195 | 196 | 197 | 198 | Source tenant admin consents to TenantRelationship app. 200 | 201 | Sheet.51 202 | Target tenant sets up tenantrelationship app with permissions... 203 | 204 | 205 | 206 | Target tenant sets up tenantrelationship app with permissions on MSGraph and EXO and consents to it.App stores its application secret (cert) in keyvault and gives Exchange service principal permissions to read the cert. 212 | 213 | Sheet.52 214 | 215 | 216 | 218 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | Sheet.53 245 | 246 | 247 | 249 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | Sheet.54 276 | 277 | 278 | 280 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | Rectangle.55 307 | MSGraph 308 | 309 | 310 | 311 | 312 | 313 | 314 | MSGraph 315 | 316 | Dynamic connector.56 317 | 318 | 319 | 321 | Type inheritance 322 | 323 | 324 | 325 | 326 | 328 | 329 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | Triangle.59 338 | 339 | 340 | 341 | 342 | 343 | 344 | Sheet.60 345 | Target Tenant 346 | 347 | 348 | 349 | Target Tenant 351 | 352 | Sheet.61 353 | MigrationEndpoint - ApplicationId -AppSecretKeyVaultUrl - Rem... 354 | 355 | 356 | 357 | MigrationEndpoint- ApplicationId-AppSecretKeyVaultUrl- RemoteTenant 360 | 361 | Sheet.62 362 | OrganizationRelationship With Source Tenant 363 | 364 | 365 | 366 | OrganizationRelationshipWith Source Tenant 368 | 369 | Triangle.63 370 | 371 | 372 | 373 | 374 | 375 | 376 | Sheet.64 377 | Source Tenant 378 | 379 | 380 | 381 | Source Tenant 383 | 384 | Sheet.65 385 | OrganizationRelationship - OAuthApplicationId (AppId of Tenan... 386 | 387 | 388 | 389 | OrganizationRelationship- OAuthApplicationId (AppId of TenantRelationship app)- MailboxMovePublishedScopes (Security group created in 6) 392 | 393 | Simple Double Arrow 394 | 395 | 396 | 397 | 398 | 400 | 401 | 402 | Sheet.67 403 | Azure 404 | 405 | 406 | 407 | Azure 408 | 409 | Sheet.68 410 | Exchange Online 411 | 412 | 413 | 414 | ExchangeOnline 416 | 417 | Sheet.1000 418 | After source admin consent TF App is assigned: - Mailbox.Migr... 419 | 420 | 421 | 422 | After source admin consent TF App is assigned: - Mailbox.Migration permission on exchange. (Required)- Directory.ReadWrite.All app permission on MSGraph (Optional) 426 | 427 | Rectangle.1001 428 | Target Tenant key vault contains tenant friending app secret. 429 | 430 | 431 | 432 | 433 | 434 | 435 | Target Tenant key vault contains tenant friending app secret. 437 | 438 | Simple Double Arrow.1002 439 | 440 | 441 | 442 | 443 | 445 | 446 | 447 | Sheet.1003 448 | ApplicationId of the tenant relationship app AppSecretKeyVaul... 449 | 450 | 451 | 452 | ApplicationId of the tenant relationship appAppSecretKeyVaultUrl is the url to the secret certificate created in (2) 455 | 456 | Sheet.1004 457 | SecurityGroup containing mbxs in scope for move 458 | 459 | 460 | 461 | SecurityGroup containing mbxs in scope for move 463 | 464 | Circle.2001 465 | 1 466 | 467 | 468 | 469 | 470 | 471 | 472 | 1 473 | 474 | Circle.2002 475 | 2 476 | 477 | 478 | 479 | 480 | 481 | 482 | 2 483 | 484 | Circle.2003 485 | 3 486 | 487 | 488 | 489 | 490 | 491 | 492 | 3 493 | 494 | Circle.2004 495 | 4 496 | 497 | 498 | 499 | 500 | 501 | 502 | 4 503 | 504 | Circle.2005 505 | 5 506 | 507 | 508 | 509 | 510 | 511 | 512 | 5 513 | 514 | Circle.2006 515 | 6 516 | 517 | 518 | 519 | 520 | 521 | 522 | 6 523 | 524 | Circle.2007 525 | 7 526 | 527 | 528 | 529 | 530 | 531 | 532 | 7 533 | 534 | Simple Arrow.2008 535 | 536 | 537 | 538 | 539 | 541 | 542 | 543 | Circle.2009 544 | 8 545 | 546 | 547 | 548 | 549 | 550 | 551 | 8 552 | 553 | 554 | --------------------------------------------------------------------------------