Api/FlyCommon.ps1
|
<#
.SYNOPSIS Check if the project name available .DESCRIPTION Check if the project name available .PARAMETER ProjectName The name of the project which you want to check .OUTPUTS Boolean, True if the project name is available #> function Resolve-FlyProjectName { [CmdletBinding()] Param ( [Parameter(Position = 0, ValueFromPipelineByPropertyName = $true, Mandatory = $true)] [String] ${ProjectName} ) Process { 'Calling method: Resolve-FlyProjectName' | Write-Debug $notExist = Invoke-FlyCheckProjectExist -Name $ProjectName if (!$notExist) { throw ('The project "{0}" already exists. Configure a unique name for the project.' -f $ProjectName) } } } <# .SYNOPSIS Get the connection by name .DESCRIPTION Get the connection by name .PARAMETER ConnectionName The name of the connection .OUTPUTS ConnectionSummaryModel, the object model of the connection #> function Get-FlyConnectionByName { [CmdletBinding()] Param ( [Parameter(Position = 0, ValueFromPipelineByPropertyName = $true, Mandatory = $true)] [String] ${ConnectionName} ) Process { 'Calling method: Get-FlyConnectionByName' | Write-Debug $top = 200; $skip = 0; while ($true) { $connections = Get-FlyConnections -Search $ConnectionName -Top $top -Skip $skip $result = $connections.data | Where-Object { $_.Name -eq $ConnectionName } | Select-Object -First 1 if ($null -ne $result) { return $result } if (!$connections.nextLink) { throw ('Failed to retrieve the connection: {0}. Log in to Fly to confirm the connection name.' -f $ConnectionName) } $skip = $skip + $top } } } <# .SYNOPSIS Get the policy by name .DESCRIPTION Get the policy by name .PARAMETER PolicyName The name of the policy .OUTPUTS PolicySummaryModel, the object model of the policy #> function Get-FlyPolicyByName { [CmdletBinding()] Param ( [Parameter(Position = 0, ValueFromPipelineByPropertyName = $true, Mandatory = $true)] [String] ${PolicyName}, [Parameter(Mandatory = $true)] [PSCustomObject] ${PlatformType} ) Process { 'Calling method: Get-FlyPolicyByName' | Write-Debug $top = 200; $skip = 0; while ($true) { $policies = Get-FlyPolicies -MigrationModuleType $PlatformType -Search $PolicyName -Top $top -Skip $skip $result = $policies.data | Where-Object { $_.Name -eq $PolicyName } | Select-Object -First 1 if ($null -ne $result) { return $result } if (!$policies.nextLink) { throw ('Failed to retrieve the migration policy: {0}. Log in to Fly to confirm the policy name.' -f $PolicyName) } $skip = $skip + $top } } } <# .SYNOPSIS Get the tag by name .DESCRIPTION Get the tag by name .PARAMETER TagName The name of the tag .OUTPUTS TagSummaryModel, the object model of the tag #> function Get-FlyTagByName { [CmdletBinding()] Param ( [Parameter(Position = 0, ValueFromPipelineByPropertyName = $true, Mandatory = $true)] [String] ${TagName} ) Process { 'Calling method: Get-FlyTagByName' | Write-Debug $top = 200; $skip = 0; while ($true) { $tags = Get-FlyTags -Search $TagName -Top $top -Skip $skip $result = $tags.data | Where-Object { $_.Name -eq $TagName } | Select-Object -First 1 if ($null -ne $result) { return $result } if (!$tags.nextLink) { throw ('Failed to retrieve the tag: {0}. Log in to Fly to confirm the tag name.' -f $TagName) } $skip = $skip + $top } } } <# .SYNOPSIS Get the project by name .DESCRIPTION Get the project by name .PARAMETER ProjectName The name of the project .PARAMETER PlatformType The platform of the project .OUTPUTS ProjectSummaryModel, the object model of the project #> function Get-FlyProjectByName { [CmdletBinding()] Param ( [Parameter(Position = 0, ValueFromPipelineByPropertyName = $true, Mandatory = $true)] [String] ${ProjectName}, [Parameter(Mandatory = $false)] [String] [ValidateSet('Exchange', 'Teams', 'SharePoint', 'M365Group', 'TeamChat', 'OneDrive')] ${PlatformType} ) Process { 'Calling method: Get-FlyProjectByName' | Write-Debug $top = 200; $skip = 0; while ($true) { $projects = Get-FlyProjects -Search $ProjectName -Top $top -Skip $skip $result = $projects.data | Where-Object { $_.Name -eq $ProjectName } | Select-Object -First 1 if ($null -ne $result) { if ($PlatformType -and ($PlatformType -ne [PlatformType].GetEnumName($result.sourcePlatform))) { throw ('The type of the project "{0}" is not {1}.' -f $ProjectName, $PlatformType) } return $result } if (!$projects.nextLink) { throw ('Failed to retrieve the project: {0}. Log in to Fly to confirm the project name.' -f $ProjectName) } $skip = $skip + $top } } } <# .SYNOPSIS Get all project mappings by projectId .DESCRIPTION Get all project mappings by projectId .PARAMETER ProjectName The GUID of the project .OUTPUTS A list of the ProjectMappingSummaryModel #> function Get-FlyAllProjectMappings { [CmdletBinding()] Param ( [Parameter(Mandatory = $true)] [String] ${ProjectId} ) Process { 'Calling method: Get-FlyAllProjectMappings' | Write-Debug $top = 2000; $skip = 0; $result = New-Object System.Collections.ArrayList; while ($true) { $mappings = Get-FlyProjectMappings -ProjectId $ProjectId -Top $top -Skip $skip if ($null -ne $mappings.data) { $result.AddRange($mappings.data) } if (!$mappings.nextLink) { break; } $skip = $skip + $top } return $result } } function Get-FlySharePointMappings { [CmdletBinding()] Param ( [Parameter(Mandatory = $true)] [String] ${ProjectId}, [Parameter(Mandatory = $false)] [String] ${Mappings} ) Process { 'Calling method: Get-FlySharePointMappings' | Write-Debug $result = New-Object System.Collections.ArrayList if ($Mappings) { $allMappings = Get-FlyAllProjectMappings -ProjectId $targetProject.Id $targetMappings = Get-FlyMappingsFromCsv -Path $Mappings #Match the project mappings between csv file and specified project foreach ($target in $targetMappings) { foreach ($mapping in $allMappings) { $sourceIdentity = [System.Web.HttpUtility]::UrlDecode($target.'Source URL') $destinationIdentity = [System.Web.HttpUtility]::UrlDecode($target.'Destination URL') if ($mapping.SourceIdentity -eq $sourceIdentity -and $mapping.DestinationIdentity -eq $destinationIdentity) { [void]$result.Add($mapping); break; } } } if ($result.Count -eq 0) { throw 'No mapping in the CSV file matches the existing migration mappings in this project.' } } return $result } } function Get-FlyExchangeMappings { [CmdletBinding()] Param ( [Parameter(Mandatory = $true)] [String] ${ProjectId}, [Parameter(Mandatory = $false)] [String] ${Mappings} ) Process { 'Calling method: Get-FlyExchangeMappings' | Write-Debug $result = New-Object System.Collections.ArrayList if ($Mappings) { $allMappings = Get-FlyAllProjectMappings -ProjectId $targetProject.Id $targetMappings = Get-FlyMappingsFromCsv -Path $Mappings #Match the project mappings between csv file and specified project foreach ($target in $targetMappings) { foreach ($mapping in $allMappings) { $sourceIdentity = [System.Web.HttpUtility]::UrlDecode($target.'Source') $destinationIdentity = [System.Web.HttpUtility]::UrlDecode($target.'Destination') if ($mapping.SourceIdentity -eq $sourceIdentity -and $mapping.DestinationIdentity -eq $destinationIdentity) { [void]$result.Add($mapping); break; } } } if ($result.Count -eq 0) { throw 'No mapping in the CSV file matches the existing migration mappings in this project.' } } return $result } } function Get-FlyTeamsMappings { [CmdletBinding()] Param ( [Parameter(Mandatory = $true)] [String] ${ProjectId}, [Parameter(Mandatory = $false)] [String] ${Mappings} ) Process { 'Calling method: Get-FlyTeamsMappings' | Write-Debug $result = New-Object System.Collections.ArrayList if ($Mappings) { $allMappings = Get-FlyAllProjectMappings -ProjectId $targetProject.Id $targetMappings = Get-FlyMappingsFromCsv -Path $Mappings #Match the project mappings between csv file and specified project foreach ($target in $targetMappings) { foreach ($mapping in $allMappings) { $sourceIdentity = [System.Web.HttpUtility]::UrlDecode($target.'Source Team email address') $destinationIdentity = [System.Web.HttpUtility]::UrlDecode($target.'Destination Team email address') if ($mapping.SourceIdentity -eq $sourceIdentity -and $mapping.DestinationIdentity -eq $destinationIdentity) { [void]$result.Add($mapping); break; } } } if ($result.Count -eq 0) { throw 'No mapping in the CSV file matches the existing migration mappings in this project.' } } return $result } } function Get-FlyM365GroupMappings { [CmdletBinding()] Param ( [Parameter(Mandatory = $true)] [String] ${ProjectId}, [Parameter(Mandatory = $false)] [String] ${Mappings} ) Process { 'Calling method: Get-FlyM365GroupMappings' | Write-Debug $result = New-Object System.Collections.ArrayList if ($Mappings) { $allMappings = Get-FlyAllProjectMappings -ProjectId $targetProject.Id $targetMappings = Get-FlyMappingsFromCsv -Path $Mappings #Match the project mappings between csv file and specified project foreach ($target in $targetMappings) { foreach ($mapping in $allMappings) { $sourceIdentity = [System.Web.HttpUtility]::UrlDecode($target.'Source Group email address') $destinationIdentity = [System.Web.HttpUtility]::UrlDecode($target.'Destination Group email address') if ($mapping.SourceIdentity -eq $sourceIdentity -and $mapping.DestinationIdentity -eq $destinationIdentity) { [void]$result.Add($mapping); break; } } } if ($result.Count -eq 0) { throw 'No mapping in the CSV file matches the existing migration mappings in this project.' } } return $result } } function Get-FlyTeamChatMappings { [CmdletBinding()] Param ( [Parameter(Mandatory = $true)] [String] ${ProjectId}, [Parameter(Mandatory = $false)] [String] ${Mappings} ) Process { 'Calling method: Get-FlyTeamChatMappings' | Write-Debug $result = New-Object System.Collections.ArrayList if ($Mappings) { $allMappings = Get-FlyAllProjectMappings -ProjectId $targetProject.Id $targetMappings = Get-FlyMappingsFromCsv -Path $Mappings #Match the project mappings between csv file and specified project foreach ($target in $targetMappings) { foreach ($mapping in $allMappings) { $sourceIdentity = [System.Web.HttpUtility]::UrlDecode($target.'Source user') $destinationIdentity = [System.Web.HttpUtility]::UrlDecode($target.'Destination user') if ($mapping.SourceIdentity -eq $sourceIdentity -and $mapping.DestinationIdentity -eq $destinationIdentity) { [void]$result.Add($mapping); break; } } } if ($result.Count -eq 0) { throw 'No mapping in the CSV file matches the existing migration mappings in this project.' } } return $result } } function Get-FlyOneDriveMappings { [CmdletBinding()] Param ( [Parameter(Mandatory = $true)] [String] ${ProjectId}, [Parameter(Mandatory = $false)] [String] ${Mappings} ) Process { 'Calling method: Get-FlyOneDriveMappings' | Write-Debug $result = New-Object System.Collections.ArrayList if ($Mappings) { $allMappings = Get-FlyAllProjectMappings -ProjectId $targetProject.Id $targetMappings = Get-FlyMappingsFromCsv -Path $Mappings #Match the project mappings between csv file and specified project foreach ($target in $targetMappings) { foreach ($mapping in $allMappings) { $sourceIdentity = [System.Web.HttpUtility]::UrlDecode($target.'Source user') $destinationIdentity = [System.Web.HttpUtility]::UrlDecode($target.'Destination user') if ($mapping.SourceIdentity -eq $sourceIdentity -and $mapping.DestinationIdentity -eq $destinationIdentity) { [void]$result.Add($mapping); break; } } } if ($result.Count -eq 0) { throw 'No mapping in the CSV file matches the existing migration mappings in this project.' } } return $result } } function Get-FlyMappingSettingByName { [CmdletBinding()] Param ( [Parameter(Position = 0, ValueFromPipelineByPropertyName = $true, Mandatory = $false)] [String] ${MappingName}, [Parameter(Mandatory = $true)] [PSCustomObject] ${Type}, [Parameter(Mandatory = $false)] [PSCustomObject[]] ${MigrationModuleTypes}, [Parameter(Mandatory = $false)] [Boolean] ${NeedValidation} = $true ) Process { 'Calling method: Get-FlyMappingSettingByName' | Write-Debug if ($null -ne $MappingName -and $MappingName -ne '' -and $NeedValidation) { $top = 200; $skip = 0; while ($true) { $mappings = Get-FlyMappingSummaries -Type $Type -MigrationModuleTypes $MigrationModuleTypes -Search $MappingName -Top $top -Skip $skip $result = $mappings.data | Where-Object { $_.Name -eq $MappingName } | Select-Object -First 1 if ($null -ne $result) { return $result.Id } if (!$mappings.nextLink) { throw ('Failed to retrieve the mapping name: {0}. Log in to Fly to confirm the {1} mapping name.' -f $MappingName, $Type) } $skip = $skip + $top } } else { if ($null -ne $MappingName -and $MappingName -ne '') { Write-Host ('Skip verify {0} mapping name: {1}, it will not be saved since custom mapping is not selected.' -f $Type, $MappingName) -ForegroundColor Green } return [Guid]::Empty } } } function Get-FlyMappingSettingById { [CmdletBinding()] Param ( [Parameter(Position = 0, ValueFromPipelineByPropertyName = $true, Mandatory = $true)] [Guid] ${MappingId}, [Parameter(Mandatory = $true)] [PSCustomObject] ${Type}, [Parameter(Mandatory = $false)] [PSCustomObject[]] ${MigrationModuleTypes} ) Process { 'Calling method: Get-FlyMappingSettingById' | Write-Debug if ($null -eq $MappingId -or $MappingId -eq [Guid]::Empty) { return '' } $top = 200; $skip = 0; while ($true) { $mappings = Get-FlyMappingSummaries -Type $Type -MigrationModuleTypes $MigrationModuleTypes -Top $top -Skip $skip $result = $mappings.data | Where-Object { $_.Id -eq $MappingId } | Select-Object -First 1 if ($null -ne $result) { return $result.Name } if (!$mappings.nextLink) { throw ('Failed to retrieve the mapping name. Log in to Fly to confirm the {0} mapping id: {1}.' -f $Type, $MappingId) } $skip = $skip + $top } } } function Get-FlyEmailTemplateByName { [CmdletBinding()] Param ( [Parameter(Position = 0, ValueFromPipelineByPropertyName = $true, Mandatory = $true)] [String] ${TemplateName}, [Parameter(Mandatory = $true)] [PSCustomObject] ${Type} ) Process { 'Calling method: Get-FlyEmailTemplateByName' | Write-Debug $top = 200; $skip = 0; while ($true) { $templates = Get-FlyEmailTemplateSummaries -Search $TemplateName -Top $top -Skip $skip $result = $templates.data | Where-Object { $_.Name -eq $TemplateName -and $_.EmailType -eq $Type } | Select-Object -First 1 if ($null -ne $result) { return $result } if (!$templates.nextLink) { throw ('Failed to retrieve the email template: {0}. Log in to Fly to confirm the email template name.' -f $TemplateName) } $skip = $skip + $top } } } function Get-FlyEmailTemplateById { [CmdletBinding()] Param ( [Parameter(Position = 0, ValueFromPipelineByPropertyName = $true, Mandatory = $true)] [Guid] ${TemplateId} ) Process { 'Calling method: Get-FlyEmailTemplateById' | Write-Debug if ($null -eq $TemplateId -or $TemplateId -eq [Guid]::Empty) { return ''; } $top = 200; $skip = 0; while ($true) { $templates = Get-FlyEmailTemplateSummaries -Top $top -Skip $skip $result = $templates.data | Where-Object { $_.Id -eq $TemplateId } | Select-Object -First 1 if ($null -ne $result) { return $result.Name } if (!$templates.nextLink) { throw ('Failed to retrieve the email template name. Log in to Fly to confirm the email template id: {0}.' -f $TemplateId) } $skip = $skip + $top } } } function Get-FlyFilterPolicyByName { [CmdletBinding()] Param ( [Parameter(Position = 0, ValueFromPipelineByPropertyName = $true, Mandatory = $true)] [String] ${FilterPolicyName}, [Parameter(Mandatory = $true)] [PSCustomObject] ${MigrationModuleType} ) Process { 'Calling method: Get-FlyFilterPolicyByName' | Write-Debug $top = 200; $skip = 0; while ($true) { $filterPolicies = Get-FlyFilterSummaries -MigrationModuleType $MigrationModuleType -Search $FilterPolicyName -Top $top -Skip $skip $result = $filterPolicies.data | Where-Object { $_.Name -eq $FilterPolicyName } | Select-Object -First 1 if ($null -ne $result) { return $result } if (!$filterPolicies.nextLink) { throw ('Failed to retrieve the filter policy: {0}. Log in to Fly to confirm the filter policy name.' -f $FilterPolicyName) } $skip = $skip + $top } } } function Get-FlyFilterPolicyById { [CmdletBinding()] Param ( [Parameter(Position = 0, ValueFromPipelineByPropertyName = $true, Mandatory = $true)] [Guid] ${PolicyId}, [Parameter(Mandatory = $true)] [PSCustomObject] ${MigrationModuleType} ) Process { 'Calling method: Get-FlyFilterPolicyById' | Write-Debug if ($null -eq $PolicyId -or $PolicyId -eq [Guid]::Empty) { return ''; } $top = 200; $skip = 0; while ($true) { $filterPolicies = Get-FlyFilterSummaries -MigrationModuleType $MigrationModuleType -Top $top -Skip $skip $result = $filterPolicies.data | Where-Object { $_.Id -eq $PolicyId } | Select-Object -First 1 if ($null -ne $result) { return $result.Name } if (!$filterPolicies.nextLink) { throw ('Failed to retrieve the filter policy name. Log in to Fly to confirm the filter policy id: {0}.' -f $PolicyId) } $skip = $skip + $top } } } function Initialize-FlyNotificationCreationModel { [CmdletBinding()] Param ( [Parameter(Mandatory = $false)] [Notification] $NotificationSettings, [Parameter(Mandatory = $false)] [PSCustomObject]$MigrationModuleType ) Process { 'Calling method: Initialize-FlyNotificationCreationModel' | Write-Debug if ( $null -eq $NotificationSettings ) { return [PSCustomObject]@{ "sendProjectMigration" = $false "sendIndividualMapping" = $false } } else { $individualEmailTemplateId = if ($NotificationSettings.SendIndividualMapping) { (Get-FlyEmailTemplateByName -TemplateName $NotificationSettings.IndividualLevelEmailTemplateName -Type ([EmailTemplateType]::MappingNotification)).Id } else { [Guid]::Empty } $projectMigrationEmailTemplateId = if ($NotificationSettings.SendProjectMigration) { (Get-FlyEmailTemplateByName -TemplateName $NotificationSettings.ProjectLevelEmailTemplateName -Type ([EmailTemplateType]::ProjectNotification)).Id } else { [Guid]::Empty } $startTime = if ($NotificationSettings.SendProjectMigration) { Convert-DateTimeToTicks -DateTime $NotificationSettings.StartTime } else { 0 } $endTime = if ($NotificationSettings.SendProjectMigration -and $NotificationSettings.IsEnableEndTime) { Convert-DateTimeToTicks -DateTime $NotificationSettings.EndTime } else { 0 } $projectLevelEmailRecipients = $null if ($NotificationSettings.SendProjectMigration -and $null -ne $NotificationSettings.ProjectLevelEmailRecipients) { $projectLevelEmailRecipients = @($NotificationSettings.ProjectLevelEmailRecipients) } $additionalRecipients = $null if ($NotificationSettings.SendIndividualMapping -and $null -ne $NotificationSettings.AdditionalRecipients) { $additionalRecipients = @($NotificationSettings.AdditionalRecipients) } $notificationModel = [PSCustomObject]@{ "sendProjectMigration" = $NotificationSettings.SendProjectMigration "frequency" = if ($NotificationSettings.SendProjectMigration) { $NotificationSettings.FrequencyDays }else { 1 } "startTime" = $startTime "isEnableEndTime" = if ($NotificationSettings.SendProjectMigration) { $NotificationSettings.IsEnableEndTime } else { $false } "endTime" = $endTime "migrationEmailRecipients" = $projectLevelEmailRecipients "projectMigrationEmailTemplateId" = $projectMigrationEmailTemplateId "sendIndividualMapping" = $NotificationSettings.SendIndividualMapping "mappingCondition" = if ($NotificationSettings.SendIndividualMapping) { $NotificationSettings.IndividualMappingLevel } else { [InformMappingCondition]::OnlyFailedMappings } "sendToSourceEmail" = if ($NotificationSettings.SendIndividualMapping -and $MigrationModuleType -eq 'Exchange') { $NotificationSettings.SendToSourceEmail }else { $false } "sendToDestinationEmail" = if ($NotificationSettings.SendIndividualMapping -and $MigrationModuleType -eq 'Exchange') { $NotificationSettings.SendToDestinationEmail } else { $false } "sendToMe" = if ($NotificationSettings.SendIndividualMapping) { $NotificationSettings.SendToPersonRunMigration } else { $false } "sendToAdditionalRecipients" = if ($NotificationSettings.SendIndividualMapping) { $NotificationSettings.SendToAdditionalRecipients } else { $false } "additionalRecipients" = $additionalRecipients "emailTemplateId" = $individualEmailTemplateId } return $notificationModel } } } function Resolve-FlyNotificationCreationModel { [CmdletBinding()] Param ( [Parameter(Mandatory = $false)] [PSCustomObject]$NotificationSettings, [Parameter(Mandatory = $false)] [PSCustomObject]$MigrationModuleType ) Process { 'Calling method: Resolve-FlyNotificationCreationModel' | Write-Debug if ( $null -eq $NotificationSettings ) { throw 'Notification object should not be null.' } $notificationProperties = ("sendProjectMigration", "frequency", "startTime", "isEnableEndTime", "endTime", "migrationEmailRecipients", "projectMigrationEmailTemplateId", "sendIndividualMapping", "mappingCondition", "sendToSourceEmail", "sendToDestinationEmail", "sendToMe", "sendToAdditionalRecipients", "additionalRecipients", "emailTemplateId") if ($MigrationModuleType -ne 'Exchange') { $notificationProperties = $notificationProperties | Where-Object { $_ -ne "sendToSourceEmail" -and $_ -ne "sendToDestinationEmail" } } Confirm-FlyObjectPropertyExistence -Object $NotificationSettings -Properties $notificationProperties $individualEmailTemplateId = if ($NotificationSettings.SendIndividualMapping) { (Get-FlyEmailTemplateByName -TemplateName $NotificationSettings.EmailTemplateId -Type ([EmailTemplateType]::MappingNotification)).Id } else { [Guid]::Empty } $projectMigrationEmailTemplateId = if ($NotificationSettings.SendProjectMigration) { (Get-FlyEmailTemplateByName -TemplateName $NotificationSettings.ProjectMigrationEmailTemplateId -Type ([EmailTemplateType]::ProjectNotification)).Id } else { [Guid]::Empty } $startTime = if ($NotificationSettings.SendProjectMigration -and (Confirm-FlyDateTimeProperty -DateTimeValue $NotificationSettings.StartTime -PropertyName 'StartTime' -Format 'MM/dd/yyyy HH:mm')) { Convert-DateTimeToTicks -DateTime $NotificationSettings.StartTime } else { 0 } $endTime = if ($NotificationSettings.SendProjectMigration -and (Confirm-FlyDateTimeProperty -DateTimeValue $NotificationSettings.EndTime -PropertyName 'EndTime' -Format 'MM/dd/yyyy HH:mm')) { Convert-DateTimeToTicks -DateTime $NotificationSettings.EndTime } else { 0 } $projectLevelEmailRecipients = $null if ($NotificationSettings.SendProjectMigration -and $null -ne $NotificationSettings.MigrationEmailRecipients) { $projectLevelEmailRecipients = @($NotificationSettings.MigrationEmailRecipients) } $additionalRecipients = $null if ($NotificationSettings.SendIndividualMapping -and $null -ne $NotificationSettings.AdditionalRecipients) { $additionalRecipients = @($NotificationSettings.AdditionalRecipients) } $notificationModel = [PSCustomObject]@{ "sendProjectMigration" = $NotificationSettings.SendProjectMigration "frequency" = if ($NotificationSettings.SendProjectMigration) { $NotificationSettings.Frequency } else { 1 } "startTime" = $startTime "isEnableEndTime" = if ($NotificationSettings.SendProjectMigration) { $NotificationSettings.IsEnableEndTime } else { $false } "endTime" = $endTime "migrationEmailRecipients" = $projectLevelEmailRecipients "projectMigrationEmailTemplateId" = $projectMigrationEmailTemplateId "sendIndividualMapping" = $NotificationSettings.SendIndividualMapping "mappingCondition" = if ($NotificationSettings.SendIndividualMapping) { $NotificationSettings.MappingCondition } else { [InformMappingCondition]::OnlyFailedMappings } "sendToSourceEmail" = if ($NotificationSettings.SendIndividualMapping -and $MigrationModuleType -eq 'Exchange') { $NotificationSettings.SendToSourceEmail }else { $false } "sendToDestinationEmail" = if ($NotificationSettings.SendIndividualMapping -and $MigrationModuleType -eq 'Exchange') { $NotificationSettings.SendToDestinationEmail } else { $false } "sendToMe" = if ($NotificationSettings.SendIndividualMapping) { $NotificationSettings.SendToMe } else { $false } "sendToAdditionalRecipients" = if ($NotificationSettings.SendIndividualMapping) { $NotificationSettings.SendToAdditionalRecipients } else { $false } "additionalRecipients" = $additionalRecipients "emailTemplateId" = $individualEmailTemplateId } return $notificationModel } } function Initialize-FlyUserMappingRules { [CmdletBinding()] Param ( [Parameter(Mandatory = $false)] [UserMappingRuleProperty[]] $UserMappingRuleSettings, [Parameter(Mandatory = $false)] [PSCustomObject]$MigrationModuleType ) Process { 'Calling method: Initialize-FlyUserMappingRules' | Write-Debug $all = [UserMappingRuleProperty]::UPN, [UserMappingRuleProperty]::DisplayName, [UserMappingRuleProperty]::Email if ($null -eq $UserMappingRuleSettings -or $UserMappingRuleSettings.Count -eq 0 ) { return @( [PSCustomObject]@{ enabled = $false; sequence = 0; property = $all[0] } [PSCustomObject]@{ enabled = $false; sequence = 1; property = $all[1] } [PSCustomObject]@{ enabled = $false; sequence = 2; property = $all[2] } ) } elseif ($UserMappingRuleSettings.Count -gt 3) { throw ('UserMappingRules cannot contain more than 3 rules.') } elseif ($MigrationModuleType -eq 'TeamChat' -and $UserMappingRuleSettings -contains [UserMappingRuleProperty]::Email) { throw ('TeamChat UserMappingRules are not supported Email property.') } else { $orderedUnique = $UserMappingRuleSettings | Select-Object -Unique if ( $orderedUnique.Count -ne $UserMappingRuleSettings.Count ) { throw ('UserMappingRules cannot contain duplicate rules.') } $result = @() $seq = 0 foreach ($name in $UserMappingRuleSettings) { $result += [PSCustomObject]@{ enabled = $true sequence = $seq property = $name } $seq++ } foreach ($p in $all) { if (-not ($UserMappingRuleSettings -contains $p)) { $result += [PSCustomObject]@{ enabled = $false sequence = $seq property = $p } $seq++ } } return $result } } } function Resolve-FlyUserMappingRules { [CmdletBinding()] Param ( [Parameter(Mandatory = $false)] [PSCustomObject] $UserMappingRuleSettings, [Parameter(Mandatory = $false)] [PSCustomObject]$MigrationModuleType ) Process { 'Calling method: Resolve-FlyUserMappingRules' | Write-Debug if ($null -eq $UserMappingRuleSettings -or -not ($UserMappingRuleSettings -is [System.Collections.IEnumerable] -and $UserMappingRuleSettings -isnot [string])) { throw ('UserMappingRules must be an array of objects.') } elseif ($MigrationModuleType -eq 'TeamChat' -and $UserMappingRuleSettings.Count -ne 2) { throw ('TeamChat UserMappingRules must contain exactly 2 rules.') } elseif ($MigrationModuleType -ne 'TeamChat' -and $UserMappingRuleSettings.Count -ne 3) { throw ('UserMappingRules must contain exactly 3 rules.') } $required = 'enabled', 'sequence', 'property' $sequences = @() $properties = @() foreach ($item in $UserMappingRuleSettings) { Confirm-FlyObjectPropertyExistence -Object $item -Properties $required if ($item.enabled -isnot [bool]) { throw 'UserMappingRules: enabled must be true or false.' } try { $seq = [int]$item.sequence } catch { throw 'UserMappingRules: sequence must be 0 or 1 or 2.' } try { $prop = [int]$item.property } catch { throw 'UserMappingRules: property must be 0 or 1 or 2.' } $sequences += $seq $properties += $prop } if ($MigrationModuleType -eq 'TeamChat') { if ((@(0, 1) -join ',') -ne (($sequences | Sort-Object) -join ',')) { throw 'UserMappingRules: sequence must be unique values 0, 1.' } if ((@(0, 1) -join ',') -ne (($properties | Sort-Object) -join ',')) { throw 'UserMappingRules: property must be unique values 0, 1.' } } else { if ((@(0, 1, 2) -join ',') -ne (($sequences | Sort-Object) -join ',')) { throw 'UserMappingRules: sequence must be unique values 0, 1, 2.' } if ((@(0, 1, 2) -join ',') -ne (($properties | Sort-Object) -join ',')) { throw 'UserMappingRules: property must be unique values 0, 1, 2.' } } $sorted = $UserMappingRuleSettings | Sort-Object { [int]$_.sequence } $seenDisabled = $false foreach ($r in $sorted) { if (-not $r.enabled) { $seenDisabled = $true } elseif ($seenDisabled -and $r.enabled) { throw 'UserMappingRules: sequence number of enabled property must be less than sequence number of disabled property.' } } $disabledProps = $sorted | Where-Object { -not $_.enabled } | ForEach-Object { [int]$_.property } if ($disabledProps.Count -gt 1) { for ($i = 1; $i -lt $disabledProps.Count; $i++) { if ($disabledProps[$i] -lt $disabledProps[$i - 1]) { throw ('For disabled property of UserMappingRules: sequence number of property: {0} must be less than sequence number of property: {1}.' -f $disabledProps[$i], $disabledProps[$i - 1]) } } } return $sorted } } function Confirm-FlyObjectPropertyExistence { param ( [Parameter(Mandatory = $true)] [PSCustomObject] ${Object}, [Parameter(Mandatory = $true)] [string[]] ${Properties} ) Process { 'Calling method: Confirm-FlyObjectPropertyExistence' | Write-Debug foreach ($propertyKey in $Properties) { $prop = $Object.PSObject.Properties[$propertyKey] if (-not $prop) { throw ('{0} property cannot be empty.' -f $propertyKey) } } } } function Confirm-FlyDateTimeProperty { [CmdletBinding()] Param( [Parameter(Mandatory = $true)] [PSCustomObject] ${DateTimeValue}, [Parameter(Mandatory = $true)] [String] ${PropertyName}, [Parameter(Mandatory = $false)] [String] ${Format} ) Process { 'Calling method: Confirm-FlyDateTimeProperty' | Write-Debug if ($null -eq $DateTimeValue) { return $false } if ($DateTimeValue -is [String] -and [String]::IsNullOrWhiteSpace($DateTimeValue)) { return $false } $result = $false if ($DateTimeValue -is [DateTime]) { $result = $true } elseif ($DateTimeValue -is [String]) { $parsedDt = [DateTime]::MinValue if ([DateTime]::TryParse($DateTimeValue, [ref]$parsedDt)) { $result = $true } else { throw "Property $PropertyName is not a valid DateTime: $DateTimeValue" } } else { throw "Error: Property $PropertyName is not a valid DateTime: $DateTimeValue" } if ($null -ne $Format -and $Format -ne '') { $culture = [System.Globalization.CultureInfo]::InvariantCulture try { $dt = [DateTime]::ParseExact($DateTimeValue, $Format, $culture, [System.Globalization.DateTimeStyles]::None) if (($dt.ToString($Format, $culture) -ne $DateTimeValue)) { throw "Property $PropertyName should be $Format format." } } catch { throw "Error: Property $PropertyName should be $Format format." } } return $result } } function Convert-TicksToDateTime { [CmdletBinding()] [OutputType([String])] Param( [Parameter(Mandatory = $true)] [Int64]$Ticks, [Parameter(Mandatory = $true)] [Boolean]$ShowHourFormat ) Process { 'Calling method: Convert-TicksToDateTime' | Write-Debug if ($Ticks -lt 0 -or $Ticks -gt [DateTime]::MaxValue.Ticks) { throw "Ticks out of range: $Ticks" } if ($Ticks -eq 0 ) { return '' } $utc = [DateTime]::SpecifyKind([DateTime]::new($Ticks), [DateTimeKind]::Utc) if ($ShowHourFormat) { return $utc.ToLocalTime().ToString('MM/dd/yyyy HH:mm') } else { return $utc.ToLocalTime().ToString('MM/dd/yyyy') } } } function Convert-DateTimeToTicks { [CmdletBinding()] [OutputType([Int64])] Param( [Parameter(Mandatory = $true)] [DateTime]$DateTime ) Process { 'Calling method: Convert-DateTimeToTicks' | Write-Debug $utc = if ($DateTime.Kind -eq [DateTimeKind]::Utc) { $DateTime } else { $DateTime.ToUniversalTime() } return $utc.Ticks } } function Get-FlyPolicyFromJson { [CmdletBinding()] Param ( [Parameter(Mandatory = $true)] [String] ${Path} ) Process { 'Calling method: Get-FlyPolicyFromJson' | Write-Debug $isJson = Confirm-FileExtension -Path $Path -AllowedExtensions 'json' if (-not $isJson) { throw 'The file is not a JSON file.' } $result = Get-Content -Path $Path -Raw -Encoding UTF8 if ([String]::IsNullOrWhiteSpace($result) -or $result -eq '{}') { throw 'JSON file is empty.' } $model = $result | ConvertFrom-Json if ($null -eq $model) { throw "Object parsed is null." } if ($model -isnot [PSCustomObject]) { throw "The content is not a JSON object." } return $model } } function Get-FlyMappingsFromCsv { [CmdletBinding()] Param ( [Parameter(Mandatory = $true)] [String] ${Path} ) Process { 'Calling method: Get-FlyMappingsFromCsv' | Write-Debug $isCsv = Confirm-FileExtension -Path $Path -AllowedExtensions 'csv' if (-not $isCsv) { throw 'The file is not a CSV file.' } $result = Import-Csv -Path $Path if ($null -eq $result) { throw 'There is no data configured in the CSV file.' } return $result } } function Confirm-FileExtension { [CmdletBinding()] Param ( [Parameter(Mandatory = $true)] [String] ${Path}, [Parameter(Mandatory = $true)] [String[]] ${AllowedExtensions} ) Process { 'Calling method: Confirm-FileExtension' | Write-Debug $extension = '' if ($Path) { $split = (Split-Path -Path $Path -Leaf).Split(".") if ($split -and $split.Count -gt 1) { $extension = $split[1] } } if ($AllowedExtensions -contains $extension) { return $true } else { return $false } } } function Confirm-PSCustomObjectProperties { param ( [Parameter(Mandatory = $true)] [PSCustomObject] ${Object}, [Parameter(Mandatory = $true)] [string[]] ${Properties} ) Process { 'Calling method: Confirm-PSCustomObjectProperties' | Write-Debug foreach ($property in $Properties) { if (($null -eq $Object.$property) -or ('' -eq $Object.$property)) { throw ('{0} cannot be empty.' -f $property) } } } } function Convert-FlyMappingStatus { [CmdletBinding()] Param ( [Parameter(Mandatory = $true)] [PSCustomObject] ${Mapping} ) Process { 'Convert-FlyMappingStatus' | Write-Debug $progress = $Mapping.JobProgress $finalStatus = @([ProjectMappingItemStageStatus]::Successful, [ProjectMappingItemStageStatus]::Exceptioned, [ProjectMappingItemStageStatus]::Failed) if ($finalStatus -contains $Mapping.StageStatus) { $progress = 100 } return [PSCustomObject]@{ 'Source' = $Mapping.SourceIdentity 'Destination' = $Mapping.DestinationIdentity 'Stage' = Get-FlyMappingStage $Mapping.Stage 'Stage status' = Get-FlyMappingStageStatus $Mapping 'Job progress (%)' = $progress } } } function Get-FlyMappingStage { param ( [Parameter(Position = 0, ValueFromPipelineByPropertyName = $true, Mandatory = $true)] [ProjectMappingItemStage] ${Stage} ) Process { 'Calling method: Get-FlyMappingStage' | Write-Debug switch ($Stage) { ([ProjectMappingItemStage]::CreateMapping) { 'New mapping' } ([ProjectMappingItemStage]::RunVerification) { 'Mapping verification' } ([ProjectMappingItemStage]::RunAssessment) { 'Scan source data' } ([ProjectMappingItemStage]::RunDataMigration) { 'Migration' } ([ProjectMappingItemStage]::EmailForwarding) { 'Email forwarding' } ([ProjectMappingItemStage]::KeepX500EmailAddress) { 'Add X500 email address to destination mailboxes' } Default { [ProjectMappingItemStage].GetEnumName($Stage) } } } } function Get-FlyMappingStageStatus { param ( [Parameter(Position = 0, ValueFromPipelineByPropertyName = $true, Mandatory = $true)] [PSCustomObject] ${Mapping} ) Process { 'Calling method: Get-FlyMappingStageStatus' | Write-Debug if ($Mapping.StageStatus -eq [ProjectMappingItemStageStatus]::Waiting -and $Mapping.ScheduleTime -gt 0) { return 'Scheduled' } elseif ($Mapping.StageStatus -eq 9) { return 'Finished' } else { switch ([ProjectMappingItemStageStatus]$Mapping.StageStatus) { ([ProjectMappingItemStageStatus]::NotStart) { 'Not started' } ([ProjectMappingItemStageStatus]::Waiting) { 'In queue' } ([ProjectMappingItemStageStatus]::Queued) { 'In queue with priority' } ([ProjectMappingItemStageStatus]::InProgress) { 'In progress' } ([ProjectMappingItemStageStatus]::Successful) { 'Finished' } ([ProjectMappingItemStageStatus]::Exceptioned) { 'Exceptions' } ([ProjectMappingItemStageStatus]::Failed) { 'Failed' } ([ProjectMappingItemStageStatus]::Stopped) { 'Stopped' } Default { [ProjectMappingItemStageStatus].GetEnumName($Mapping.StageStatus) } } } } } <# .SYNOPSIS Convert the string value of object level to integer .DESCRIPTION Convert the string value of object level to integer .PARAMETER Level The string value of object level .OUTPUTS The integer value of object level #> function Get-FlyDataType { param ( [Parameter(Position = 0, ValueFromPipelineByPropertyName = $true, Mandatory = $true)] [String] ${Level} ) Process { 'Calling method: Get-FlyDataType' | Write-Debug switch ($Level) { 'Site collection' { 600 } 'Site' { 400 } 'List' { 200 } 'Folder' { 100 } 'User mailbox' { 1001 } 'Archive mailbox' { 1002 } 'Distribution list' { 1007 } 'Microsoft 365 Group mailbox' { 1003 } 'Resource mailbox' { 1004 } 'Shared mailbox' { 1005 } 'Mail-enabled security group' { 1008 } 'Microsoft 365 Group' { 1006 } Default { throw ('Invalid data type') } } } } <# .SYNOPSIS Convert the string value of method to SharePointMethodTypes, only support SharePoint object for now .DESCRIPTION Convert the string value of method to SharePointMethodTypes, only support SharePoint object for now .PARAMETER Level The string value of method .OUTPUTS SharePointMethodTypes, refer to SharePointMethodTypes.ps1 #> function Get-FlySharePointMethod { param ( [Parameter(Position = 0, ValueFromPipelineByPropertyName = $true, Mandatory = $true)] [String] ${Method} ) Process { 'Calling method: Get-FlySharePointMethod' | Write-Debug switch ($Method) { 'Merge' { [SharePointMethodTypes]::Combine } 'Attach' { [SharePointMethodTypes]::Attach } Default { throw ('Invalid method') } } } } <# .SYNOPSIS Convert the string value of type to TeamsChannelType .DESCRIPTION Convert the string value of type to TeamsChannelType .PARAMETER Level The string value of type .OUTPUTS TeamsChannelType, refer to TeamsChannelType.ps1 #> function Get-FlyTeamsChannelType { param ( [Parameter(Position = 0, ValueFromPipelineByPropertyName = $true, Mandatory = $true)] [String] ${Type} ) Process { 'Calling method: Get-FlyTeamsChannelType' | Write-Debug switch ($Type) { 'Standard' { [TeamsChannelType]::Standard } 'Private' { [TeamsChannelType]::Private } 'Shared' { [TeamsChannelType]::Shared } Default { throw ('Invalid teams channel type') } } } } <# .SYNOPSIS Output the error details of ErrorRecord .DESCRIPTION Output the error details of ErrorRecord .PARAMETER Level The information of ErrorRecord .OUTPUTS #> function ErrorDetail { [CmdletBinding()] Param ( [Parameter(Position = 0, ValueFromPipelineByPropertyName = $true, Mandatory = $true)] [System.Management.Automation.ErrorRecord] ${Error} ) Process { 'Calling method: ErrorDetail' | Write-Debug if ($Error.ErrorDetails.Message) { Write-Host $Error.ErrorDetails.Message -ForegroundColor Red } Write-Error $Error.Exception } } function Get-IdentityServiceToken { [CmdletBinding()] [OutputType([string])] Param( [Parameter(Mandatory)] [string]$IdentityServiceUri, [Parameter(Mandatory)] [string]$Scope, [Parameter(Mandatory)] [string]$ClientId, [Parameter(Mandatory)] [Alias("Certificate", "Cert")] [System.Security.Cryptography.X509Certificates.X509Certificate2]$SigningCertificate ) PROCESS { 'Calling method: Get-IdentityServiceToken' | Write-Debug $encodedThumbprint = ConvertTo-Base64UrlEncodedString -Bytes $SigningCertificate.GetCertHash() $headerTable = [ordered]@{typ = "JWT"; alg = "RS256"; kid = $encodedThumbprint } $header = $headerTable | ConvertTo-Json -Compress | ConvertTo-Base64UrlEncodedString $now = Get-Date $currentEpochTime = Convert-DateTimeToEpoch -DateTime $now $notBefore = $currentEpochTime $futureEpochTime = Convert-DateTimeToEpoch -DateTime ($now.AddHours(1)) $payloadTable = [ordered]@{sub = $ClientId; jti = ([System.Guid]::NewGuid()).ToString(); iss = $ClientId; aud = $IdentityServiceUri.TrimEnd('/') + "/connect/token"; nbf = $notBefore; exp = $futureEpochTime; iat = $currentEpochTime } $payload = $payloadTable | ConvertTo-Json -Compress | ConvertTo-Base64UrlEncodedString $jwtPlainText = "{0}.{1}" -f $header, $payload $jwtSig = New-JwtRsaSignature -JsonWebToken $jwtPlainText -SigningCertificate $SigningCertificate $ClientAssertion = "{0}.{1}" -f $jwtPlainText, $jwtSig $RequestUri = $IdentityServiceUri.TrimEnd('/') + "/connect/token" $Body = @{ grant_type = 'client_credentials' scope = $Scope client_assertion_type = 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer' client_assertion = $ClientAssertion } $Response = Invoke-WebRequest -Uri $RequestUri -Method 'POST' -Body $Body -ErrorAction Stop -UseBasicParsing return (ConvertFrom-Json $Response).access_token } } function Get-IdentityServiceTokenByClientSecret { [CmdletBinding()] [OutputType([string])] Param( [Parameter(Mandatory)] [string]$IdentityServiceUri, [Parameter(Mandatory)] [string]$Scope, [Parameter(Mandatory)] [string]$ClientId, [Parameter(Mandatory)] [string]$ClientSecret ) PROCESS { 'Calling method: Get-IdentityServiceTokenByClientSecret' | Write-Debug $RequestUri = $IdentityServiceUri.TrimEnd('/') + "/connect/token" $Body = @{ grant_type = 'client_credentials' scope = $Scope client_id = $ClientId client_secret = $ClientSecret } $Response = Invoke-WebRequest -Uri $RequestUri -Method 'POST' -Body $Body -ErrorAction Stop -UseBasicParsing return (ConvertFrom-Json $Response).access_token } } function New-JwtRsaSignature { [CmdletBinding()] [OutputType([string])] Param( [System.Security.Cryptography.X509Certificates.X509Certificate2]$SigningCertificate, [String]$JsonWebToken ) PROCESS { 'Calling method: New-JwtRsaSignature' | Write-Debug $rsa = ([System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($SigningCertificate)) if ($null -eq $rsa) { # Requiring the private key to be present throw "There's no private key in the supplied certificate." } [byte[]]$message = [System.Text.Encoding]::UTF8.GetBytes($JsonWebToken) $sigBytes = $rsa.SignData($message, [Security.Cryptography.HashAlgorithmName]::SHA256, [Security.Cryptography.RSASignaturePadding]::Pkcs1) return ConvertTo-Base64UrlEncodedString -Bytes $sigBytes } } function ConvertTo-Base64UrlEncodedString { [CmdletBinding()] [OutputType([string])] Param ( [Parameter(Position = 0, ParameterSetName = "String", Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [string]$InputString, [Parameter(Position = 1, ParameterSetName = "Byte Array", Mandatory = $false, ValueFromPipeline = $false, ValueFromPipelineByPropertyName = $false)] [byte[]]$Bytes ) PROCESS { [string]$base64UrlEncodedString = "" if ($PSBoundParameters.ContainsKey("Bytes")) { $output = [Convert]::ToBase64String($Bytes) $output = $output.Split('=')[0] # Remove any trailing '='s $output = $output.Replace('+', '-') # 62nd char of encoding $output = $output.Replace('/', '_') # 63rd char of encoding $base64UrlEncodedString = $output } else { $encoder = [System.Text.UTF8Encoding]::new() [byte[]]$inputBytes = $encoder.GetBytes($InputString) $base64String = [Convert]::ToBase64String($inputBytes) [string]$base64UrlEncodedString = "" $base64UrlEncodedString = $base64String.Split('=')[0] # Remove any trailing '='s $base64UrlEncodedString = $base64UrlEncodedString.Replace('+', '-'); # 62nd char of encoding $base64UrlEncodedString = $base64UrlEncodedString.Replace('/', '_'); # 63rd char of encoding } return $base64UrlEncodedString } } function Convert-DateTimeToEpoch { [CmdletBinding()] [OutputType([System.Int64])] Param( [Parameter(Mandatory)] [DateTime]$DateTime ) PROCESS { 'Calling method: Convert-DateTimeToEpoch' | Write-Debug $dtut = $DateTime.ToUniversalTime() [TimeSpan]$ts = New-TimeSpan -Start (Get-Date "01/01/1970") -End $dtut [Int64]$secondsSinceEpoch = [Math]::Floor($ts.TotalSeconds) return $secondsSinceEpoch } } function Convert-JWTtoken { [CmdletBinding()] [OutputType([System.Int64])] Param( [Parameter(Mandatory = $true)] [String]$token ) PROCESS { 'Calling method: Convert-JWTtoken' | Write-Debug #Validate as per https://tools.ietf.org/html/rfc7519 #Access and ID tokens are fine, Refresh tokens will not work if (!$token.Contains(".") -or !$token.StartsWith("eyJ")) { Write-Error "Invalid token" -ErrorAction Stop } #Header $tokenheader = $token.Split(".")[0].Replace('-', '+').Replace('_', '/') #Fix padding as needed, keep adding "=" until string length modulus 4 reaches 0 while ($tokenheader.Length % 4) { Write-Verbose "Invalid length for a Base-64 char array or string, adding ="; $tokenheader += "=" } #Payload $tokenPayload = $token.Split(".")[1].Replace('-', '+').Replace('_', '/') #Fix padding as needed, keep adding "=" until string length modulus 4 reaches 0 while ($tokenPayload.Length % 4) { Write-Verbose "Invalid length for a Base-64 char array or string, adding ="; $tokenPayload += "=" } #Convert to Byte array $tokenByteArray = [System.Convert]::FromBase64String($tokenPayload) #Convert to string array $tokenArray = [System.Text.Encoding]::UTF8.GetString($tokenByteArray) #Convert from JSON to PSObject $result = $tokenArray | ConvertFrom-Json return $result } } |