Bin/ADSyncDiagnostics/PSScripts/ADSyncObjectSyncDiagnostics.ps1
#------------------------------------------------------------------------- # Copyright (c) Microsoft Corporation. All rights reserved. #------------------------------------------------------------------------- Set-Variable ObjectDiagnosticsReportOutputDirectory "$env:ProgramData\AADConnect\ADSyncObjectDiagnostics" -Option Constant -Scope global Set-Variable SynchronizationIssueHtmlItems (New-Object System.Collections.Generic.List[string]) -Option Constant -Scope script Set-Variable HtmlGroupList (New-Object System.Collections.Generic.List[string]) -Option Constant -Scope script Set-Variable SynchronizationIssueList (New-Object System.Collections.Generic.List[string]) -Option Constant -Scope script # # Event IDs # Set-Variable EventIdSingleObjectTroubleshootingRun 2501 -Option Constant -Scope script Set-Variable EventIdMVObjectAttributeNotFound 2502 -Option Constant -Scope script Set-Variable EventIdFailedToImportMSOnlineModule 2503 -Option Constant -Scope script Set-Variable EventIdDiagnoseSingleObject 2504 -Option Constant -Scope script Set-Variable EventIdUPNMismatch 2505 -Option Constant -Scope script Set-Variable EventIdUPNUpdateBlocked 2506 -Option Constant -Scope script Set-Variable EventIdUPNSuffixNotVerified 2507 -Option Constant -Scope script Set-Variable EventIdUPNSuffixVerified 2508 -Option Constant -Scope script Set-Variable EventIdUPNFederatedDomainChange 2509 -Option Constant -Scope script Set-Variable EventIdDomainFiltered 2510 -Option Constant -Scope script Set-Variable EventIdOUFiltered 2511 -Option Constant -Scope script Set-Variable EventIdCsObjectAttributeNotFound 2512 -Option Constant -Scope script Set-Variable EventIdDynamicDistributionGroup 2513 -Option Constant -Scope script Set-Variable EventIdLinkedMailboxIssue 2514 -Option Constant -Scope script Set-Variable EventIdGroupMembershipTroubleshootingRun 2515 -Option Constant -Scope script Set-Variable EventIdIsToolHelpful 2516 -Option Constant -Scope script Set-Variable EventIdConnectorAccountReadPermissions 2517 -Option Constant -Scope script Set-Variable EventIdCloudOwnedAttributes 2518 -Option Constant -Scope script Set-Variable EventIdObjectTypeInclusion 2519 -Option Constant -Scope script Set-Variable EventIdGroupFiltered 2520 -Option Constant -Scope script # # Event Messages # Set-Variable EventMsgSingleObjectTroubleshootingRun "Single object troubleshooting workflow has been run." -Option Constant -Scope script Set-Variable EventMsgGroupMembershipTroubleshootingRun "Group membership troubleshooting workflow has been run." -Option Constant -Scope script Set-Variable EventMsgMVObjectAttributeNotFound "Metaverse object attribute is not found. Attribute Name: {0}." -Option Constant -Scope script Set-Variable EventMsgFailedToImportMSOnlineModule "Failed to import MSOnline Module." -Option Constant -Scope script Set-Variable EventMsgDiagnoseSingleObject "Single object diagnostics has been run." -Option Constant -Scope script Set-Variable EventMsgUPNMismatch "UPN Mismatch. AADConnect Object UPN: {0}, AAD Tenant Object UPN: {1}." -Option Constant -Scope script Set-Variable EventMsgUPNUpdateBlocked "UPN update is blocked for the AAD Tenant Object: {0}." -Option Constant -Scope script Set-Variable EventMsgUPNSuffixNotVerified "AADConnect object UPN suffix {0} is NOT verified with AAD Tenant {1}." -Option Constant -Scope script Set-Variable EventMsgUPNSuffixVerified "AADConnect object UPN suffix {0} is verified with AAD Tenant {1}." -Option Constant -Scope script Set-Variable EventMsgUPNFederatedDomainChange "Federated Domain Change. AADConnect Object UPN: {0}, AAD Tenant Object UPN: {1}." -Option Constant -Scope script Set-Variable EventMsgDomainFiltered "Object {0} filtered due to domain filtering. Domain: {1}" -Option Constant -Scope script Set-Variable EventMsgOUFiltered "Object {0} filtered due to OU filtering. OU: {1}" -Option Constant -Scope script Set-Variable EventMsgCsObjectAttributeNotFound "Connector space object attribute is not found. Attribute Name: {0}." -Option Constant -Scope script Set-Variable EventMsgDynamicDistributionGroup "Object is not synchronized since it is a dynamic distribution group." -Option Constant -Scope script Set-Variable EventMsgLinkedMailboxIssue "Object is not synchronized since it has on-premises linked mailbox." -Option Constant -Scope script Set-Variable EventMsgConnectorAccountReadPermissions "Comparing read permissions on object. AD Connector Name: {0}, Object DN: {1}, Connector account: {2}, Provided account: {3}" -Option Constant -Scope script Set-Variable EventMsgCloudOwnedAttributes "Cloud owned attributes found. AAD Object DN: {0}, Attributes: {1}" -Option Constant -Scope script Set-Variable EventMsgObjectTypeInclusion "Object {0} is of type '{1}' which is not part of the object type inclusion list for connector {2}" -Option Constant -Scope script Set-Variable EventMsgGroupFiltered "Object {0} filtered due to group filtering. Connector: {1}, Group filtering group: {2}" -Option Constant -Scope script Function Debug-ADSyncObjectSynchronizationIssuesNonInteractiveMode { param ( [string] [parameter(mandatory=$false)] $ADConnectorName, [string] [parameter(mandatory=$false)] $ObjectDN, [string] [parameter(mandatory=$true)] $DiagnosticOption ) $timezone = [TimeZoneInfo]::Local ReportOutput -PropertyName 'Sync Server TimeZone' -PropertyValue $timezone.DisplayName Debug-ADSyncObjectSynchronizationIssues -ADConnectorName $ADConnectorName -ObjectDN $ObjectDN -DiagnosticOption $DiagnosticOption } Function Debug-ADSyncGroupMembershipSynchronizationIssuesNonInteractiveMode { param ( [string] [parameter(mandatory=$false)] $GroupADConnectorName, [string] [parameter(mandatory=$false)] $GroupDN, [string] [parameter(mandatory=$false)] $MemberADConnectorName, [string] [parameter(mandatory=$false)] $MemberDN ) $timezone = [TimeZoneInfo]::Local ReportOutput -PropertyName 'Sync Server TimeZone' -PropertyValue $timezone.DisplayName Debug-ADSyncGroupMembershipSynchronizationIssues -GroupADConnectorName $GroupADConnectorName -GroupDN $GroupDN -MemberADConnectorName $MemberADConnectorName -MemberDN $MemberDN } Function Debug-ADSyncAttributeSynchronizationIssues { param ( [string] [parameter(mandatory=$false)] $ADConnectorName, [string] [parameter(mandatory=$false)] $ObjectDN, [string] [parameter(mandatory=$true)] $DiagnosticOption ) WriteEventLog($EventIdSingleObjectTroubleshootingRun)($EventMsgSingleObjectTroubleshootingRun) Write-Host "`r`n" $title = $DiagnosticOption # # Write Title to the PowerShell Console # WriteTitle($title) try { $aadConnectRegKey = Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Azure AD Connect' $aadConnectWizardPath = $aadConnectRegKey.WizardPath $aadConnectWizardFileName = "AzureADConnect.exe" $aadConnectPathLength = $aadConnectWizardPath.IndexOf($aadConnectWizardFileName) $aadConnectPath = $aadConnectWizardPath.Substring(0, $aadConnectPathLength) $modulePath = [System.IO.Path]::Combine($aadConnectPath, "AADPowerShell\MSOnline.psd1") Import-Module $modulePath -ErrorAction Stop $adSyncToolsModulePath = [System.IO.Path]::Combine($aadConnectPath, "Tools\AdSyncTools.psm1") Import-Module $adSyncToolsModulePath -ErrorAction Stop } catch { WriteEventLog($EventIdFailedToImportMSOnlineModule)($EventMsgFailedToImportMSOnlineModule) "MSOnline module needs to be imported" | ReportError return } Write-Host "`r`n" $adConnector = GetADConnectorByName($ADConnectorName)("Please enter AD Connector Name") if ($adConnector -eq $null) { "There is no AD Connector with name `"$ADConnectorName`"." | ReportError Write-Host "`r`n" return } $ADConnectorName = $adConnector.Name $ObjectDN = GetObjectDN($ObjectDN)("Please enter AD object Distinguished Name") # # Validate ObjectDN parameter # $adObject = Search-ADSyncDirectoryObjects -AdConnectorId $adConnector.Identifier -LdapFilter "(distinguishedName=$ObjectDN)" -SearchScope Subtree -SizeLimit 1 if ($adObject -eq $null -or $adObject.Count -lt 1) { "Could not find an object in on-premises AD with distinguishedName=`"$ObjectDN`"." | ReportError Write-Host "`r`n" return } # # Check if object is a dynamic distribution group # $isDynamicDistributionGroup = IsObjectTypeMatch($adObject[0])("msExchDynamicDistributionList") if ($isDynamicDistributionGroup -eq $true) { WriteEventLog($EventIdDynamicDistributionGroup)($EventMsgDynamicDistributionGroup) $SynchronizationIssueList.Add($global:DynamicDistributionGroupIssue) "The given object is a dynamic distribution group. Azure AD Connect does not synchronize on-premises dynamic distribution groups to Azure AD." | ReportError -PropertyName "Dynamic Distribution Group" -PropertyValue "True" Write-Host "`r`n" AskIfToolHelpful($ObjectDN) $SynchronizationIssueList.Clear() return } else { ReportOutput -PropertyName "Dynamic Distribution Group" -PropertyValue "False" } # # Get object graph # $adCsObject = $null $mvObject = $null $aadCsObject = $null $objectGraph = CheckObjectGraph($ADConnectorName)($ObjectDN)([ref]$adCsObject)([ref]$mvObject)([ref]$aadCsObject) $cloudownedAttributes = $null foreach ($attribute in $aadCsObject.Attributes) { if ($attribute.Name -ieq 'cloudMasteredProperties') { foreach ($value in $attribute.Values) { if ($cloudownedAttributes -eq $null) { $cloudownedAttributes = "[" + $value } else { $cloudownedAttributes += " ," + $value } } if ($cloudownedAttributes -ne $null) { $cloudownedAttributes += "]" } } } if ($cloudownedAttributes -ne $null) { $SynchronizationIssueList.Add($global:CloudOwnedAttributeIssue) WriteEventLog($EventIdCloudOwnedAttributes)($EventMsgCloudOwnedAttributes -f ($aadCsObject.DistinguishedName, $cloudownedAttributes)) "The given AD CS Object:`r`n" + $adCsObject.DistinguishedName + "`r`nAAD CS object:`r`n" + $aadCsObject.DistinguishedName + "`r`nhas cloud owned attributes:`r`n" + $cloudownedAttributes + "`r`nAzure AD Connect does not synchronize cloud owned attributes to Azure AD." | ReportError -PropertyName "Cloud owned attributes found" -PropertyValue "True" Write-Host "`r`n" AskIfToolHelpful($ObjectDN) $SynchronizationIssueList.Clear() return } else { ReportOutput -PropertyName "Cloud owned attributes found" -PropertyValue "False" } # # Ask if the tool is helpful for the synchronization issues # # The customer is going to answer for each synchronization issue separately # AskIfToolHelpful($ObjectDN) $SynchronizationIssueList.Clear() } Function Debug-ADSyncObjectSynchronizationIssues { param ( [string] [parameter(mandatory=$false)] $ADConnectorName, [string] [parameter(mandatory=$false)] $ObjectDN, [string] [parameter(mandatory=$true)] $DiagnosticOption ) WriteEventLog($EventIdSingleObjectTroubleshootingRun)($EventMsgSingleObjectTroubleshootingRun) Write-Host "`r`n" $title = $DiagnosticOption # # Write Title to the PowerShell Console # WriteTitle($title) try { $aadConnectRegKey = Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Azure AD Connect' $aadConnectWizardPath = $aadConnectRegKey.WizardPath $aadConnectWizardFileName = "AzureADConnect.exe" $aadConnectPathLength = $aadConnectWizardPath.IndexOf($aadConnectWizardFileName) $aadConnectPath = $aadConnectWizardPath.Substring(0, $aadConnectPathLength) $modulePath = [System.IO.Path]::Combine($aadConnectPath, "AADPowerShell\MSOnline.psd1") Import-Module $modulePath -ErrorAction Stop $adSyncToolsModulePath = [System.IO.Path]::Combine($aadConnectPath, "Tools\AdSyncTools.psm1") Import-Module $adSyncToolsModulePath -ErrorAction Stop } catch { WriteEventLog($EventIdFailedToImportMSOnlineModule)($EventMsgFailedToImportMSOnlineModule) "MSOnline module needs to be imported" | ReportError return } Write-Host "`r`n" $adConnector = GetADConnectorByName($ADConnectorName)("Please enter AD Connector Name") if ($adConnector -eq $null) { "There is no AD Connector with name `"$ADConnectorName`"." | ReportError Write-Host "`r`n" return } $ADConnectorName = $adConnector.Name $ObjectDN = GetObjectDN($ObjectDN)("Please enter AD object Distinguished Name") Write-Host "`r`n" Write-Host "Searching for object `"$ObjectDN`" using `"$($adConnector.Name)`" Connector credentials `"$($adConnector.ConnectivityParameters["forest-login-domain"].Value)\$($adConnector.ConnectivityParameters["forest-login-user"].Value)`"..." Write-Host "`r`n" # # Validate ObjectDN parameter # $adObject = Search-ADSyncDirectoryObjects -AdConnectorId $adConnector.Identifier -LdapFilter "(distinguishedName=$ObjectDN)" -SearchScope Subtree -SizeLimit 1 if ($adObject -eq $null -or $adObject.Count -lt 1) { "Could not find an object in on-premises AD with distinguishedName=`"$ObjectDN`" using Connector credentials." | ReportError if (!$isNonInteractiveMode) { $searchWithAdminOptions = [System.Management.Automation.Host.ChoiceDescription[]] @("&Yes", "&No") $searchWithAdmin = !($host.UI.PromptForChoice("Confirm object existence", "Would you like to search for the object using admin credentials?", $searchWithAdminOptions, 0)) if ($searchWithAdmin) { $ADForest = $adConnector.ConnectivityParameters["forest-name"].Value $ADForestCredential = GetValidADForestCredentials($ADForest) Write-Host "`r`n" if ($ADForestCredential -eq $null) { "No valid credentials were provided for forest with name=`"$ADForest`"." | ReportError } else { $adObjectFromProvidedAccount = Search-ADSyncDirectoryObjects -AdConnectorId $adConnector.Identifier -LdapFilter "(distinguishedName=$ObjectDN)" -SearchScope Subtree -SizeLimit 1 -AdConnectorCredential $ADForestCredential -ForestFqdn $ADForest if ($adObjectFromProvidedAccount -eq $null -or $adObjectFromProvidedAccount.Count -lt 1) { "Could not find an object in on-premises AD with distinguishedName=`"$ObjectDN`" using provided credentials. Please ensure the object exits in AD and the Distinguished Name is correct." | ReportError } else { "Object found in forest using admin credentials. Connector credentials to not have sufficient permissions to read the object. More information on configuring permissions can be found here: `"$global:ConfigureAccountPermissionsUrl`"" | ReportError } } } } Write-Host "`r`n" return } # # Check if object is a dynamic distribution group # $isDynamicDistributionGroup = IsObjectTypeMatch($adObject[0])("msExchDynamicDistributionList") if ($isDynamicDistributionGroup -eq $true) { WriteEventLog($EventIdDynamicDistributionGroup)($EventMsgDynamicDistributionGroup) $SynchronizationIssueList.Add($global:DynamicDistributionGroupIssue) "The given object is a dynamic distribution group. Azure AD Connect does not synchronize on-premises dynamic distribution groups to Azure AD." | ReportError -PropertyName "Dynamic Distribution Group" -PropertyValue "True" Write-Host "`r`n" AskIfToolHelpful($ObjectDN) $SynchronizationIssueList.Clear() return } else { ReportOutput -PropertyName "Dynamic Distribution Group" -PropertyValue "False" } # # Active Directory Connector Partition Details # $partitionDN = Get-DirectoryPartitionDN($ObjectDN) $partition = Get-ADSyncConnectorPartition -Connector $adConnector -Name $partitionDN # # Check Domain/OU Filtering, object type inclusion, and group filtering # CheckDomainBasedFiltering($partition)($adConnector)($ObjectDN) CheckOUBasedFiltering($partition)($ObjectDN) CheckObjectTypeInclusion($adConnector)($adObject[0]) CheckGroupFiltering($adConnector)($adObject[0]) # # Get object graph # $adCsObject = $null $mvObject = $null $aadCsObject = $null $objectGraph = CheckObjectGraph($ADConnectorName)($ObjectDN)([ref]$adCsObject)([ref]$mvObject)([ref]$aadCsObject) if ($adCsObject -ne $null) { # # Check attribute based filtering # CheckAttributeBasedFiltering($ADConnectorName)($ObjectDN)($adCsObject)($mvObject)($aadCsObject) # Check for Sync Errors and Export Errors if ((ReportSyncError($adCsObject)) -or (ReportSyncError($aadCsObject))) { ReportOutput -PropertyName "Sync Error(s)" -PropertyValue "True" } if ((ReportExportError($adCsObject)) -or (ReportExportError($aadCsObject))) { ReportOutput -PropertyName "Export Error(s)" -PropertyValue "True" } # Check for transient if ((ReportTransient($adCsObject)) -or (ReportTransient($aadCsObject))) { ReportOutput -PropertyName "Transient Object" -PropertyValue "True" } # # Connect to Azure Active Directory # ConnectToAAD # # Get all domains registered to AAD Tenant. # # Each AAD Tenant includes 2 default verified domains. # # 1. <Initial-default-domain-name>.onmicrosoft.com # 2. <Initial-default-domain-name>.mail.onmicrosoft.com # # Each AAD Tenant is associated with an initial default domain name. # $aadTenantDomains = GetAADDomains # # Get default domain name "<Initial-default-domain-name>.onmicrosoft.com" which replaces # non-routable OR unverified OR notAdded on-premises upn sufixes. # $aadTenantDefaultDomainName = GetAADTenantDefaultDomainName($aadTenantDomains) } # # Get AAD Tenant object and create html group # if (IsObjectTypeMatch($adObject[0])("user")) { $aadTenantUser = $null if ($mvObject -and $aadCsObject) { $aadTenantUser = GetAADTenantUser($mvObject) } $userObjectDetailsHtmlGroup = GetUserObjectHtmlGroup($adObject[0])($adCsObject)($mvObject)($aadCsObject)($aadTenantUser) $HtmlGroupList.Insert(0, $userObjectDetailsHtmlGroup) } elseif (IsObjectTypeMatch($adObject[0])("group")) { $aadTenantGroup = $null if ($mvObject -and $aadCsObject) { $aadTenantGroup = GetAADTenantGroup($mvObject) } $groupObjectDetailsHtmlGroup = GetGroupObjectHtmlGroup($adObject[0])($adCsObject)($mvObject)($aadCsObject)($aadTenantGroup) $HtmlGroupList.Insert(0, $groupObjectDetailsHtmlGroup) } elseif (IsObjectTypeMatch($adObject[0])("contact")) { $aadTenantContact = $null if ($mvObject -and $aadCsObject) { $aadTenantContact = GetAADTenantContact($mvObject) } $contactObjectDetailsHtmlGroup = GetContactObjectHtmlGroup($adObject[0])($adCsObject)($mvObject)($aadCsObject)($aadTenantContact) $HtmlGroupList.Insert(0, $contactObjectDetailsHtmlGroup) } # # Diagnose Issues # if ($title -eq $global:DiagnoseObjectSyncIssues) { if ($adCsObject -ne $null) { if (IsObjectTypeMatch($adObject[0])("user")) { DiagnoseUserObject($adConnector)($adObject[0])($adCsObject)($mvObject)($aadCsObject)($aadTenantUser)($aadTenantDefaultDomainName)($aadTenantDomains) } elseif(IsObjectTypeMatch($adObject[0])("group")) { DiagnoseGroupObject($adConnector)($adObject[0])($adCsObject)($mvObject)($aadCsObject)($aadTenantGroup) } elseif(IsObjectTypeMatch($adObject[0])("foreignSecurityPrincipal")) { DiagnoseFSP($adConnector)($adObject[0])($adCsObject)($mvObject)($aadCsObject) } elseif (IsObjectTypeMatch($adObject[0])("contact")) { } else { "This diagnostic option is currently only supported on user, contact, group and foreignSecurityPrincipal objects." | ReportError Write-Host "`r`n" } } if ($SynchronizationIssueHtmlItems.Count -eq 0) { Write-Host "`r`n" "We couldn't identify the issue with this object, please contact customer support if you need assistance, or read the AADConnect documentation at `"$global:TroubleshootingTaskUrl`"" | ReportWarning } } elseif ($title -eq $global:ChangePrimaryEmailAddress) { if (IsObjectTypeMatch($adObject[0])("user")) { DiagnoseUserObjectProxyAddresses } else { "This diagnostic option is currently only supported on user objects." | ReportError Write-Host "`r`n" } } elseif ($title -eq $global:HideFromGlobalAddressList) { if ($adCsObject -ne $null) { if (IsObjectTypeMatch($adObject[0])("user")) { DiagnoseUserObjectHideFromAddressLists($AdConnector)($AdObject[0])($AdCsObject) } else { "This diagnostic option is currently only supported on user objects." | ReportError Write-Host "`r`n" } } } # # Set output directory for per-object html report # Set-OutputDirectory # # Per-object html report date-time # $reportDate = [string] $(Get-Date -Format yyyyMMddHHmmss) if ($HtmlGroupList.Count -gt 0) { if ($SynchronizationIssueHtmlItems.Count -gt 0) { $synchronizationIssuesHtmlGroup = WriteHtmlAccordionGroup($SynchronizationIssueHtmlItems)($global:HtmlSynchronizationIssuesSectionTitle) $HtmlGroupList.Insert(0, $synchronizationIssuesHtmlGroup) } $objectSyncDiagnosticsHtmlContent = WriteHtmlAccordion($HtmlGroupList)($ObjectDN) $objectSyncDiagnosticsHtmlBody = WriteHtmlBody($objectSyncDiagnosticsHtmlContent) $objectSyncDiagnosticsHtml = WriteHtml($objectSyncDiagnosticsHtmlBody) Write-Host "`r`n" Export-ObjectDiagnosticsHtmlReport -Title $ObjectDN -ReportDate $reportDate -HtmlDoc $objectSyncDiagnosticsHtml $SynchronizationIssueHtmlItems.Clear() $HtmlGroupList.Clear() } Write-Host "`r`n" Write-Host "`r`n" # # Ask if the tool is helpful for the synchronization issues # # The customer is going to answer for each synchronization issue separately # AskIfToolHelpful($ObjectDN) $SynchronizationIssueList.Clear() } Function Debug-ADSyncObjectAttributeRetrievalIssues { param ( [string] [parameter(mandatory=$false)] $ADConnectorName, [string] [parameter(mandatory=$false)] $ObjectDN ) if ($isNonInteractiveMode) { return } Write-Host "`r`n" # # Write Title to the PowerShell Console # WriteTitle($global:ADConnectorAccountReadPermissions) $adConnector = GetADConnectorByName($ADConnectorName)("Please enter AD Connector Name") if ($adConnector -eq $null) { "There is no AD Connector with name `"$ADConnectorName`"." | ReportError Write-Host "`r`n" return } $ObjectDN = GetObjectDN($ObjectDN)("Please enter AD object Distinguished Name") $adObject = Search-ADSyncDirectoryObjects -AdConnectorId $adConnector.Identifier -LdapFilter "(distinguishedName=$ObjectDN)" -SearchScope Subtree -SizeLimit 1 if ($adObject -eq $null -or $adObject.Count -lt 1) { "Could not find an object in on-premises AD with distinguishedName=`"$ObjectDN`"." | ReportError Write-Host "`r`n" return } $ADForest = $adConnector.ConnectivityParameters["forest-name"].Value $ADConnectorAccountName = "$($adConnector.ConnectivityParameters['forest-login-domain'].Value)\$($adConnector.ConnectivityParameters['forest-login-user'].Value)" $ADForestCredential = GetValidADForestCredentials($ADForest) if ($ADForestCredential -eq $null) { "No valid credentials were provided for forest with name=`"$ADForest`"." | ReportError Write-Host "`r`n" return } WriteEventLog($EventIdConnectorAccountReadPermissions)($EventMsgConnectorAccountReadPermissions -f ($ADConnectorName, $ObjectDN, $ADConnectorAccountName, $ADForestCredential.UserName)) $adObjectFromProvidedAccount = Search-ADSyncDirectoryObjects -AdConnectorId $adConnector.Identifier -LdapFilter "(distinguishedName=$ObjectDN)" -SearchScope Subtree -SizeLimit 1 -AdConnectorCredential $ADForestCredential -ForestFqdn $ADForest $objectAttributesFromConnectorAccount = [System.Collections.Generic.Dictionary[[String], [Object]]] $adObject[0] $objectAttributesFromProvidedAccount = [System.Collections.Generic.Dictionary[[String], [Object]]] $adObjectFromProvidedAccount[0] $attributeDetailsHtmlGroup = GetObjectAllADAttributeHtmlGroup($objectAttributesFromConnectorAccount)($objectAttributesFromProvidedAccount)($ADConnectorAccountName)($ADForestCredential.UserName) $HtmlGroupList.Insert(0, $attributeDetailsHtmlGroup) # # Set output directory for per-object html report # Set-OutputDirectory # # Per-object html report date-time # $reportDate = [string] $(Get-Date -Format yyyyMMddHHmmss) $objectSyncDiagnosticsHtmlContent = WriteHtmlAccordion($HtmlGroupList)($ObjectDN) $objectSyncDiagnosticsHtmlBody = WriteHtmlBody($objectSyncDiagnosticsHtmlContent) $objectSyncDiagnosticsHtml = WriteHtml($objectSyncDiagnosticsHtmlBody) Write-Host "`r`n" Export-ObjectDiagnosticsHtmlReport -Title $ObjectDN -ReportDate $reportDate -HtmlDoc $objectSyncDiagnosticsHtml $HtmlGroupList.Clear() Write-Host "`r`n" Write-Host "`r`n" } Function Debug-ADSyncGroupMembershipSynchronizationIssues { param ( [string] [parameter(mandatory=$false)] $GroupADConnectorName, [string] [parameter(mandatory=$false)] $GroupDN, [string] [parameter(mandatory=$false)] $MemberADConnectorName, [string] [parameter(mandatory=$false)] $MemberDN ) WriteEventLog($EventIdGroupMembershipTroubleshootingRun)($EventMsgGroupMembershipTroubleshootingRun) Write-Host "`r`n" # # Write Title to the PowerShell Console # WriteTitle($global:DiagnoseGroupMembershipSyncIssues) # # Get AD Connector Name for the group # $adConnectorGroup = GetADConnectorByName($GroupADConnectorName)("Please enter AD Connector Name for the group") if ($adConnectorGroup -eq $null) { "There is no AD Connector with name `"$GroupADConnectorName`"." | ReportError Write-Host "`r`n" return } $GroupADConnectorName = $adConnectorGroup.Name $GroupDN = GetObjectDN($GroupDN)("Please enter group Distinguished Name") # # Validate GroupDN parameter # $adGroupObject = Search-ADSyncDirectoryObjects -AdConnectorId $adConnectorGroup.Identifier -LdapFilter "(distinguishedName=$GroupDN)" -SearchScope Subtree -SizeLimit 1 if ($adGroupObject -eq $null -or $adGroupObject.Count -lt 1) { "Could not find an object in on-premises AD with distinguishedName=`"$GroupDN`"." | ReportError Write-Host "`r`n" return } # # Validating that the object is a group # $isGroup = IsObjectTypeMatch($adGroupObject[0])("group") if ($isGroup -eq $false) { "Given object with distinguishedName=`"$GroupDN`" is not a group." | ReportError Write-Host "`r`n" return } # # Active Directory Connector Partition Details for group # $partitionDNGroup = Get-DirectoryPartitionDN($GroupDN) $partitionGroup = Get-ADSyncConnectorPartition -Connector $adConnectorGroup -Name $partitionDNGroup # # Check Domain/OU Filtering for group # CheckDomainBasedFiltering($partitionGroup)($adConnectorGroup)($GroupDN) CheckOUBasedFiltering($partitionGroup)($GroupDN) # # Get AD Connector Name for the group member # $adConnectorMember = GetADConnectorByName($MemberADConnectorName)("Please enter AD Connector Name for the group member") if ($adConnectorMember -eq $null) { "There is no AD Connector with name `"$MemberADConnectorName`"." | ReportError Write-Host "`r`n" return } $MemberADConnectorName = $adConnectorMember.Name $MemberDN = GetObjectDN($MemberDN)("Please enter Distinguished Name for the group member") # # Validate MemberDN parameter # $adMemberObject = Search-ADSyncDirectoryObjects -AdConnectorId $adConnectorMember.Identifier -LdapFilter "(distinguishedName=$MemberDN)" -SearchScope Subtree -SizeLimit 1 if ($adMemberObject -eq $null -or $adMemberObject.Count -lt 1) { "Could not find an object in on-premises AD with distinguishedName=`"$MemberDN`"." | ReportError Write-Host "`r`n" return } # # Active Directory Connector Partition Details for group member # $partitionDNMember = Get-DirectoryPartitionDN($MemberDN) $partitionMember = Get-ADSyncConnectorPartition -Connector $adConnectorMember -Name $partitionDNMember # # Check Domain/OU Filtering for group member # CheckDomainBasedFiltering($partitionMember)($adConnectorMember)($MemberDN) CheckOUBasedFiltering($partitionMember)($MemberDN) # # Foreign Security Principal # $isForeignSecurityPrincipal = $false if ($GroupADConnectorName -ne $MemberADConnectorName) { $isForeignSecurityPrincipal = $true $memberSidBinary = $adMemberObject[0]["objectsid"] $memberSidString = (New-Object System.Security.Principal.SecurityIdentifier($memberSidBinary[0], 0)).Value $MemberDN = "CN=$memberSidString,CN=ForeignSecurityPrincipals,$partitionDNGroup" $adMemberObject = Search-ADSyncDirectoryObjects -AdConnectorId $adConnectorGroup.Identifier -LdapFilter "(distinguishedName=$MemberDN)" -SearchScope Subtree -SizeLimit 1 if ($adMemberObject -eq $null -or $adMemberObject.Count -lt 1) { "Could not find an object in on-premises AD with distinguishedName=`"$MemberDN`"." | ReportError Write-Host "`r`n" return } else { "Foreign Security Principal `"$memberSidString`" exists in Active Directory" | ReportOutput Write-Host "`r`n" } # # Check OU Filtering for group member which is a foreign security principal # CheckOUBasedFiltering($partitionGroup)($MemberDN) } # Validate if member reference is present in AD $isMemberInAD = IsMemberOfGroupInAD($adGroupObject[0])($MemberDN) if (-not $isMemberInAD) { "Object `"$MemberDN`" is not a member of group `"$GroupDN`" in Active Directory" | ReportError Write-Host "`r`n" return } # # Get object graph for group # $adCsObjectGroup = $null $mvObjectGroup = $null $aadCsObjectGroup = $null $objectGraphGroup = CheckObjectGraph($GroupADConnectorName)($GroupDN)([ref]$adCsObjectGroup)([ref]$mvObjectGroup)([ref]$aadCsObjectGroup) # # Check attribute based filtering for group # CheckAttributeBasedFiltering($GroupADConnectorName)($GroupDN)($adCsObjectGroup)($mvObjectGroup)($aadCsObjectGroup) # Check for Sync Errors and Export Errors on group object if ((ReportSyncError($adCsObjectGroup)) -or (ReportSyncError($aadCsObjectGroup))) { ReportOutput -PropertyName "Sync Error(s) on group" -PropertyValue "True" } if ((ReportExportError($adCsObjectGroup)) -or (ReportExportError($aadCsObjectGroup))) { ReportOutput -PropertyName "Export Error(s) on group" -PropertyValue "True" } # Check for transient on group object if ((ReportTransient($adCsObjectGroup)) -or (ReportTransient($aadCsObjectGroup))) { ReportOutput -PropertyName "Group - Transient Object" -PropertyValue "True" } # # Check if group has synchronization issues due to "Maximum group member limit exceeded" # if ($adCsObjectGroup.SerializedXml -contains "Maximum Group member count exceeded" -or $aadCsObjectGroup.SerializedXml -contains "Maximum Group member count exceeded") { "Maximum group member count limit has been exceeded" | ReportError Write-Host "`r`n" } # # Get object graph for member # $adCsObjectMember = $null $mvObjectMember = $null $aadCsObjectMember = $null $objectGraphMember = CheckObjectGraph($GroupADConnectorName)($MemberDN)([ref]$adCsObjectMember)([ref]$mvObjectMember)([ref]$aadCsObjectMember) # # Check attribute based filtering for member # CheckAttributeBasedFiltering($GroupADConnectorName)($MemberDN)($adCsObjectMember)($mvObjectMember)($aadCsObjectMember) # Check for Sync Errors and Export Errors on group member object if ((ReportSyncError($adCsObjectMember)) -or (ReportSyncError($aadCsObjectMember))) { ReportOutput -PropertyName "Sync Error(s) on group member" -PropertyValue "True" } if ((ReportExportError($adCsObjectMember)) -or (ReportExportError($aadCsObjectMember))) { ReportOutput -PropertyName "Export Error(s) on group member" -PropertyValue "True" } # Check for transient on group member object if ((ReportTransient($adCsObjectMember)) -or (ReportTransient($aadCsObjectMember))) { ReportOutput -PropertyName "Group Member - Transient Object" -PropertyValue "True" } # # Validate member reference exists in AD CS # "Checking for group membership in sync engine..." | Write-Host -fore White $isMemberInAdCS = IsMemberOfGroupInCS($adCsObjectGroup)($adCsObjectMember) if (-not $isMemberInAdCS) { "Object `"$MemberDN`" is not a member of group `"$GroupDN`" in AD connector space" | ReportError } else { "Object `"$MemberDN`" is a member of group `"$GroupDN`" in AD connector space" | ReportOutput } # # Validate member reference exists in MV # $isMemberInMV = IsMemberOfGroupInMV($mvObjectGroup)($mvObjectMember) if (-not $isMemberInMV) { "Object `"$MemberDN`" is not a member of group `"$GroupDN`" in Metaverse" | ReportError } else { "Object `"$MemberDN`" is a member of group `"$GroupDN`" in Metaverse" | ReportOutput } # # Validate member reference exists in AAD CS # $isMemberInAadCS = IsMemberOfGroupInCS($aadCsObjectGroup)($aadCsObjectMember) if (-not $isMemberInAadCS) { "Object `"$MemberDN`" is not a member of group `"$GroupDN`" in AAD connector space" | ReportError } else { "Object `"$MemberDN`" is a member of group `"$GroupDN`" in AAD connector space" | ReportOutput } # # Set output directory for per-object html report # Set-OutputDirectory # # Per-object html report date-time # $reportDate = [string] $(Get-Date -Format yyyyMMddHHmmss) if ($SynchronizationIssueHtmlItems.Count -gt 0) { $synchronizationIssuesHtmlGroup = WriteHtmlAccordionGroup($SynchronizationIssueHtmlItems)($global:HtmlSynchronizationIssuesSectionTitle) $HtmlGroupList.Insert(0, $synchronizationIssuesHtmlGroup) $SynchronizationIssueHtmlItems.Clear() } if ($HtmlGroupList.Count -gt 0) { $objectSyncDiagnosticsHtmlContent = WriteHtmlAccordion($HtmlGroupList)($GroupDN) $objectSyncDiagnosticsHtmlBody = WriteHtmlBody($objectSyncDiagnosticsHtmlContent) $objectSyncDiagnosticsHtml = WriteHtml($objectSyncDiagnosticsHtmlBody) Write-Host "`r`n" Export-ObjectDiagnosticsHtmlReport -Title $GroupDN -ReportDate $reportDate -HtmlDoc $objectSyncDiagnosticsHtml $HtmlGroupList.Clear() } Write-Host "`r`n" Write-Host "`r`n" # # Ask if the tool is helpful for the synchronization issues # # The customer is going to answer for each synchronization issue separately # AskIfToolHelpful($GroupDN) $SynchronizationIssueList.Clear() } Function ReportTransient { param ( [Microsoft.IdentityManagement.PowerShell.ObjectModel.CsObject] [parameter(mandatory=$false)] [AllowNull()] $AdCSObject ) if ($AdCSObject -ne $null -and $AdCSObject.IsTransient) { $AdCSObjectDN = $AdCSObject.DistinguishedName "Object `"$AdCSObjectDN`" is a transient object" | ReportWarning return $true } return $false } Function ReportSyncError { param ( [Microsoft.IdentityManagement.PowerShell.ObjectModel.CsObject] [parameter(mandatory=$false)] [AllowNull()] $AdCSObject ) if ($AdCSObject -ne $null -and $AdCSObject.HasSyncError) { $AdCSObjectDN = $AdCSObject.DistinguishedName "Object `"$AdCSObjectDN`" has synchronization error(s)" | ReportError if ($AdCsObject.ExportedChangedNotReimported) { "Object `"$AdCSObjectDN`" has exported-change-not-reimported error. This means that the changes exported to the connected data source is being overriden." | ReportError } return $true } return $false } Function ReportExportError { param ( [Microsoft.IdentityManagement.PowerShell.ObjectModel.CsObject] [parameter(mandatory=$false)] [AllowNull()] $AdCSObject ) if ($AdCSObject -ne $null -and $AdCSObject.HasExportError) { $AdCSObjectDN = $AdCSObject.DistinguishedName "Object `"$AdCSObjectDN`" has export error(s)" | ReportError return $true } return $false } Function GetObjectDN { param ( [string] [parameter(mandatory=$false)] $ObjectDN, [string] [parameter(mandatory=$false)] $PromptMessage ) while ([string]::IsNullOrEmpty($ObjectDN)) { $ObjectDN = Read-Host $PromptMessage } return $ObjectDN } Function CheckObjectGraph { param ( [string] [parameter(mandatory=$true)] $ADConnectorName, [string] [parameter(mandatory=$true)] $ObjectDN, [parameter(mandatory=$false)] [ref]$AdCSObject, [parameter(mandatory=$false)] [ref]$MVObject, [parameter(mandatory=$false)] [ref]$AadCSObject ) "Checking for object `"$ObjectDN`" in sync engine..." | Write-Host -fore White # # Get object in the AD connector space # $AdCSObject.Value = GetCSObject($ADConnectorName)($ObjectDN) if ($AdCSObject.Value -eq $null) { "Object `"$ObjectDN`" is not found in AD Connector Space - `"$ADConnectorName`"" | ReportError return $false } else { "Object `"$ObjectDN`" is found in AD Connector Space - `"$ADConnectorName`"" | ReportOutput } # # Get object in the metaverse # if ($AdCSObject.Value -ne $null -and $AdCSObject.Value.ConnectedMVObjectId -ne [System.Guid]::Empty) { $MVObject.Value = GetMVObjectByIdentifier($AdCSObject.Value.ConnectedMVObjectId) } if ($MVObject.Value -eq $null) { "Object `"$ObjectDN`" is not found in Metaverse" | ReportError } else { "Object `"$ObjectDN`" is found in Metaverse" | ReportOutput } # # Get joined AD connector space objects # $joinedAdCsObjects = New-Object System.Collections.ArrayList if ($MVObject.Value -ne $null) { $links = $MVObject.Value.Lineage $joinedAdCsObjectLinks = $links.Where( {$_.ConnectedCsObjectId -ne $AdCSObject.Value.ObjectId -and $_.ConnectorId -ne "b891884f-051e-4a83-95af-2544101c9083"}) $joinedAdCsObjectLinks | ForEach { $joinedAdCsObjectDN = $_.ConnectedCsObjectDN $joinedAdCsObjectConnectorName = $_.ConnectorName $joinedAdCsObject = GetCSObjectByIdentifier($_.ConnectedCsObjectId) if ($joinedAdCsObject -eq $null) { "Could not find joined object `"$joinedAdCsObjectDN`" in the AD Connector Space `"$joinedAdCsObjectConnectorName`"" | ReportError } else { "Object `"$ObjectDN`" is joined to object `"$joinedAdCsObjectDN`" in the Metaverse." | ReportOutput $joinedAdCsObjects.Add($joinedAdCsObject) } } } # # Get joined AD objects # $joinedAdObjects = New-Object System.Collections.ArrayList if ($joinedAdCsObjects.Count -gt 0) { $joinedAdCsObjects | ForEach { $joinedAdCsObjectDN = $_.DistinguishedName $joinedAdCsObjectConnectorName = $_.ConnectorName $joinedAdObject = Search-ADSyncDirectoryObjects -AdConnectorId $_.ConnectorId -LdapFilter "(distinguishedName=$joinedAdCsObjectDN)" -SearchScope Subtree -SizeLimit 1 if ($joinedAdObject -eq $null -or $joinedAdObject.Count -lt 1) { "Could not find joined object `"$joinedAdCsObjectDN`" in on-premises AD `"$joinedAdCsObjectConnectorName`"." | ReportError } else { $joinedAdObjects.Add($joinedAdObject[0]) } } } # # Get object in AAD connector space # if ($MVObject.Value -ne $null) { $aadConnector = GetAADConnector if ($aadConnector -ne $null) { $aadCsObjectId = GetTargetCSObjectId($MVObject.Value)($aadConnector.Identifier) if ($aadCsObjectId -ne $null) { $AadCSObject.Value = GetCSObjectByIdentifier($aadCsObjectId) } } } if ($AadCSObject.Value -eq $null) { "Object `"$ObjectDN`" is not found in AAD Connector Space" | ReportError } else { "Object `"$ObjectDN`" is found in AAD Connector Space" | ReportOutput } Write-Host "`r`n" if ($AdCsObject.Value -ne $null -and $MVObject.Value -ne $null -and $AadCsObject.Value -ne $null) { return $true } return $false } Function GetAADDomains { if ($isNonInteractiveMode) { return $null } return Get-MsolDomain } Function ConnectToAAD { if ($isNonInteractiveMode) { return } # # Get AAD Tenant credentials if not specified as an input parameter # if ($global:AADTenantCredential -eq $null) { $global:AADTenantCredential = Get-Credential -Message "Please enter Azure AD Tenant global administrator or hybrid identity administrator credentials:" } Write-Host "`r`n" # # Connect to AAD Tenant # try { "Connecting to Azure AD Tenant..." | Write-Host -fore White Connect-MsolService -Credential $global:AADTenantCredential -ErrorAction Stop "OK" | Write-Host -fore Green Write-Host "`r`n" } catch { $global:AADTenantCredential = $null "Failed to connect to AAD Tenant. Details: $($_.Exception.Message)" | Write-Host -fore Red Write-Host "`r`n" return } } Function CheckDomainBasedFiltering { param ( [Microsoft.IdentityManagement.PowerShell.ObjectModel.ConnectorPartition] [parameter(mandatory=$false)] $Partition, [Microsoft.IdentityManagement.PowerShell.ObjectModel.Connector] [parameter(mandatory=$true)] $AdConnector, [string] [parameter(mandatory=$true)] $ObjectDN ) if ($Partition -eq $null) { return } $htmlMessageList = New-Object System.Collections.Generic.List[string] "Checking Domain Filtering configuration..." | Write-Host $partitionDN = $Partition.DN $adConnectorName = $AdConnector.Name if (-Not $Partition.Selected) { WriteEventLog($EventIdDomainFiltered)($EventMsgDomainFiltered -f ($ObjectDN, $partitionDN)) $SynchronizationIssueList.Add($global:DomainFilteringIssue) Write-Host "`r`n" "DOMAIN FILTERING - ANALYSIS:" | Write-Host -fore Cyan "----------------------------" | Write-Host -fore Cyan $htmlMessage = WriteHtmlMessage -message "Analysis:" -color "#252525" -fontSize "16px" -fontWeight "700" -numberOfLineBreaks 1 $htmlMessageList.Add($htmlMessage) $consoleMessage = "Object is not present in sync scope. Object belongs to domain $partitionDN which is filtered from synchronization." $consoleMessage | ReportError -PropertyName "Domain Filtered" -PropertyValue "True" Write-Host "`r`n" $htmlMessage = WriteHtmlMessage -message $consoleMessage -color "#252525" -numberOfLineBreaks 2 $htmlMessageList.Add($htmlMessage) "DOMAIN FILTERING - RECOMMENDED ACTIONS:" | Write-Host -fore Cyan "---------------------------------------" | Write-Host -fore Cyan $htmlMessage = WriteHtmlMessage -message "Recommended Actions:" -color "#252525" -fontSize "16px" -fontWeight "700" -numberOfLineBreaks 1 $htmlMessageList.Add($htmlMessage) $message = "Include the partition $partitionDN in the list of domains that should be synced. To read more on how to do this, please see:" $consoleMessage = GetConsoleMessageWithLink($message)($global:DomainBasedFilteringUrl) $consoleMessage | Write-Host -fore Yellow Write-Host "`r`n" $htmlMessage = GetHtmlMessageWithLink($message)($global:DomainBasedFilteringUrl)($global:DomainBasedFilteringText) $htmlMessage = WriteHtmlMessage -message $htmlMessage -color "#252525" -numberOfLineBreaks 0 $htmlMessageList.Add($htmlMessage) $domainFilteringHtmlItem = WriteHtmlAccordionItemForParagraph($htmlMessageList)("Domain Filtering") $SynchronizationIssueHtmlItems.Add($domainFilteringHtmlItem) return } $runProfiles = $AdConnector.RunProfiles if ($runProfiles -eq $null) { WriteEventLog($EventIdDomainFiltered)($EventMsgDomainFiltered -f ($ObjectDN, $partitionDN)) $SynchronizationIssueList.Add($global:DomainFilteringIssue) Write-Host "`r`n" "DOMAIN FILTERING - ANALYSIS:" | Write-Host -fore Cyan "----------------------------" | Write-Host -fore Cyan $htmlMessage = WriteHtmlMessage -message "Analysis:" -color "#252525" -fontSize "16px" -fontWeight "700" -numberOfLineBreaks 1 $htmlMessageList.Add($htmlMessage) $consoleMessage = "No run profiles are configured on the AD Connector $adConnectorName." $consoleMessage | ReportError -PropertyName "Domain Filtered" -PropertyValue "True" Write-Host "`r`n" $htmlMessage = WriteHtmlMessage -message $consoleMessage -color "#252525" -numberOfLineBreaks 2 $htmlMessageList.Add($htmlMessage) "DOMAIN FILTERING - RECOMMENDED ACTIONS:" | Write-Host -fore Cyan "---------------------------------------" | Write-Host -fore Cyan $htmlMessage = WriteHtmlMessage -message "Recommended Actions:" -color "#252525" -fontSize "16px" -fontWeight "700" -numberOfLineBreaks 1 $htmlMessageList.Add($htmlMessage) $message = "Update the run profiles to include the partition $partitionDN that should be synced. To read more on how to do this, please see:" $consoleMessage = GetConsoleMessageWithLink($message)($global:DomainBasedFilteringUrl) $consoleMessage | Write-Host -fore Yellow Write-Host "`r`n" $htmlMessage = GetHtmlMessageWithLink($message)($global:DomainBasedFilteringUrl)($global:DomainBasedFilteringText) $htmlMessage = WriteHtmlMessage -message $htmlMessage -color "#252525" -numberOfLineBreaks 0 $htmlMessageList.Add($htmlMessage) $domainFilteringHtmlItem = WriteHtmlAccordionItemForParagraph($htmlMessageList)("Domain Filtering") $SynchronizationIssueHtmlItems.Add($domainFilteringHtmlItem) return } $runProfilesMissingStep = @() foreach ($runProfile in $runProfiles) { $runSteps = $runProfile.RunSteps if ($runSteps -eq $null) { $runProfilesMissingStep += $runProfile.Name continue } $foundRunStep = $false foreach ($runStep in $runSteps) { if ($runStep.PartitionIdentifier -eq $Partition.Identifier) { $foundRunStep = $true break } } if (-Not $foundRunStep) { $runProfilesMissingStep += $runProfile.Name } } if ($runProfilesMissingStep.Count -gt 0) { WriteEventLog($EventIdDomainFiltered)($EventMsgDomainFiltered -f ($ObjectDN, $partitionDN)) $SynchronizationIssueList.Add($global:DomainFilteringIssue) $runProfilesMissingStepOutput = $runProfilesMissingStep -join "," Write-Host "`r`n" "DOMAIN FILTERING - ANALYSIS:" | Write-Host -fore Cyan "----------------------------" | Write-Host -fore Cyan $htmlMessage = WriteHtmlMessage -message "Analysis:" -color "#252525" -fontSize "16px" -fontWeight "700" -numberOfLineBreaks 1 $htmlMessageList.Add($htmlMessage) $consoleMessage = "No run steps are configured for directory partition $partitionDN for some run profile(s), namely: $runProfilesMissingStepOutput." $consoleMessage | ReportError -PropertyName "Domain Filtered" -PropertyValue "True" Write-Host "`r`n" $htmlMessage = WriteHtmlMessage -message $consoleMessage -color "#252525" -numberOfLineBreaks 2 $htmlMessageList.Add($htmlMessage) "DOMAIN FILTERING - RECOMMENDED ACTIONS:" | Write-Host -fore Cyan "---------------------------------------" | Write-Host -fore Cyan $htmlMessage = WriteHtmlMessage -message "Recommended Actions:" -color "#252525" -fontSize "16px" -fontWeight "700" -numberOfLineBreaks 1 $htmlMessageList.Add($htmlMessage) $message = "Add steps to the above run profile(s) to include the partition $partitionDN. To read more on how to do this, please see:" $consoleMessage = GetConsoleMessageWithLink($message)($global:DomainBasedFilteringUrl) $consoleMessage | Write-Host -fore Yellow Write-Host "`r`n" $htmlMessage = GetHtmlMessageWithLink($message)($global:DomainBasedFilteringUrl)($global:DomainBasedFilteringText) $htmlMessage = WriteHtmlMessage -message $htmlMessage -color "#252525" -numberOfLineBreaks 0 $htmlMessageList.Add($htmlMessage) $domainFilteringHtmlItem = WriteHtmlAccordionItemForParagraph($htmlMessageList)("Domain Filtering") $SynchronizationIssueHtmlItems.Add($domainFilteringHtmlItem) } else { "There is no domain level filtering configuration that prevents this object from being imported in the AD connector space." | Write-Host Write-Host "`r`n" ReportOutput -PropertyName "Domain Filtered" -PropertyValue "False" } } Function Get-DirectoryPartitionDN { param ( [string] [parameter(mandatory=$true)] [ValidateNotNullOrEmpty] $ObjectDN ) $index = $ObjectDN.IndexOf("DC=") return $ObjectDN.Substring($index) } Function DiagnoseGroupObject { param ( [Microsoft.IdentityManagement.PowerShell.ObjectModel.Connector] [parameter(mandatory=$true)] $AdConnector, [System.Collections.Generic.Dictionary[[String], [Object]]] [parameter(mandatory=$true)] [AllowNull()] $AdObject, [Microsoft.IdentityManagement.PowerShell.ObjectModel.CsObject] [parameter(mandatory=$true)] [AllowNull()] $AdCsObject, [Microsoft.IdentityManagement.PowerShell.ObjectModel.MvObject] [parameter(mandatory=$true)] [AllowNull()] $MvObject, [Microsoft.IdentityManagement.PowerShell.ObjectModel.CsObject] [parameter(mandatory=$true)] [AllowNull()] $AadCsObject, [Microsoft.Online.Administration.Group] [parameter(mandatory=$true)] [AllowNull()] $AadGroupObject ) WriteEventLog($EventIdDiagnoseSingleObject)($EventMsgDiagnoseSingleObject) "Checking group member count limitation..." | Write-Verbose # check member count if ($AdCsObject -ne $null -and $AdCsObject.HasSyncError -and $AdCsObject.SerializedXml -contains "Maximum Group member count exceeded") { $htmlMessage = WriteHtmlMessage -message "Analysis:" -color "#252525" -fontSize "16px" -fontWeight "700" -numberOfLineBreaks 1 $htmlMessageList.Add($htmlMessage) $consoleMessage = "For the given group object the maximum member count limit has been exceeded. Member attribute is not going to be synchronized to Azure Active Directory." $consoleMessage | ReportError -PropertyName "Maximum member count limit exceeded" -PropertyValue "True" Write-Host "`r`n" $htmlMessage = WriteHtmlMessage -message $consoleMessage -color "#252525" -numberOfLineBreaks 2 $htmlMessageList.Add($htmlMessage) $groupMemberCountHtmlItem = WriteHtmlAccordionItemForParagraph($htmlMessageList)("Group Member Count") $SynchronizationIssueHtmlItems.Add($groupMemberCountHtmlItem) } else { "OK" | Write-Verbose } } Function DiagnoseFSP { param ( [Microsoft.IdentityManagement.PowerShell.ObjectModel.Connector] [parameter(mandatory=$true)] $AdConnector, [System.Collections.Generic.Dictionary[[String], [Object]]] [parameter(mandatory=$true)] [AllowNull()] $AdObject, [Microsoft.IdentityManagement.PowerShell.ObjectModel.CsObject] [parameter(mandatory=$true)] [AllowNull()] $AdCsObject, [Microsoft.IdentityManagement.PowerShell.ObjectModel.MvObject] [parameter(mandatory=$true)] [AllowNull()] $MvObject, [Microsoft.IdentityManagement.PowerShell.ObjectModel.CsObject] [parameter(mandatory=$true)] [AllowNull()] $AadCsObject ) WriteEventLog($EventIdDiagnoseSingleObject)($EventMsgDiagnoseSingleObject) if ($AdCsObject -ne $null -and $MvObject -eq $null) { $htmlMessage = WriteHtmlMessage -message "Analysis:" -color "#252525" -fontSize "16px" -fontWeight "700" -numberOfLineBreaks 1 $htmlMessageList.Add($htmlMessage) $consoleMessage = "FSP is unable to find an exisitng metaverse object to join to. This typically happens if the primary object is not synchronized." $consoleMessage | ReportError Write-Host "`r`n" } } Function DiagnoseUserObject { param ( [Microsoft.IdentityManagement.PowerShell.ObjectModel.Connector] [parameter(mandatory=$true)] $AdConnector, [System.Collections.Generic.Dictionary[[String], [Object]]] [parameter(mandatory=$true)] [AllowNull()] $AdObject, [Microsoft.IdentityManagement.PowerShell.ObjectModel.CsObject] [parameter(mandatory=$true)] [AllowNull()] $AdCsObject, [Microsoft.IdentityManagement.PowerShell.ObjectModel.MvObject] [parameter(mandatory=$true)] [AllowNull()] $MvObject, [Microsoft.IdentityManagement.PowerShell.ObjectModel.CsObject] [parameter(mandatory=$true)] [AllowNull()] $AadCsObject, [Microsoft.Online.Administration.User] [parameter(mandatory=$true)] [AllowNull()] $AadUserObject, [string] [parameter(mandatory=$true)] [AllowNull()] [AllowEmptyString()] $AadTenantDefaultDomainName, [Microsoft.Online.Administration.Domain[]] [parameter(mandatory=$true)] [AllowNull()] $AadTenantDomains ) WriteEventLog($EventIdDiagnoseSingleObject)($EventMsgDiagnoseSingleObject) # # Diagnose linked mailbox issues # if ($AdCsObject -and $MvObject) { DiagnoseUserObjectLinkedMailBox($AdCsObject)($MvObject) } if ($AadUserObject -eq $null) { return } "Checking UserPrincipalName (UPN) Mismatch..." | Write-Host -fore White # # Get "userPrincipalName" attribute value of the metaverse object # $userPrincipalNameAttribute = GetMVObjectAttribute($MvObject)("userPrincipalName") $userPrincipalNameValue = $null if ($userPrincipalNameAttribute -ne $null) { $userPrincipalNameValue = $userPrincipalNameAttribute.Values[0] } else { "Failed to get `"userPrincipalName`" attribute value of AADConnect object." | Write-Host -fore Red Write-Host "`r`n" return } # # Check if there is a mismatch between MV object UPN and AAD Tenant object UPN # if ($userPrincipalNameValue -ne $AadUserObject.UserPrincipalName) { Write-Host "`r`n" "USERPRINCIPALNAME MISMATCH - ANALYSIS:" | Write-Host -fore Cyan "--------------------------------------" | Write-Host -fore Cyan "For the given user object there is a `"userPrincipalName`" attribute value mismatch between AADConnect and AAD Tenant." | Write-Host -fore Yellow Write-Host "`r`n" DiagnoseUPNMismatch($MvObject)($AadUserObject)($AadTenantDefaultDomainName)($AadTenantDomains) } else { "OK" | Write-Host -fore Green Write-Host "`r`n" } } # # Provisioning a user object having exchange recipient type as "Linked Mailbox" results in a metaverse object with NULL source anchor. # By default, a metaverse object with NULL source anchor does NOT flow into AAD connector space. # # The exchange recipient type "Linked Mailbox" is specified with the on-premises AD attribute "msExchRecipientTypeDetails" having value 2. # # In order not to end up with a metaverse object having NULL source anchor: # # 1- There should be an active account (not disabled, userAccountControl != 2) in another account forest which joins into the same metaverse object. # So that source anchor would flow from the active account. # # 2- If there is no active account joining into the same metaverse object, then the customer should consider converting the "Linked Mailbox" to a "User Mailbox". # The exchange recipient type "User Mailbox" is specified with the on-premises AD attribute "msExchRecipientTypeDetails" having value 1. # Function DiagnoseUserObjectLinkedMailBox { param ( [Microsoft.IdentityManagement.PowerShell.ObjectModel.CsObject] [parameter(mandatory=$true)] $AdCsObject, [Microsoft.IdentityManagement.PowerShell.ObjectModel.MvObject] [parameter(mandatory=$true)] $MvObject ) $htmlMessageList = New-Object System.Collections.Generic.List[string] # # Get AD connector space object attribute "msExchRecipientTypeDetails". # $msExchRecipientTypeDetailsAttribute = GetCSObjectAttribute($AdCsObject)("msExchRecipientTypeDetails") $msExchRecipientTypeDetailsValue = $null if ($msExchRecipientTypeDetailsAttribute -ne $null) { $msExchRecipientTypeDetailsValue = $msExchRecipientTypeDetailsAttribute.Values[0] } else { # # Attribute "msExchRecipientTypeDetails" is not imported from on-premises AD. # return } # # Exchage recipient type is NOT "Linked Mailbox" # $msExchRecipientTypeDetailsValueInt=0 if (([System.Int32]::TryParse($msExchRecipientTypeDetailsValue, [ref] $msExchRecipientTypeDetailsValueInt)) -or ($msExchRecipientTypeDetailsValueInt -ne 2)) { return } "Checking Linked Mailbox related issues..." | Write-Host -fore White # # Get metaverse object attribute "sourceAnchor" # $sourceAnchorAttribute = GetMVObjectAttribute($MvObject)("sourceAnchor") # # The metaverse object is NOT going to flow into AAD connector space. # if ($sourceAnchorAttribute -eq $null) { WriteEventLog($EventIdLinkedMailboxIssue)($EventMsgLinkedMailboxIssue) $SynchronizationIssueList.Add($global:LinkedMailboxIssue) Write-Host "`r`n" "LINKED MAILBOX ISSUE - ANALYSIS:" | Write-Host -fore Cyan "--------------------------------" | Write-Host -fore Cyan $htmlMessage = WriteHtmlMessage -message "Analysis:" -color "#252525" -fontSize "16px" -fontWeight "700" -numberOfLineBreaks 1 $htmlMessageList.Add($htmlMessage) ReportOutput -PropertyName "Linked Mailbox Issue" -PropertyValue "True" $consoleMessage = "Azure AD Connect cannot synchronize the given user object to Azure AD tenant since the object has on-premises linked mailbox." $consoleMessage | ReportError Write-Host "`r`n" $htmlMessage = WriteHtmlMessage -message $consoleMessage -color "#252525" -numberOfLineBreaks 1 $htmlMessageList.Add($htmlMessage) $consoleMessage = "The exchange recipient type `"Linked Mailbox`" is specified with the on-premises AD attribute `"msExchRecipientTypeDetails`" having value 2." $consoleMessage | Write-Host -fore Red Write-Host "`r`n" $htmlMessage = WriteHtmlMessage -message $consoleMessage -color "#252525" -numberOfLineBreaks 2 $htmlMessageList.Add($htmlMessage) "LINKED MAILBOX ISSUE - RECOMMENDED ACTIONS:" | Write-Host -fore Cyan "-------------------------------------------" | Write-Host -fore Cyan $htmlMessage = WriteHtmlMessage -message "Recommended Actions:" -color "#252525" -fontSize "16px" -fontWeight "700" -numberOfLineBreaks 1 $htmlMessageList.Add($htmlMessage) $consoleMessage = "1- Convert the linked mailbox to a user mailbox." $consoleMessage | Write-Host -fore White $htmlMessage = WriteHtmlMessage -message $consoleMessage -color "#252525" -fontWeight "600" -numberOfLineBreaks 1 $htmlMessageList.Add($htmlMessage) $consoleMessage = "In case the given user object is represented only once across all on-premises directories, then you may consider converting the linked mailbox to a user mailbox to unblock the synchronization." $consoleMessage | Write-Host -fore Yellow $htmlMessage = WriteHtmlMessage -message $consoleMessage -color "#252525" -numberOfLineBreaks 1 $htmlMessageList.Add($htmlMessage) $message = "In order to learn how to convert the mailbox, please see:" $consoleMessage = GetConsoleMessageWithLink($message)($global:ConvertLinkedMailboxUrl) $consoleMessage | Write-Host -fore Yellow $htmlMessage = GetHtmlMessageWithLink($message)($global:ConvertLinkedMailboxUrl)($global:ConvertLinkedMailboxText) $htmlMessage = WriteHtmlMessage -message $htmlMessage -color "#252525" -numberOfLineBreaks 1 $htmlMessageList.Add($htmlMessage) $message = "In order to learn how objects are represented only once across all on-premises directories, please see `"Uniquely identifying your users`" section at:" $consoleMessage = GetConsoleMessageWithLink($message)($global:InstallationUrl) $consoleMessage | Write-Host -fore Yellow Write-Host "`r`n" $htmlMessage = GetHtmlMessageWithLink($message)($global:InstallationUrl)($global:InstallationText) $htmlMessage = WriteHtmlMessage -message $htmlMessage -color "#252525" -numberOfLineBreaks 2 $htmlMessageList.Add($htmlMessage) $consoleMessage = "2- Create an active user account in another on-premises account forest joining into the same user object." $consoleMessage | Write-Host -fore White $htmlMessage = WriteHtmlMessage -message $consoleMessage -color "#252525" -fontWeight "600" -numberOfLineBreaks 1 $htmlMessageList.Add($htmlMessage) $consoleMessage = "A linked mailbox is supposed to be associated with a disabled user account or contact object joining into an active user account from another on-premises AD forest." $consoleMessage | Write-Host -fore Yellow $htmlMessage = WriteHtmlMessage -message $consoleMessage -color "#252525" -numberOfLineBreaks 1 $htmlMessageList.Add($htmlMessage) $message = "In order to learn how objects join from different on-premises forests, please see `"Uniquely identifying your users`" section at:" $consoleMessage = GetConsoleMessageWithLink($message)($global:InstallationUrl) $consoleMessage | Write-Host -fore Yellow $htmlMessage = GetHtmlMessageWithLink($message)($global:InstallationUrl)($global:InstallationText) $htmlMessage = WriteHtmlMessage -message $htmlMessage -color "#252525" -numberOfLineBreaks 1 $htmlMessageList.Add($htmlMessage) $message = "In order to learn about account-resource forest topology, please see `"Multiple forests: account-resource forest`" section at:" $consoleMessage = GetConsoleMessageWithLink($message)($global:TopologiesUrl) $consoleMessage | Write-Host -fore Yellow $htmlMessage = GetHtmlMessageWithLink($message)($global:TopologiesUrl)($global:TopologiesText) $htmlMessage = WriteHtmlMessage -message $htmlMessage -color "#252525" -numberOfLineBreaks 1 $htmlMessageList.Add($htmlMessage) $linkedMailboxHtmlItem = WriteHtmlAccordionItemForParagraph($htmlMessageList)("Linked Mailbox") $SynchronizationIssueHtmlItems.Add($linkedMailboxHtmlItem) } else { ReportOutput -PropertyName "Linked Mailbox Issue" -PropertyValue "False" "OK" | Write-Host -fore Green Write-Host "`r`n" } } Function DiagnoseUPNMismatch { param ( [Microsoft.IdentityManagement.PowerShell.ObjectModel.MvObject] [parameter(mandatory=$true)] $MvObject, [Microsoft.Online.Administration.User] [parameter (mandatory=$true)] $AadTenantObject, [string] [parameter (mandatory=$true)] $AadTenantDefaultDomainName, [Microsoft.Online.Administration.Domain[]] [parameter(mandatory=$true)] $AadTenantDomains ) $htmlMessageList = New-Object System.Collections.Generic.List[string] # # Report metaverse object UPN # $mvObjectUserPrincipalNameAttribute = GetMVObjectAttribute($MvObject)("userPrincipalName") $mvObjectUserPrincipalNameValue = $null if ($mvObjectUserPrincipalNameAttribute -ne $null) { $mvObjectUserPrincipalNameValue = $mvObjectUserPrincipalNameAttribute.Values[0] } else { "Failed to get `"userPrincipalName`" attribute value of the metaverse object." | Write-Host -fore Red Write-Host "`r`n" return } $htmlMessage = WriteHtmlMessage -message "Analysis:" -color "#252525" -fontSize "16px" -fontWeight "700" -numberOfLineBreaks 1 $htmlMessageList.Add($htmlMessage) $consoleMessage = "For the given user object there is a `"userPrincipalName`" attribute value mismatch between AADConnect and AAD Tenant." $htmlMessage = WriteHtmlMessage -message $consoleMessage -color "#252525" -numberOfLineBreaks 2 $htmlMessageList.Add($htmlMessage) $consoleMessage = "AADConnect object `"userPrincipalName`" attribute value is:" $consoleMessage | Write-Host -fore Cyan $htmlMessage = WriteHtmlMessage -message $consoleMessage -color "#252525" -fontWeight "600" -numberOfLineBreaks 1 $htmlMessageList.Add($htmlMessage) $consoleMessage = $mvObjectUserPrincipalNameValue $consoleMessage | Write-Host -fore Green Write-Host "`r`n" $htmlMessage = WriteHtmlMessage -message $consoleMessage -color "#252525" -numberOfLineBreaks 2 $htmlMessageList.Add($htmlMessage) # # Report AAD Tenant object UPN # $aadTenantObjectUserPrincipalNameValue = $AadTenantObject.UserPrincipalName $consoleMessage = "AAD Tenant object `"userPrincipalName`" attribute value is:" $consoleMessage | Write-Host -fore Cyan $htmlMessage = WriteHtmlMessage -message $consoleMessage -color "#252525" -fontWeight "600" -numberOfLineBreaks 1 $htmlMessageList.Add($htmlMessage) $consoleMessage = $aadTenantObjectUserPrincipalNameValue $consoleMessage | Write-Host -fore Green $htmlMessage = WriteHtmlMessage -message $consoleMessage -color "#252525" -numberOfLineBreaks 2 $htmlMessageList.Add($htmlMessage) WriteEventLog($EventIdUPNMismatch)($EventMsgUPNMismatch -f ($mvObjectUserPrincipalNameValue, $aadTenantObjectUserPrincipalNameValue)) Write-Host "`r`n" $mvObjectUpnPrefix = $mvObjectUserPrincipalNameValue.Split('@')[0] $mvObjectUpnSuffix = $mvObjectUserPrincipalNameValue.Split('@')[1] $aadTenantObjectUpnSuffix = $aadTenantObjectUserPrincipalNameValue.Split('@')[1] # # Get AAD Tenant object domain # $aadTenantObjectDomain = $AadTenantDomains | Where {$_.Name -eq $aadTenantObjectUpnSuffix} # # Azure AD does NOT allow updates to UserPrincipalName in case following conditions occur: # # + AAD Tenant user object is MANAGED (not federated) # + AAD Tenant user object is LICENSED # + AAD Tenant "SynchronizeUpnForManagedUsers" feature is DISABLED # # # LIMITATION: Currently there is NO way to determine authentication type for the AAD Tenant user object. Therefore, checking the authenticaton type (managed/federated) # of the domain that the AAD Tenant user object is part of until finding a way to specifically detecting the authentication type for the AAD Tenant user object. # # As an example, today it is possible to have an AAD Tenant domain with managed authentication having federated user accounts. # $isSynchronizeUpnForManagedUserFeatureEnabled = IsSynchronizeUpnForManagedUsersFeatureEnabled $isAadTenantObjectUPNUpdatesBlocked = (($aadTenantObjectDomain.Authentication -eq "Managed") -and $AadTenantObject.IsLicensed -and !$isSynchronizeUpnForManagedUserFeatureEnabled) # # Get on-premises attribute name configured as source of metaverse object UPN # $upnAttributeName = (Get-ADSyncGlobalSettings).Parameters["Microsoft.SynchronizationOption.UPNAttribute"].Value if ($upnAttributeName -ne "userPrincipalName") { "Alternate Login ID:" | Write-Host -fore Cyan "On-premises attribute `"$($upnAttributeName)`" is configured as source of Azure AD username." | Write-Host -fore Green Write-Host "`r`n" } # # Report that UPN update is blocked for the AAD Tenant user object. # if ($isAadTenantObjectUPNUpdatesBlocked) { WriteEventLog($EventIdUPNUpdateBlocked)($EventMsgUPNUpdateBlocked -f $aadTenantObjectUserPrincipalNameValue) $SynchronizationIssueList.Add($global:UPNMismatchDirSyncFeatureIssue) $consoleMessage = "Updates to `"userPrincipalName`" attribute of the AAD Tenant object `"$($aadTenantObjectUserPrincipalNameValue)`" is blocked since AAD Tenant DirSync feature `"SynchronizeUpnForManagedUsers`" is disabled." $consoleMessage | Write-Host -fore Yellow Write-Host "`r`n" $htmlMessage = WriteHtmlMessage -message $consoleMessage -color "#252525" -numberOfLineBreaks 2 $htmlMessageList.Add($htmlMessage) "USERPRINCIPALNAME MISMATCH - RECOMMENDED ACTIONS:" | Write-Host -fore Cyan "-------------------------------------------------" | Write-Host -fore Cyan $htmlMessage = WriteHtmlMessage -message "Recommended Actions:" -color "#252525" -fontSize "16px" -fontWeight "700" -numberOfLineBreaks 1 $htmlMessageList.Add($htmlMessage) $consoleMessage = "AAD Tenant DirSync feature `"SynchronizeUpnForManagedUsers`" should be enabled to unblock updates." $consoleMessage | Write-Host -fore Yellow $htmlMessage = WriteHtmlMessage -message $consoleMessage -color "#252525" -numberOfLineBreaks 1 $htmlMessageList.Add($htmlMessage) $consoleMessage = "Once AAD Tenant DirSync feature `"SynchronizeUpnForManagedUsers`" is enabled, it cannot be disabled." $consoleMessage | Write-Host -fore Yellow Write-Host "`r`n" $htmlMessage = WriteHtmlMessage -message $consoleMessage -color "#252525" -numberOfLineBreaks 2 $htmlMessageList.Add($htmlMessage) $message = "For more information, please see `"Scenario 2`" in the following:" $consoleMessage = GetConsoleMessageWithLink($message)($global:UPNMismatchUrl) $consoleMessage | Write-Host -fore Yellow Write-Host "`r`n" $htmlMessage = GetHtmlMessageWithLink($message)($global:UPNMismatchUrl)($global:UPNMismatchText) $htmlMessage = WriteHtmlMessage -message $htmlMessage -color "#252525" -numberOfLineBreaks 2 $htmlMessageList.Add($htmlMessage) $upnMismatchHtmlItem = WriteHtmlAccordionItemForParagraph($htmlMessageList)("UserPrincipalName Mismatch") $SynchronizationIssueHtmlItems.Add($upnMismatchHtmlItem) return } # # Check if metaverse object UPN suffix is verified as an internet-routable domain name in AAD Tenant. # $isMvObjectUpnSuffixVerified = IsUPNSuffixVerifiedInAADTenant($MvObjectUpnSuffix)($AadTenantDomains) if ($isMvObjectUpnSuffixVerified) { WriteEventLog($EventIdUPNSuffixVerified)($EventMsgUPNSuffixVerified -f ($mvObjectUpnSuffix, $AadTenantDefaultDomainName)) $updatedAadTenantDomain = Get-MsolDomain -DomainName $mvObjectUpnSuffix if ($updatedAadTenantDomain.Authentication -eq "Federated" -and $aadTenantObjectDomain.Authentication -eq "Federated") { WriteEventLog($EventIdUPNFederatedDomainChange)($EventMsgUPNFederatedDomainChange -f ($mvObjectUserPrincipalNameValue, $aadTenantObjectUserPrincipalNameValue)) $SynchronizationIssueList.Add($global:UPNMismatchFederatedDomainChangeIssue) $consoleMessage = "Azure Active Directory does NOT allow to synchronize UPN suffix change from one federated domain to another federated domain." $consoleMessage | Write-Host -fore Yellow Write-Host "`r`n" $htmlMessage = WriteHtmlMessage -message $consoleMessage -color "#252525" -numberOfLineBreaks 2 $htmlMessageList.Add($htmlMessage) "USERPRINCIPALNAME MISMATCH - RECOMMENDED ACTIONS:" | Write-Host -fore Cyan "-------------------------------------------------" | Write-Host -fore Cyan $htmlMessage = WriteHtmlMessage -message "Recommended Actions:" -color "#252525" -fontSize "16px" -fontWeight "700" -numberOfLineBreaks 1 $htmlMessageList.Add($htmlMessage) $message = "To change from one federated domain to another, please see:" $consoleMessage = GetConsoleMessageWithLink($message)($global:FederatedDomainChangeUrl) $consoleMessage | Write-Host -fore Yellow Write-Host "`r`n" $htmlMessage = GetHtmlMessageWithLink($message)($global:FederatedDomainChangeUrl)($global:FederatedDomainChangeText) $htmlMessage = WriteHtmlMessage -message $htmlMessage -color "#252525" -numberOfLineBreaks 2 $htmlMessageList.Add($htmlMessage) } } else { WriteEventLog($EventIdUPNSuffixNotVerified)($EventMsgUPNSuffixNotVerified -f ($mvObjectUpnSuffix, $AadTenantDefaultDomainName)) $SynchronizationIssueList.Add($global:UPNMismatchNonVerifiedUpnSuffixIssue) $consoleMessage = "UPN suffix `"$($mvObjectUpnSuffix)`" is NOT verified with AAD Tenant `"$($AadTenantDefaultDomainName)`" as a domain." $consoleMessage | Write-Host -fore Yellow $htmlMessage = WriteHtmlMessage -message $consoleMessage -color "#252525" -numberOfLineBreaks 1 $htmlMessageList.Add($htmlMessage) $consoleMessage = "Azure Active Directory replaces such UPN suffixes with default domain name `"onmicrosoft.com`"." $consoleMessage | Write-Host -fore Yellow Write-Host "`r`n" $htmlMessage = WriteHtmlMessage -message $consoleMessage -color "#252525" -numberOfLineBreaks 2 $htmlMessageList.Add($htmlMessage) $consoleMessage = "When the UPN suffix is NOT verified with the AAD Tenant, Azure AD takes the following inputs into account in the given order to calculate the UPN prefix in the cloud:" $consoleMessage | Write-Host -fore Yellow $htmlMessage = WriteHtmlMessage -message $consoleMessage -color "#252525" -numberOfLineBreaks 1 $htmlMessageList.Add($htmlMessage) "`t1- On-Premises `"mailNickName`" attribute" | Write-Host -fore Yellow "`t2- Primary SMTP Address" | Write-Host -fore Yellow "`t3- On-Premises `"mail`" attribute" | Write-Host -fore Yellow "`t4- UserPrincipalName/Alternate Login ID" | Write-Host -fore Yellow Write-Host "`r`n" $htmlMessage = WriteHtmlMessage -message "1- On-Premises `"mailNickName`" attribute" -color "#252525" -paddingLeft "30px" -numberOfLineBreaks 1 $htmlMessageList.Add($htmlMessage) $htmlMessage = WriteHtmlMessage -message "2- Primary SMTP Address" -color "#252525" -paddingLeft "30px" -numberOfLineBreaks 1 $htmlMessageList.Add($htmlMessage) $htmlMessage = WriteHtmlMessage -message "3- On-Premises `"mail`" attribute" -color "#252525" -paddingLeft "30px" -numberOfLineBreaks 1 $htmlMessageList.Add($htmlMessage) $htmlMessage = WriteHtmlMessage -message "4- UserPrincipalName/Alternate Login ID" -color "#252525" -paddingLeft "30px" -numberOfLineBreaks 2 $htmlMessageList.Add($htmlMessage) $message = "Please see to understand how Azure Active Directory populates UPN in the cloud:" $consoleMessage = GetConsoleMessageWithLink($message)($global:CloudUPNPopulationUrl) $consoleMessage | Write-Host -fore Yellow Write-Host "`r`n" Write-Host "`r`n" $htmlMessage = GetHtmlMessageWithLink($message)($global:CloudUPNPopulationUrl)($global:CloudUPNPopulationText) $htmlMessage = WriteHtmlMessage -message $htmlMessage -color "#252525" -numberOfLineBreaks 2 $htmlMessageList.Add($htmlMessage) "USERPRINCIPALNAME MISMATCH - RECOMMENDED ACTIONS:" | Write-Host -fore Cyan "-------------------------------------------------" | Write-Host -fore Cyan Write-Host "`r`n" $htmlMessage = WriteHtmlMessage -message "Recommended Actions:" -color "#252525" -fontSize "16px" -fontWeight "700" -numberOfLineBreaks 2 $htmlMessageList.Add($htmlMessage) $consoleMessage = "Please consider to follow one of the options given below:" $consoleMessage | Write-Host -fore White Write-Host "`r`n" $htmlMessage = WriteHtmlMessage -message $consoleMessage -color "#252525" -fontWeight "600" -numberOfLineBreaks 2 $htmlMessageList.Add($htmlMessage) $consoleMessage = "1- Verify current UPN suffix with AAD Tenant" $consoleMessage | Write-Host -fore White $htmlMessage = WriteHtmlMessage -message $consoleMessage -color "#252525" -fontWeight "600" -numberOfLineBreaks 1 $htmlMessageList.Add($htmlMessage) $consoleMessage = "In order to use `"$($mvObjectUserPrincipalNameValue)`" as Azure Active Directory username, you need to verify UPN suffix `"$($mvObjectUpnSuffix)`" with AAD Tenant `"$($AadTenantDefaultDomainName)`"." $consoleMessage | Write-Host -fore Yellow $htmlMessage = WriteHtmlMessage -message $consoleMessage -color "#252525" -numberOfLineBreaks 1 $htmlMessageList.Add($htmlMessage) $message = "In order to verify a domain name with AAD Tenant, please see:" $consoleMessage = GetConsoleMessageWithLink($message)($global:VerifyDomainNameUrl) $consoleMessage | Write-Host -fore Yellow Write-Host "`r`n" $htmlMessage = GetHtmlMessageWithLink($message)($global:VerifyDomainNameUrl)($global:VerifyDomainNameText) $htmlMessage = WriteHtmlMessage -message $htmlMessage -color "#252525" -numberOfLineBreaks 2 $htmlMessageList.Add($htmlMessage) if ($upnAttributeName -eq "userPrincipalName") { $consoleMessage = "2- Alternative UPN Suffix" $consoleMessage | Write-Host -fore White $htmlMessage = WriteHtmlMessage -message $consoleMessage -color "#252525" -fontWeight "600" -numberOfLineBreaks 1 $htmlMessageList.Add($htmlMessage) $consoleMessage = "As another option, you may consider adding an alternative UPN suffix to your on-premises accounts." $consoleMessage | Write-Host -fore Yellow $htmlMessage = WriteHtmlMessage -message $consoleMessage -color "#252525" -numberOfLineBreaks 1 $htmlMessageList.Add($htmlMessage) $message = "Please see:" $consoleMessage = GetConsoleMessageWithLink($message)($global:AlternativeUPNSuffixUrl) $consoleMessage | Write-Host -fore Yellow Write-Host "`r`n" $htmlMessage = GetHtmlMessageWithLink($message)($global:AlternativeUPNSuffixUrl)($global:AlternativeUPNSuffixText) $htmlMessage = WriteHtmlMessage -message $htmlMessage -color "#252525" -numberOfLineBreaks 2 $htmlMessageList.Add($htmlMessage) $consoleMessage = "3- Run `"Set-MsolUserPrincipalName`" Cmdlet" $consoleMessage | Write-Host -fore White $htmlMessage = WriteHtmlMessage -message $consoleMessage -color "#252525" -fontWeight "600" -numberOfLineBreaks 1 $htmlMessageList.Add($htmlMessage) } else { $consoleMessage = "2- Run `"Set-MsolUserPrincipalName`" Cmdlet" $consoleMessage | Write-Host -fore White $htmlMessage = WriteHtmlMessage -message $consoleMessage -color "#252525" -fontWeight "600" -numberOfLineBreaks 1 $htmlMessageList.Add($htmlMessage) } $consoleMessage = "Apart from the UPN mismatch, if you are only interested in changing AAD Tenant user object `"userPrincipalName`" attribute prefix and/or suffix, then run AAD PowerShell cmdlet `"Set-MsolUserPrincipalName`"." $consoleMessage | Write-Host -fore Yellow $htmlMessage = WriteHtmlMessage -message $consoleMessage -color "#252525" -numberOfLineBreaks 1 $htmlMessageList.Add($htmlMessage) $message = "In order to learn about the cmdlet, please see:" $consoleMessage = GetConsoleMessageWithLink($message)($global:SetUPNCmdletUrl) $consoleMessage | Write-Host -fore Yellow Write-Host "`r`n" $htmlMessage = GetHtmlMessageWithLink($message)($global:SetUPNCmdletUrl)($global:SetUPNCmdletText) $htmlMessage = WriteHtmlMessage -message $htmlMessage -color "#252525" -numberOfLineBreaks 2 $htmlMessageList.Add($htmlMessage) } $upnMismatchHtmlItem = WriteHtmlAccordionItemForParagraph($htmlMessageList)("UserPrincipalName Mismatch") $SynchronizationIssueHtmlItems.Add($upnMismatchHtmlItem) } Function DiagnoseUserObjectProxyAddresses { $htmlMessageList = New-Object System.Collections.Generic.List[string] $SynchronizationIssueList.Add($global:ChangePrimaryEmailAddress) "CHANGING EXCHANGE ONLINE PRIMARY EMAIL ADDRESS - RECOMMENDED ACTIONS:" | Write-Host -fore Cyan "---------------------------------------------------------------------" | Write-Host -fore Cyan $htmlMessage = WriteHtmlMessage -message "Recommended Actions:" -color "#252525" -fontSize "16px" -fontWeight "700" -numberOfLineBreaks 1 $htmlMessageList.Add($htmlMessage) $consoleMessage = "1- Open `"ADSI Edit`" within a domain controller storing the target object in your on-premises directory." $consoleMessage | Write-Host -fore White Write-Host "`r`n" $htmlMessage = WriteHtmlMessage -message $consoleMessage -color "#252525" -fontWeight "600" -numberOfLineBreaks 2 $htmlMessageList.Add($htmlMessage) $consoleMessage = "2- Locate the target object." $consoleMessage | Write-Host -fore White Write-Host "`r`n" $htmlMessage = WriteHtmlMessage -message $consoleMessage -color "#252525" -fontWeight "600" -numberOfLineBreaks 2 $htmlMessageList.Add($htmlMessage) $consoleMessage = "3- Right click on the target object and go to `"Properties`"." $consoleMessage | Write-Host -fore White Write-Host "`r`n" $htmlMessage = WriteHtmlMessage -message $consoleMessage -color "#252525" -fontWeight "600" -numberOfLineBreaks 2 $htmlMessageList.Add($htmlMessage) $consoleMessage = "4- Find `"proxyAddresses`" attribute in the Attribute Editor." $consoleMessage | Write-Host -fore White Write-Host "`r`n" $htmlMessage = WriteHtmlMessage -message $consoleMessage -color "#252525" -fontWeight "600" -numberOfLineBreaks 2 $htmlMessageList.Add($htmlMessage) $consoleMessage = "5- Add/Change primary smtp address." $consoleMessage | Write-Host -fore White $htmlMessage = WriteHtmlMessage -message $consoleMessage -color "#252525" -fontWeight "600" -numberOfLineBreaks 1 $htmlMessageList.Add($htmlMessage) $consoleMessage = "Primary smtp address should have the prefix `"SMTP:`" in capital letters. Example: `"SMTP:myaccount@testdomain.com`"." $consoleMessage | Write-Host -fore Yellow $htmlMessage = WriteHtmlMessage -message $consoleMessage -color "#252525" -numberOfLineBreaks 1 $htmlMessageList.Add($htmlMessage) $consoleMessage = "The domain part of the email address should be a domain name verified with the Azure AD tenant and it should NOT be the default tenant domain name ending with `"onmicrosoft.com`"." $consoleMessage | Write-Host -fore Yellow Write-Host "`r`n" $htmlMessage = WriteHtmlMessage -message $consoleMessage -color "#252525" -numberOfLineBreaks 2 $htmlMessageList.Add($htmlMessage) $consoleMessage = "6- Find `"mail`" attribute in the Attribute Editor." $consoleMessage | Write-Host -fore White Write-Host "`r`n" $htmlMessage = WriteHtmlMessage -message $consoleMessage -color "#252525" -fontWeight "600" -numberOfLineBreaks 2 $htmlMessageList.Add($htmlMessage) $consoleMessage = "7- Set/Change `"mail`" attribute value to the same email address WITHOUT the prefix `"SMTP:`"." $consoleMessage | Write-Host -fore White Write-Host "`r`n" $htmlMessage = WriteHtmlMessage -message $consoleMessage -color "#252525" -fontWeight "600" -numberOfLineBreaks 2 $htmlMessageList.Add($htmlMessage) $consoleMessage = "8- At the end of next sync cycle, the primary email address in the cloud will change to the primary smtp address." $consoleMessage | Write-Host -fore White Write-Host "`r`n" $htmlMessage = WriteHtmlMessage -message $consoleMessage -color "#252525" -fontWeight "600" -numberOfLineBreaks 2 $htmlMessageList.Add($htmlMessage) $consoleMessage = "IMPORTANT:" $consoleMessage | Write-Host -fore Yellow $htmlMessage = WriteHtmlMessage -message $consoleMessage -color "#252525" -fontWeight "600" -numberOfLineBreaks 1 $htmlMessageList.Add($htmlMessage) $consoleMessage = "Azure Active Directory does NOT allow to synchronize email addresses from the on-premises directory in case:" $consoleMessage | Write-Host -fore Yellow $htmlMessage = WriteHtmlMessage -message $consoleMessage -color "#252525" -numberOfLineBreaks 1 $htmlMessageList.Add($htmlMessage) $consoleMessage = "a- the domain part of the on-premises email address is NOT a domain name verified with the Azure AD tenant." $consoleMessage | Write-Host -fore Yellow $htmlMessage = WriteHtmlMessage -message $consoleMessage -color "#252525" -numberOfLineBreaks 1 $htmlMessageList.Add($htmlMessage) $consoleMessage = "b- the domain part of the on-premises email address is the default tenant domain name ending with `"onmicrosoft.com`"." $consoleMessage | Write-Host -fore Yellow $htmlMessage = WriteHtmlMessage -message $consoleMessage -color "#252525" -numberOfLineBreaks 1 $htmlMessageList.Add($htmlMessage) $primaryEmailAddressHtmlItem = WriteHtmlAccordionItemForParagraph($htmlMessageList)($global:ChangePrimaryEmailAddress) $SynchronizationIssueHtmlItems.Add($primaryEmailAddressHtmlItem) Write-Host "`r`n" Write-Host "`r`n" } Function DiagnoseUserObjectHideFromAddressLists { param ( [Microsoft.IdentityManagement.PowerShell.ObjectModel.Connector] [parameter(mandatory=$true)] $AdConnector, [System.Collections.Generic.Dictionary[[String], [Object]]] [parameter(mandatory=$true)] $AdObject, [Microsoft.IdentityManagement.PowerShell.ObjectModel.CsObject] [parameter(mandatory=$true)] $AdCsObject ) $htmlMessageList = New-Object System.Collections.Generic.List[string] $SynchronizationIssueList.Add($global:HideFromGlobalAddressList) "HIDING MAILBOX FROM EXCHANGE ONLINE GLOBAL ADDRESS LIST - RECOMMENDED ACTIONS:" | Write-Host -fore Cyan "-------------------------------------------------------------------------------" | Write-Host -fore Cyan $htmlMessage = WriteHtmlMessage -message "Recommended Actions:" -color "#252525" -fontSize "16px" -fontWeight "700" -numberOfLineBreaks 1 $htmlMessageList.Add($htmlMessage) # # Get AD connector space object attribute "msExchHideFromAddressLists" # $msExchHideFromAddressListsAttribute = GetCSObjectAttribute($AdCsObject)("msExchHideFromAddressLists") $msExchHideFromAddressListsValue = $null if ($msExchHideFromAddressListsAttribute -ne $null) { $msExchHideFromAddressListsValue = $msExchHideFromAddressListsAttribute.Values[0] } # # Get AD connector space object attribute "mailNickname" # $mailNickNameAttribute = GetCSObjectAttribute($AdCsObject)("mailNickname") $mailNickNameValue = $null if ($mailNickNameAttribute -ne $null) { $mailNickNameValue = $mailNickNameAttribute.Values[0] } $step = 1; # # Synchronizing "msExchHideFromAddressLists" attribute requires "mailNickname" attribute to be populated within on-premises directory. # if ($msExchHideFromAddressListsValue -ne $null -and [bool] $msExchHideFromAddressListsValue -eq $true -and $mailNickNameValue -ne $null) { $consoleMessage = "$($step)- Your settings are correct. Please open a service request through Azure Portal or Office 365 Portal." $consoleMessage | Write-Host -fore White Write-Host "`r`n" $htmlMessage = WriteHtmlMessage -message $consoleMessage -color "#252525" -fontWeight "600" -numberOfLineBreaks 2 $htmlMessageList.Add($htmlMessage) $message = "`tMicrosoft Azure Portal:" $consoleMessage = GetConsoleMessageWithLink($message)($global:AzurePortalSupportBladeUrl) $consoleMessage | Write-Host -fore Yellow Write-Host "`r`n" $htmlMessage = WriteHyperlink($global:AzurePortalSupportBladeUrl)($global:AzurePortalSupportBladeText) $htmlMessage = WriteHtmlMessage -message $htmlMessage -color "#252525" -numberOfLineBreaks 2 $htmlMessageList.Add($htmlMessage) $message = "`tOffice 365 Portal:" $consoleMessage = GetConsoleMessageWithLink($message)($global:OfficePortalUrl) $consoleMessage | Write-Host -fore Yellow Write-Host "`r`n" $htmlMessage = WriteHyperlink($global:OfficePortalUrl)($global:OfficePortalText) $htmlMessage = WriteHtmlMessage -message $htmlMessage -color "#252525" -numberOfLineBreaks 2 $htmlMessageList.Add($htmlMessage) $hideFromAddressListHtmlItem = WriteHtmlAccordionItemForParagraph($htmlMessageList)("Hiding Account From Global Address List") $SynchronizationIssueHtmlItems.Add($hideFromAddressListHtmlItem) return } # # In order to hide account from exchange online global address list, "msExchHideFromAdressLists" attribute value must be TRUE. # if ($msExchHideFromAddressListsValue -ne $null -and [bool] $msExchHideFromAddressListsValue -eq $false) { $consoleMessage = "$($step)- Please set `"msExchHideFromAddressLists`" attribute value to `"TRUE`" within your on-premises directory." $consoleMessage | Write-Host -fore White Write-Host "`r`n" $htmlMessage = WriteHtmlMessage -message $consoleMessage -color "#252525" -fontWeight "600" -numberOfLineBreaks 2 $htmlMessageList.Add($htmlMessage) $step++ } elseif ($msExchHideFromAddressListsValue -eq $null) { # # Check if "msExchHideFromAddressLists" attribute is included in the schema of the given AD Connector. # $adConnectorSchemaAttributeType = $AdConnector.Schema.AttributeTypes["msExchHideFromAddressLists"] # # Get on-premises AD object "msExchHideFromAddressLists" attribute value. # $adObjectAttributeValue = $null if ($AdObject.ContainsKey("msexchhidefromaddresslists")) { $adObjectAttributeValue = [bool] ([System.Collections.ArrayList] $AdObject["msexchhidefromaddresslists"])[0] } # # Recommended steps based on availability of "msExchHideFromAddressLists" attribute in AD Connector schema. # if ($adConnectorSchemaAttributeType -eq $null) { if ($adObjectAttributeValue -eq $null) { # # Customer needs to set the "msExchHideFromAddressLists" attribute value in the on-premises directory. # # If the Active Directory schema does NOT have the Exchange attributes, then the customer needs to extend the schema. # $consoleMessage = "$($step)- Please set `"msExchHideFromAddressLists`" attribute value to `"TRUE`" within your on-premises directory." $consoleMessage | Write-Host -fore White $htmlMessage = WriteHtmlMessage -message $consoleMessage -color "#252525" -fontWeight "600" -numberOfLineBreaks 1 $htmlMessageList.Add($htmlMessage) $consoleMessage = "If the object does NOT have the attribute, then you need to extend your on-premises Active Directory schema to include the Exchange attributes." $consoleMessage | Write-Host -fore Yellow $htmlMessage = WriteHtmlMessage -message $consoleMessage -color "#252525" -numberOfLineBreaks 1 $htmlMessageList.Add($htmlMessage) $message = "In order to extend Active Directory schema, please see:" $consoleMessage = GetConsoleMessageWithLink($message)($global:ExtendSchemaUrl) $consoleMessage | Write-Host -fore Yellow Write-Host "`r`n" $htmlMessage = GetHtmlMessageWithLink($message)($global:ExtendSchemaUrl)($global:ExtendSchemaText) $htmlMessage = WriteHtmlMessage -message $htmlMessage -color "#252525" -numberOfLineBreaks 2 $htmlMessageList.Add($htmlMessage) $step++ } elseif ($adObjectAttributeValue -eq $false) { $consoleMessage = "$($step)- Please set `"msExchHideFromAddressLists`" attribute value to `"TRUE`" within your on-premises directory." $consoleMessage | Write-Host -fore White Write-Host "`r`n" $htmlMessage = WriteHtmlMessage -message $consoleMessage -color "#252525" -fontWeight "600" -numberOfLineBreaks 2 $htmlMessageList.Add($htmlMessage) $step++ } # # As the AD Connector schema does NOT have the "msExchHideFromAddressLists" attribute, the customer needs to refresh the schema. # $consoleMessage = "$($step)- Please refresh schema stored in AADConnect for the on-premises directory `"$($AdConnector.Name)`"." $consoleMessage | Write-Host -fore White $htmlMessage = WriteHtmlMessage -message $consoleMessage -color "#252525" -fontWeight "600" -numberOfLineBreaks 1 $htmlMessageList.Add($htmlMessage) $message = "In order to refresh schema from AADConnect Wizard, please see:" $consoleMessage = GetConsoleMessageWithLink($message)($global:RefreshSchemaUrl) $consoleMessage | Write-Host -fore Yellow Write-Host "`r`n" $htmlMessage = GetHtmlMessageWithLink($message)($global:RefreshSchemaUrl)($global:RefreshSchemaText) $htmlMessage = WriteHtmlMessage -message $htmlMessage -color "#252525" -numberOfLineBreaks 2 $htmlMessageList.Add($htmlMessage) $step++ } else # "msExchHideFromAddressLists" attribute is available in the AD Connector schema. { # # Check if "msExchHideFromAddressLists" attribute is added to attribute inclusion list of the AD Connector. # $inclusionListAttribute = $AdConnector.AttributeInclusionList | ? {$_ -eq "msExchHideFromAddressLists"} if ($inclusionListAttribute -eq $null) { $consoleMessage = "$($step)- Please add `"msExchHideFromAddressLists`" attribute into attribute inclusion list of the AD Connector. Follow these steps:" $consoleMessage | Write-Host -fore White $htmlMessage = WriteHtmlMessage -message $consoleMessage -color "#252525" -fontWeight "600" -numberOfLineBreaks 1 $htmlMessageList.Add($htmlMessage) $consoleMessage = "`ta- Open `"Synchronization Service Manager`" UI." $consoleMessage | Write-Host -fore Yellow $htmlMessage = WriteHtmlMessage -message $consoleMessage -color "#252525" -numberOfLineBreaks 1 $htmlMessageList.Add($htmlMessage) $consoleMessage = "`tb- Locate target AD Connector from `"Connectors`" tab." $consoleMessage | Write-Host -fore Yellow $htmlMessage = WriteHtmlMessage -message $consoleMessage -color "#252525" -numberOfLineBreaks 1 $htmlMessageList.Add($htmlMessage) $consoleMessage = "`tc- Locate target AD Connector from `"Connectors`" tab." $consoleMessage | Write-Host -fore Yellow $htmlMessage = WriteHtmlMessage -message $consoleMessage -color "#252525" -numberOfLineBreaks 1 $htmlMessageList.Add($htmlMessage) $consoleMessage = "`td- Right click on the AD Connector and go to `"Properties`"." $consoleMessage | Write-Host -fore Yellow $htmlMessage = WriteHtmlMessage -message $consoleMessage -color "#252525" -numberOfLineBreaks 1 $htmlMessageList.Add($htmlMessage) $consoleMessage = "`te- Go to `"Select attributes`" option and select `"Show All`" checkbox." $consoleMessage | Write-Host -fore Yellow $htmlMessage = WriteHtmlMessage -message $consoleMessage -color "#252525" -numberOfLineBreaks 1 $htmlMessageList.Add($htmlMessage) $consoleMessage = "`tf- Select checkbox for the `"msExchHideFromAddressLists`" attribute." $consoleMessage | Write-Host -fore Yellow Write-Host "`r`n" $htmlMessage = WriteHtmlMessage -message $consoleMessage -color "#252525" -numberOfLineBreaks 2 $htmlMessageList.Add($htmlMessage) $step++ # # A change is needed to pick the "msExchHideFromAddressLists" attribute in the next import. # $consoleMessage = "$($step)- Please set `"msExchHideFromAddressLists`" attribute value to at first `"Not set`" and then `"TRUE`" within your on-premises directory. If the value is already `"Not set`", then just change it to `"TRUE`"." $consoleMessage | Write-Host -fore White Write-Host "`r`n" $htmlMessage = WriteHtmlMessage -message $consoleMessage -color "#252525" -fontWeight "600" -numberOfLineBreaks 2 $htmlMessageList.Add($htmlMessage) $step++ } elseif ($adObjectAttributeValue -eq $null) { # # Customer needs to set the "msExchHideFromAddressLists" attribute value in the on-premises directory. # $consoleMessage = "$($step)- Please set `"msExchHideFromAddressLists`" attribute value to `"TRUE`" within your on-premises directory." $consoleMessage | Write-Host -fore White Write-Host "`r`n" $htmlMessage = WriteHtmlMessage -message $consoleMessage -color "#252525" -fontWeight "600" -numberOfLineBreaks 2 $htmlMessageList.Add($htmlMessage) $step++ } else { # # In spite of the following conditions, "msExchHideFromAddressLists" attribute is NOT populated for the AD connector space object. # # + AD Connector schema has "msExchHideFromAddressLists" attribute. # # + "msExchHideFromAddressLists" attribute is added to attribute inclusion list of the AD Connector. # # + "msExchHideFromAddressLists" attribute of on-premises object is set to some value. # $consoleMessage = "$($step)- Your settings are correct. Please open a service request through Azure Portal or Office 365 Portal." $consoleMessage | Write-Host -fore White Write-Host "`r`n" $htmlMessage = WriteHtmlMessage -message $consoleMessage -color "#252525" -fontWeight "600" -numberOfLineBreaks 2 $htmlMessageList.Add($htmlMessage) $message = "`tMicrosoft Azure Portal:" $consoleMessage = GetConsoleMessageWithLink($message)($global:AzurePortalSupportBladeUrl) $consoleMessage | Write-Host -fore Yellow Write-Host "`r`n" $htmlMessage = WriteHyperlink($global:AzurePortalSupportBladeUrl)($global:AzurePortalSupportBladeText) $htmlMessage = WriteHtmlMessage -message $htmlMessage -color "#252525" -numberOfLineBreaks 2 $htmlMessageList.Add($htmlMessage) $message = "`tOffice 365 Portal:" $consoleMessage = GetConsoleMessageWithLink($message)($global:OfficePortalUrl) $consoleMessage | Write-Host -fore Yellow Write-Host "`r`n" $htmlMessage = WriteHyperlink($global:OfficePortalUrl)($global:OfficePortalText) $htmlMessage = WriteHtmlMessage -message $htmlMessage -color "#252525" -numberOfLineBreaks 2 $htmlMessageList.Add($htmlMessage) $step++ } } } # # If AD connector space object does NOT have "mailNickname" attribute, then it will be out of scope for "In from AD - User Exchange" synchronization rule. # # As a result, "msExchHideFromAddressLists" attribute will NOT flow into the metaverse. # if ($mailNickNameValue -eq $null) { $consoleMessage = "$($step)- Please set the `"mailNickname`" attribute to some value within your on-premises directory." $consoleMessage | Write-Host -fore White Write-Host "`r`n" $htmlMessage = WriteHtmlMessage -message $consoleMessage -color "#252525" -fontWeight "600" -numberOfLineBreaks 2 $htmlMessageList.Add($htmlMessage) } $consoleMessage = "The account should be hidden from the global address list of the Exchange Online by the end of next sync cycle." $consoleMessage | Write-Host -fore Yellow $htmlMessage = WriteHtmlMessage -message $consoleMessage -color "#252525" -numberOfLineBreaks 1 $htmlMessageList.Add($htmlMessage) $hideFromAddressListHtmlItem = WriteHtmlAccordionItemForParagraph($htmlMessageList)($global:HideFromGlobalAddressList) $SynchronizationIssueHtmlItems.Add($hideFromAddressListHtmlItem) } # # Get if AAD Tenant DirSync Feature "SynchronizeUpnForManagedUsers" enabled or not. # # If this feature is DISABLED, then on-prem upn suffix updates will NOT be synchronized to cloud upn for the users # that are MANAGED and HAS A LICENSE assigned to them. # # Updates to on-prem upn suffix are NOT synchronized to cloud upn for FEDERATED users at all. # Function IsSynchronizeUpnForManagedUsersFeatureEnabled { $isSynchronizeUpnForManagedUsersFeatureEnabled = $(Get-MsolDirSyncFeatures -Feature "SynchronizeUpnForManagedUsers").Enabled Write-Output $isSynchronizeUpnForManagedUsersFeatureEnabled } # # Get default domain name "<Initial-default-domain-name>.onmicrosoft.com" of the AAD Tenant. # Function GetAADTenantDefaultDomainName { param ( [Microsoft.Online.Administration.Domain[]] [AllowNull()] [parameter(mandatory=$true)] $AADTenantDomains ) if ($isNonInteractiveMode) { return $null } $defaultDomainName = $null foreach ($domain in $AADTenantDomains) { if ($domain.Name.Contains(".onmicrosoft.com") -and !$domain.Name.Contains(".mail.")) { $defaultDomainName = $domain.Name break } } return $defaultDomainName } Function IsUPNSuffixVerifiedInAADTenant { param ( [string] [parameter(mandatory=$true)] $upnSuffix, [Microsoft.Online.Administration.Domain[]] [parameter(mandatory=$true)] $AADTenantDomains ) $isUpnSuffixVerified = $false foreach ($domain in $AADTenantDomains) { if ($domain.Name -eq $upnSuffix) { if ($domain.Status -eq "Verified") { $isUpnSuffixVerified = $true } break } } Write-Output $isUpnSuffixVerified } # # Get AAD Tenant user object that corresponds to given metaverse object. # Function GetAADTenantUser { param ( [Microsoft.IdentityManagement.PowerShell.ObjectModel.MvObject] [parameter(mandatory=$true)] $MvObject ) if ($isNonInteractiveMode) { return $null } $aadTenantUser = $null # # Try to get AAD Tenant user object by Object Id. # # AAD Tenant Object Id is a GUID value that is embedded into "cloudAnchor" attribute of the metaverse object. # $cloudAnchorAttribute = GetMVObjectAttribute($MvObject)("cloudAnchor") if ($cloudAnchorAttribute -ne $null) { $cloudAnchorValue = $cloudAnchorAttribute.Values[0] $aadTenantObjectId = $cloudAnchorValue.Split('_')[1] $aadTenantUser = Get-MsolUser -ObjectId $aadTenantObjectId Write-Output $aadTenantUser return } # # In case metaverse object does NOT have "cloudAnchor" attribute, then # # AAD Tenant user object will be looked up by "displayName" and "sourceAnchor" attributes of the metaverse object. # # # Get "displayName" attribute value of the metaverse object # $displayNameAttribute = GetMVObjectAttribute($MvObject)("displayName") $displayNameValue = $null if ($displayNameAttribute -ne $null) { $displayNameValue = $displayNameAttribute.Values[0] } else { Write-Output $null return } # # Get "sourceAnchor" attribute value of the metaverse object # $sourceAnchorAttribute = GetMVObjectAttribute($MvObject)("sourceAnchor") $sourceAnchorValue = $null if ($sourceAnchorAttribute -ne $null) { $sourceAnchorValue = $sourceAnchorAttribute.Values[0] } else { Write-Output $null return } # # Search user objects in AAD Tenant by "displayName" attribute value of the metaverse object # $aadTenantUserList = Get-MsolUser -SearchString $displayNameValue if ($aadTenantUserList -eq $null) { Write-Output $null return } # # Try to get matching AAD Tenant user object by "sourceAnchor" attribute value of the metaverse object # $aadTenantUser = $aadTenantUserList | Where-Object{$_.ImmutableId -eq $sourceAnchorValue} Write-Output $aadTenantUser } # # Get AAD Tenant group object that corresponds to given metaverse object. # Function GetAADTenantGroup { param ( [Microsoft.IdentityManagement.PowerShell.ObjectModel.MvObject] [parameter(mandatory=$true)] $MvObject ) if ($isNonInteractiveMode) { return $null } $aadTenantGroup = $null # # Try to get AAD Tenant group object by Object Id. # # AAD Tenant Object Id is a GUID value that is embedded into "cloudAnchor" attribute of the metaverse object. # $cloudAnchorAttribute = GetMVObjectAttribute($MvObject)("cloudAnchor") if ($cloudAnchorAttribute -ne $null) { $cloudAnchorValue = $cloudAnchorAttribute.Values[0] $aadTenantObjectId = $cloudAnchorValue.Split('_')[1] $aadTenantGroup = Get-MsolGroup -ObjectId $aadTenantObjectId Write-Output $aadTenantGroup return } # # In case metaverse object does NOT have "cloudAnchor" attribute, then # # AAD Tenant group object will be looked up by "displayName" and "sourceAnchor" attributes of the metaverse object. # # # Get "displayName" attribute value of the metaverse object # $displayNameAttribute = GetMVObjectAttribute($MvObject)("displayName") $displayNameValue = $null if ($displayNameAttribute -ne $null) { $displayNameValue = $displayNameAttribute.Values[0] } else { Write-Output $null return } # # Get "sourceAnchor" attribute value of the metaverse object # $sourceAnchorAttribute = GetMVObjectAttribute($MvObject)("sourceAnchor") $sourceAnchorValue = $null if ($sourceAnchorAttribute -ne $null) { $sourceAnchorValue = $sourceAnchorAttribute.Values[0] } else { Write-Output $null return } # # Search group objects in AAD Tenant by "displayName" attribute value of the metaverse object # $aadTenantGroupList = Get-MsolGroup -SearchString $displayNameValue if ($aadTenantGroupList -eq $null) { Write-Output $null return } # # Try to get matching AAD Tenant user object by "sourceAnchor" attribute value of the metaverse object # $aadTenantGroup = $aadTenantGroupList | Where-Object{$_.ImmutableId -eq $sourceAnchorValue} Write-Output $aadTenantGroup } # # Get AAD Tenant contact object that corresponds to given metaverse object. # Function GetAADTenantContact { param ( [Microsoft.IdentityManagement.PowerShell.ObjectModel.MvObject] [parameter(mandatory=$true)] $MvObject ) if ($isNonInteractiveMode) { return $null } $aadTenantContact = $null # # Try to get AAD Tenant contact object by Object Id. # # AAD Tenant Object Id is a GUID value that is embedded into "cloudAnchor" attribute of the metaverse object. # $cloudAnchorAttribute = GetMVObjectAttribute($MvObject)("cloudAnchor") if ($cloudAnchorAttribute -ne $null) { $cloudAnchorValue = $cloudAnchorAttribute.Values[0] $aadTenantObjectId = $cloudAnchorValue.Split('_')[1] $aadTenantContact = Get-MsolContact -ObjectId $aadTenantObjectId Write-Output $aadTenantContact return } # # In case metaverse object does NOT have "cloudAnchor" attribute, then # # AAD Tenant contact object will be looked up by "displayName" and "sourceAnchor" attributes of the metaverse object. # # # Get "displayName" attribute value of the metaverse object # $displayNameAttribute = GetMVObjectAttribute($MvObject)("displayName") $displayNameValue = $null if ($displayNameAttribute -ne $null) { $displayNameValue = $displayNameAttribute.Values[0] } else { Write-Output $null return } # # Get "sourceAnchor" attribute value of the metaverse object # $sourceAnchorAttribute = GetMVObjectAttribute($MvObject)("sourceAnchor") $sourceAnchorValue = $null if ($sourceAnchorAttribute -ne $null) { $sourceAnchorValue = $sourceAnchorAttribute.Values[0] } else { Write-Output $null return } # # Search contact objects in AAD Tenant by "displayName" attribute value of the metaverse object # $aadTenantContactList = Get-MsolContact -SearchString $displayNameValue if ($aadTenantContactList -eq $null) { Write-Output $null return } # # Try to get matching AAD Tenant object by "sourceAnchor" attribute value of the metaverse object # $aadTenantContact = $aadTenantContactList | Where-Object{$_.ImmutableId -eq $sourceAnchorValue} Write-Output $aadTenantContact } Function GetCSObjectAttribute { param ( [Microsoft.IdentityManagement.PowerShell.ObjectModel.CsObject] [parameter(mandatory=$true)] $CsObject, [string] [parameter(mandatory=$true)] $AttributeName ) if ($CsObject.Attributes[$AttributeName] -eq $null) { WriteEventLog($EventIdCsObjectAttributeNotFound)($EventMsgCsObjectAttributeNotFound -f $AttributeName) Write-Output $null } else { Write-Output $CsObject.Attributes[$AttributeName] } } Function GetMVObjectAttribute { param ( [Microsoft.IdentityManagement.PowerShell.ObjectModel.MvObject] [parameter(mandatory=$true)] $MvObject, [string] [parameter(mandatory=$true)] $AttributeName ) if ($MvObject.Attributes[$AttributeName] -eq $null) { WriteEventLog($EventIdMVObjectAttributeNotFound)($EventMsgMVObjectAttributeNotFound -f $AttributeName) Write-Output $null } else { Write-Output $MvObject.Attributes[$AttributeName]; } } # Set/Create OutputDirectory global variable Function Set-OutputDirectory { if(-not (Test-Path $ObjectDiagnosticsReportOutputDirectory)) { $folder = New-Item -Path $ObjectDiagnosticsReportOutputDirectory -ItemType directory } } Function Export-ObjectDiagnosticsHtmlReport { param ( [string] [parameter(mandatory=$true)] $Title, [string] [parameter(mandatory=$true)] $ReportDate, [string] [parameter(mandatory=$true)] $HtmlDoc ) if ($isNonInteractiveMode) { return } $filename = $Title.Split([IO.Path]::GetInvalidFileNameChars()) -join '_' $filename = "$ObjectDiagnosticsReportOutputDirectory\$ReportDate-$filename.htm" try { $HtmlDoc | Out-File -FilePath $filename "HTML REPORT:" | Write-Host -fore Cyan "------------" | Write-Host -fore Cyan "Detailed HTML Report has been exported to file $filename." | Write-Host -fore Green Write-Host "`r`n" } catch { Write-Error "An error occurred while exporting HTML report to $filename : $($_.Exception.Message)" return } "Opening the html report in Internet Explorer..." | Write-Host -fore White try { # # Add 'Microsoft.VisualBasic' namespace into PowerShell session. # Add-Type -AssemblyName "Microsoft.VisualBasic" $internetExplorer = New-Object -com internetexplorer.application $internetExplorer.navigate2($filename) $internetExplorer.visible = $true if ($internetExplorer.Busy) { Sleep -Seconds 15 } $ieProcess = Get-Process | ? { $_.MainWindowHandle -eq $internetExplorer.HWND } # # Set focus to Internet Explorer so that it will appear on top of other windows. # [Microsoft.VisualBasic.Interaction]::AppActivate($ieProcess.Id) } catch { Write-Error "Unable to open Internet Explorer : $($_.Exception.Message)" } } Function CheckAttributeBasedFiltering { param ( [string] [parameter(mandatory=$true)] $ADConnectorName, [string] [parameter(mandatory=$true)] $ObjectDN, [Microsoft.IdentityManagement.PowerShell.ObjectModel.CsObject] [parameter(mandatory=$false)] [AllowNull()] $AdCsObject, [Microsoft.IdentityManagement.PowerShell.ObjectModel.MvObject] [parameter(mandatory=$false)] [AllowNull()] $MvObject, [Microsoft.IdentityManagement.PowerShell.ObjectModel.CsObject] [parameter(mandatory=$false)] [AllowNull()] $AadCsObject ) "Checking Attribute Filtering configuration..." | Write-Host -fore White if ($AdCsObject -ne $null -and ($MvObject -eq $null -or $AadCsObject -eq $null)) { $previewResult = Sync-ADSyncCsObject -ConnectorName $ADConnectorName -DistinguishedName $ObjectDN if ($previewResult -ne $null) { $previewDiagnosticsData = $previewResult.PreviewDiagnosticsData if ($previewDiagnosticsData -ne $null) { foreach ($entryDiagnoticData in $previewDiagnosticsData.EntryModificationDiagnosticsDataList) { $scopeModuleDiagnosticsData = $entryDiagnoticData.ScopeModuleDiagnosticsData foreach ($outOfScopeSyncRule in $scopeModuleDiagnosticsData.OutOfScopeSyncRules) { $syncRuleName = $outOfScopeSyncRule.SyncRuleName if ($outOfScopeSyncRule.SourceObjectMarkedForDeletion) { $consoleMessage = "Object `"$ObjectDN`" is marked for deletion. ALL sync rules are out of scope for this object." $consoleMessage | ReportError Write-Host "`r`n" } elseif ($outOfScopeSyncRule.Disabled) { $consoleMessage = "Sync Rule `"$syncRuleName`" is out of scope because it is disabled." $consoleMessage | ReportWarning Write-Host "`r`n" } else { "Sync Rule `"$syncRuleName`" is out of scope because the following scoping conditions were not satisfied:" | ReportWarning $scopeConditionGroupIndex = 1 foreach ($scopeConditionGroup in $outOfScopeSyncRule.ScopeConditionGroups) { $attribute = $scopeConditionGroup.Attribute $operator = $scopeConditionGroup.ComparisonOperator $value = $scopeConditionGroup.ComparisonValue "Scoping Group `"$scopeConditionGroupIndex`"" | ReportWarning "[`"$attribute $operator $value`"]" | ReportWarning $scopeConditionGroupIndex++ } Write-Host "`r`n" } } } } } } else { "OK" | Write-Host -fore Green Write-Host "`r`n" } } Function CheckOUBasedFiltering { param ( [Microsoft.IdentityManagement.PowerShell.ObjectModel.ConnectorPartition] [parameter(mandatory=$false)] $Partition, [string] [parameter(mandatory=$true)] $ObjectDN ) if ($Partition -eq $null) { return } $htmlMessageList = New-Object System.Collections.Generic.List[string] "Checking OU Filtering configuration..." | Write-Host $containerInclusionList = $Partition.ConnectorPartitionScope.ContainerInclusionList $containerExclusionList = $Partition.ConnectorPartitionScope.ContainerExclusionList $mostSpecificOUInInclusionList = Get-MostSpecificContainerFromContainerList $containerInclusionList $ObjectDN $mostSpecificOUInExclusionList = Get-MostSpecificContainerFromContainerList $containerExclusionList $ObjectDN $objectOutOfSyncScope = $false if ($mostSpecificOUInInclusionList -ne $null -and $mostSpecificOUInExclusionList -ne $null) { if ($mostSpecificOUInInclusionList.Length -lt $mostSpecificOUInExclusionList.Length) { $objectOutOfSyncScope = $true } } elseif ($mostSpecificOUInExclusionList -ne $null) { $objectOutOfSyncScope = $true } if ($objectOutOfSyncScope) { WriteEventLog($EventIdOUFiltered)($EventMsgOUFiltered -f ($ObjectDN, $mostSpecificOUInExclusionList)) $SynchronizationIssueList.Add($global:OuFilteringIssue) Write-Host "`r`n" "OU FILTERING - ANALYSIS:" | Write-Host -fore Cyan "------------------------" | Write-Host -fore Cyan $htmlMessage = WriteHtmlMessage -message "Analysis:" -color "#252525" -fontSize "16px" -fontWeight "700" -numberOfLineBreaks 1 $htmlMessageList.Add($htmlMessage) $consoleMessage = "Object `"$ObjectDN`" is not present in sync scope. It belongs to a container $mostSpecificOUInExclusionList that is excluded from syncing." $consoleMessage | ReportError Write-Host "`r`n" $htmlMessage = WriteHtmlMessage -message $consoleMessage -color "#252525" -numberOfLineBreaks 2 $htmlMessageList.Add($htmlMessage) "OU FILTERING - RECOMMENDED ACTIONS:" | Write-Host -fore Cyan "-----------------------------------" | Write-Host -fore Cyan $htmlMessage = WriteHtmlMessage -message "Recommended Actions:" -color "#252525" -fontSize "16px" -fontWeight "700" -numberOfLineBreaks 1 $htmlMessageList.Add($htmlMessage) $message = "Include the container $mostSpecificOUInExclusionList in the list of organizational units that should be synced. To read more on how to do this, please see:" $consoleMessage = GetConsoleMessageWithLink($message)($global:OuBasedFilteringUrl) $consoleMessage | Write-Host -fore Yellow Write-Host "`r`n" $htmlMessage = GetHtmlMessageWithLink($message)($global:OuBasedFilteringUrl)($global:OuBasedFilteringText) $htmlMessage = WriteHtmlMessage -message $htmlMessage -color "#252525" -numberOfLineBreaks 0 $htmlMessageList.Add($htmlMessage) $ouFilteringHtmlItem = WriteHtmlAccordionItemForParagraph($htmlMessageList)("OU Filtering") $SynchronizationIssueHtmlItems.Add($ouFilteringHtmlItem) } else { "There is no OU filtering configuration that prevents this object from being imported in the AD connector space." | Write-Host Write-Host "`r`n" } ReportOutput -PropertyName "OU Filtered" -PropertyValue $objectOutOfSyncScope } # This function will return the nearest ancestor to the object # which is present in the specified container list. # Example: if container list contains "DC=msft,DC=com" and "OU=Sales,DC=msft,DC=com" and objectDN = "CN=Aditis,OU=Sales,DC=msft,DC=com" # then this function will return "OU=Sales,DC=msft,DC=com" Function Get-MostSpecificContainerFromContainerList { param ( [string[]] [parameter(mandatory=$false)] $ContainerList, [string] [parameter(mandatory=$true)] $ObjectDN ) if ($ContainerList -eq $null) { return } $length = 0 $mostSpecificOU = $null foreach ($container in $ContainerList) { if ($ObjectDN.Contains($container)) { if ($container.Length -gt $length) { $mostSpecificOU = $container $length = $container.Length } } } return $mostSpecificOU } # # Get if AD object type matches the given object type. # Function IsObjectTypeMatch { param ( [System.Collections.Generic.Dictionary[[String], [Object]]] [parameter(mandatory=$true)] $AdObject, [string] [parameter(mandatory=$true)] $ObjectType ) # # AD object attribute "objectclass" is a multi-valued attribute. # $objectClasses = [System.Collections.ArrayList] $AdObject["objectclass"] foreach ($objectClass in $objectClasses) { if ($objectClass -eq $ObjectType) { Write-Output $true return } } Write-Output $false } # # Check if object type is included on connector # Function CheckObjectTypeInclusion { param ( [Microsoft.IdentityManagement.PowerShell.ObjectModel.Connector] [parameter(mandatory=$true)] $AdConnector, [System.Collections.Generic.Dictionary[[String], [Object]]] [parameter(mandatory=$true)] $AdObject ) $htmlMessageList = New-Object System.Collections.Generic.List[string] Write-Host "Checking if object type is in the connector's object type inclusion list..." $connectorClasses = [System.Collections.ArrayList] $AdConnector.ObjectInclusionList $objectClasses = [System.Collections.ArrayList] $AdObject["objectclass"] $objectClass = $objectClasses[$objectClasses.Count - 1] if ($connectorClasses -contains $objectClass) { Write-Host "Object type `"$objectClass`" is in the connector's object type inclusion list. This configuration will not prevent this object from being imported into the AD connector space." ReportOutput -PropertyName "Object Type Inclusion" -PropertyValue "False" } else { WriteEventLog($EventIdObjectTypeInclusion)($EventMsgObjectTypeInclusion -f ([String] $AdObject["distinguishedname"], $objectClass, $AdConnector.Identifier)) $SynchronizationIssueList.Add($global:ObjectTypeInclusionIssue) Write-Host "`r`n" "OBJECT TYPE INCLUSION - ANALYSIS:" | Write-Host -fore Cyan "---------------------------------" | Write-Host -fore Cyan $htmlMessage = WriteHtmlMessage -message "Analysis:" -color "#252525" -fontSize "16px" -fontWeight "700" -numberOfLineBreaks 1 $htmlMessageList.Add($htmlMessage) $consoleMessage = "Object is not present in sync scope as it is of object class `"$objectClass`" which is not part of the connector's object type inclusion list." $consoleMessage | ReportError -PropertyName "Object Type Inclusion" -PropertyValue "True" Write-Host "`r`n" $htmlMessage = WriteHtmlMessage -message $consoleMessage -color "#252525" -numberOfLineBreaks 2 $htmlMessageList.Add($htmlMessage) "OBJECT TYPE INCLUSION - RECOMMENDED ACTIONS:" | Write-Host -fore Cyan "--------------------------------------------" | Write-Host -fore Cyan $htmlMessage = WriteHtmlMessage -message "Recommended Actions:" -color "#252525" -fontSize "16px" -fontWeight "700" -numberOfLineBreaks 1 $htmlMessageList.Add($htmlMessage) $message = "Ensure object type `"$objectClass`" is being used in a sync rule for this connector. For more information on sync rules, please see:" $consoleMessage = GetConsoleMessageWithLink($message)($global:CustomizeSyncRulesUrl) $consoleMessage | Write-Host -fore Yellow Write-Host "`r`n" $htmlMessage = GetHtmlMessageWithLink($message)($global:CustomizeSyncRulesUrl)($global:CustomizeSyncRulesText) $htmlMessage = WriteHtmlMessage -message $htmlMessage -color "#252525" -numberOfLineBreaks 2 $htmlMessageList.Add($htmlMessage) $message = "If object type `"$objectClass`" is not available in the Synchronization Rules Editor, use the Wizard to refresh the directory schema. For more information on refreshing the schema, please see:" $consoleMessage = GetConsoleMessageWithLink($message)($global:RefreshSchemaUrl) $consoleMessage | Write-Host -fore Yellow Write-Host "`r`n" $htmlMessage = GetHtmlMessageWithLink($message)($global:RefreshSchemaUrl)($global:RefreshSchemaText) $htmlMessage = WriteHtmlMessage -message $htmlMessage -color "#252525" -numberOfLineBreaks 0 $htmlMessageList.Add($htmlMessage) $objectInclusionHtmlItem = WriteHtmlAccordionItemForParagraph($htmlMessageList)("Object Type Inclusion") $SynchronizationIssueHtmlItems.Add($objectInclusionHtmlItem) "Object type `"$objectClass`" is not in the connector's object type inclusion list. This will prevent this object from being imported into the AD connector space." | ReportError } Write-Host "`r`n" } # # Check group filtering # Function CheckGroupFiltering { param ( [Microsoft.IdentityManagement.PowerShell.ObjectModel.Connector] [parameter(mandatory=$true)] $AdConnector, [System.Collections.Generic.Dictionary[[String], [Object]]] [parameter(mandatory=$true)] $AdObject ) $htmlMessageList = New-Object System.Collections.Generic.List[string] Write-Host "Checking group filtering configuration..." $globalSettings = Get-ADSyncGlobalSettings $groupFilteringDN = $AdConnector.GlobalParameters["Connector.GroupFilteringGroupDn"].Value if (($globalSettings.Parameters["Microsoft.OptionalFeature.GroupFiltering"].Value -eq "True") -and $groupFilteringDN) { Write-Host "Group filtering is enabled for group `"$groupFilteringDN`"." $groupADobject = Search-ADSyncDirectoryObjects -AdConnectorId $adConnector.Identifier -LdapFilter "(distinguishedName=$groupFilteringDN)" -SearchScope Subtree -SizeLimit 1 $adObjectCSobject = GetCSObject($AdConnector.Name)($AdObject["distinguishedname"]) $groupCSobject = GetCSObject($AdConnector.Name)($groupFilteringDN) $isMemberInAD = IsMemberOfGroupInAD($groupADobject[0])([String] $AdObject["distinguishedname"]) $isMemberInCS = IsMemberOfGroupInCS($groupCSobject)($adObjectCSobject) if ($isMemberInAD -and $isMemberInCS) { Write-Host "No issues detected. The object `"$([String] $AdObject["distinguishedname"])`" is a member of group `"$groupFilteringDN`" that is configured for group filtering." ReportOutput -PropertyName "Group Filtered" -PropertyValue "False" } else { WriteEventLog($EventIdGroupFiltered)($EventMsgGroupFiltered -f ([String] $AdObject["distinguishedname"], $AdConnector.Identifier, $groupFilteringDN)) $SynchronizationIssueList.Add($global:GroupFilteringIssue) Write-Host "`r`n" "GROUP FILTERING - ANALYSIS:" | Write-Host -fore Cyan "---------------------------" | Write-Host -fore Cyan $htmlMessage = WriteHtmlMessage -message "Analysis:" -color "#252525" -fontSize "16px" -fontWeight "700" -numberOfLineBreaks 1 $htmlMessageList.Add($htmlMessage) if (!$isMemberInAD) { $consoleMessage = "Object is not present in sync scope as group filtering is enabled and the object is not a member of filtering group `"$groupFilteringDN`"." $consoleMessage | ReportError -PropertyName "Group Filtered" -PropertyValue "True" } else { $consoleMessage = "Object is a member of the filtering group `"$groupFilteringDN`" in the Active Directory but is not a member of the group in the AD Connector Space." $consoleMessage | ReportError -PropertyName "Group Filtered" -PropertyValue "True" } Write-Host "`r`n" $htmlMessage = WriteHtmlMessage -message $consoleMessage -color "#252525" -numberOfLineBreaks 2 $htmlMessageList.Add($htmlMessage) "GROUP FILTERING - RECOMMENDED ACTIONS:" | Write-Host -fore Cyan "--------------------------------------" | Write-Host -fore Cyan $htmlMessage = WriteHtmlMessage -message "Recommended Actions:" -color "#252525" -fontSize "16px" -fontWeight "700" -numberOfLineBreaks 1 $htmlMessageList.Add($htmlMessage) if (!$isMemberInAD) { $message = "Add the object to group `"$groupFilteringDN`" or change group filtering settings. For more information on group filtering, please see:" $consoleMessage = GetConsoleMessageWithLink($message)($global:ConfigureGroupSyncFilteringUrl) $consoleMessage | Write-Host -fore Yellow $htmlMessage = GetHtmlMessageWithLink($message)($global:ConfigureGroupSyncFilteringUrl)($global:ConfigureGroupSyncFilteringText) } else { $message = "Ensure an import operation has been run on the AD Connector since the object was added to group `"$groupFilteringDN`" and that there were no errors during the import. More information on operations can be found here:" $consoleMessage = GetConsoleMessageWithLink($message)($global:OperationsTabUrl) $consoleMessage | Write-Host -fore Yellow $htmlMessage = GetHtmlMessageWithLink($message)($global:OperationsTabUrl)($global:OperationsTabText) } Write-Host "`r`n" $htmlMessage = WriteHtmlMessage -message $htmlMessage -color "#252525" -numberOfLineBreaks 0 $htmlMessageList.Add($htmlMessage) $groupFilteringHtmlItem = WriteHtmlAccordionItemForParagraph($htmlMessageList)("Group Filtering") $SynchronizationIssueHtmlItems.Add($groupFilteringHtmlItem) } } else { Write-Host "No issues detected - group filtering is not in use." ReportOutput -PropertyName "Group Filtered" -PropertyValue "False" } Write-Host "`r`n" } Function IsMemberOfGroupInAD { param ( [System.Collections.Generic.Dictionary[[String], [Object]]] [parameter(mandatory=$true)] $AdObject, [string] [parameter(mandatory=$true)] $MemberDN ) # # AD object attribute "member" is a multi-valued attribute. # $members = [System.Collections.ArrayList] $AdObject["member"] foreach ($member in $members) { if ($member -eq $MemberDN) { return $true } } return $false } Function IsMemberOfGroupInCS { param ( [Microsoft.IdentityManagement.PowerShell.ObjectModel.CsObject] [parameter(mandatory=$true)] [AllowNull()] $AdCsGroupObject, [Microsoft.IdentityManagement.PowerShell.ObjectModel.CsObject] [parameter(mandatory=$true)] [AllowNull()] $AdCsMemberObject ) if ($AdCsGroupObject -ne $null -and $AdCsMemberObject -ne $null) { $members = [System.Collections.Generic.List[String]]$AdCsGroupObject.Attributes["member"].Values foreach ($member in $members) { if ($member -eq $AdCsMemberObject.DistinguishedName) { return $true } } } return $false } Function IsMemberOfGroupInMV { param ( [Microsoft.IdentityManagement.PowerShell.ObjectModel.MvObject] [parameter(mandatory=$true)] [AllowNull()] $MvGroupObject, [Microsoft.IdentityManagement.PowerShell.ObjectModel.MvObject] [parameter(mandatory=$true)] [AllowNull()] $MvMemberObject ) if ($MvGroupObject -ne $null -and $MvMemberObject -ne $null) { $memberToCheck = [System.Guid]::Parse($MvMemberObject.ObjectId) $members = [System.Collections.Generic.List[String]]$MvGroupObject.Attributes["member"].Values foreach ($member in $members) { $actualMember = [System.Guid]::Parse($member) if ($actualMember.Equals($memberToCheck)) { return $true } } } return $false } Function GetObjectAllADAttributeHtmlGroup { param ( [System.Collections.Generic.Dictionary[[String], [Object]]] [parameter(mandatory=$true)] $AdObjectFromConnectorAccount, [System.Collections.Generic.Dictionary[[String], [Object]]] [parameter(mandatory=$true)] $AdObjectFromProvidedAccount, [String] [parameter(mandatory=$true)] $ConnectorAccountName, [String] [parameter(mandatory=$true)] $ProvidedAccountName ) $htmlItems = @() $tableHeaders = ("Attribute Name", "Attribute Value retrieved by $ProvidedAccountName", "Attribute Value retrieved by $ConnectorAccountName") # # Get all Attributes from objects retrieved by both the connector and provided account # $ProvidedAccountAttributesHashTable = ConvertADObjectToHashTable($AdObjectFromProvidedAccount)($true) $ConnectorAccountAttributesHashTable = ConvertADObjectToHashTable($AdObjectFromConnectorAccount)($true) $attributesCompareHtmlItem = WriteHtmlAccordionItemForTable($global:HtmlADAttributesComparisonTitle)("ADObjectAttributeComparisonTable")($tableHeaders)($ProvidedAccountAttributesHashTable)($global:HtmlADObjectType)($ConnectorAccountAttributesHashTable) $htmlItems += $attributesCompareHtmlItem if ($htmlItems.length -gt 0) { $htmlGroup = WriteHtmlAccordionGroup($htmlItems)($global:HtmlAttributeDetailsSectionTitle) } Write-Output $htmlGroup } Function GetUserObjectHtmlGroup { param ( [System.Collections.Generic.Dictionary[[String], [Object]]] [parameter(mandatory=$true)] [AllowNull()] $AdObject, [Microsoft.IdentityManagement.PowerShell.ObjectModel.CsObject] [parameter(mandatory=$true)] [AllowNull()] $AdCsObject, [Microsoft.IdentityManagement.PowerShell.ObjectModel.MvObject] [parameter(mandatory=$true)] [AllowNull()] $MvObject, [Microsoft.IdentityManagement.PowerShell.ObjectModel.CsObject] [parameter(mandatory=$true)] [AllowNull()] $AadCsObject, [Microsoft.Online.Administration.User] [parameter(mandatory=$true)] [AllowNull()] $AadUserObject ) $htmlItems = @() $tableHeaders = ("Attribute Name", "Attribute Value") # # On-Premises AD Object # if ($AdObject) { $AdObjectHashTable = ConvertADObjectToHashTable($AdObject) $AdObjectHtmlItem = WriteHtmlAccordionItemForTable($global:HtmlADObjectTitle)("ADObjectTable")($tableHeaders)($AdObjectHashTable)($global:HtmlADObjectType) $htmlItems += $AdObjectHtmlItem } # # Metaverse Object # if ($MvObject) { $MvObjectHashTable = ConvertMVObjectToHashTable($MvObject) $MvObjectHtmlItem = WriteHtmlAccordionItemForTable($global:HtmlAADConnectObjectTitle)("AADConnectObjectTable")($tableHeaders)($MvObjectHashTable)($global:HtmlAADConnectObjectType) $htmlItems += $MvObjectHtmlItem } # # Azure AD Object # if ($AadUserObject) { $AadUserObjectHashTable = ConvertAADUserObjectToHashTable($AadUserObject) $AadUserObjectHtmlItem = WriteHtmlAccordionItemForTable($global:HtmlAzureADObjectTitle)("AADObjectTable")($tableHeaders)($AadUserObjectHashTable)($global:HtmlAzureADObjectType) $htmlItems += $AadUserObjectHtmlItem } $htmlGroup = $null if ($htmlItems.length -gt 0) { $htmlGroup = WriteHtmlAccordionGroup($htmlItems)($global:HtmlObjectDetailsSectionTitle) } Write-Output $htmlGroup } Function GetGroupObjectHtmlGroup { param ( [System.Collections.Generic.Dictionary[[String], [Object]]] [parameter(mandatory=$true)] [AllowNull()] $AdObject, [Microsoft.IdentityManagement.PowerShell.ObjectModel.CsObject] [parameter(mandatory=$true)] [AllowNull()] $AdCsObject, [Microsoft.IdentityManagement.PowerShell.ObjectModel.MvObject] [parameter(mandatory=$true)] [AllowNull()] $MvObject, [Microsoft.IdentityManagement.PowerShell.ObjectModel.CsObject] [parameter(mandatory=$true)] [AllowNull()] $AadCsObject, [Microsoft.Online.Administration.Group] [parameter(mandatory=$true)] [AllowNull()] $AadGroupObject ) $htmlItems = @() $tableHeaders = ("Attribute Name", "Attribute Value") # # On-Premises AD Object # if ($AdObject) { $AdObjectHashTable = ConvertADObjectToHashTable($AdObject) $AdObjectHtmlItem = WriteHtmlAccordionItemForTable($global:HtmlADObjectTitle)("ADObjectTable")($tableHeaders)($AdObjectHashTable)($global:HtmlADObjectType) $htmlItems += $AdObjectHtmlItem } # # Metaverse Object # if ($MvObject) { $MvObjectHashTable = ConvertMVObjectToHashTable($MvObject) $MvObjectHtmlItem = WriteHtmlAccordionItemForTable($global:HtmlAADConnectObjectTitle)("AADConnectObjectTable")($tableHeaders)($MvObjectHashTable)($global:HtmlAADConnectObjectType) $htmlItems += $MvObjectHtmlItem } # # Azure AD Object # if ($AadGroupObject) { $AadGroupObjectHashTable = ConvertAADGroupObjectToHashTable($AadGroupObject) $AadGroupObjectHtmlItem = WriteHtmlAccordionItemForTable($global:HtmlAzureADObjectTitle)("AADObjectTable")($tableHeaders)($AadGroupObjectHashTable)($global:HtmlAzureADObjectType) $htmlItems += $AadGroupObjectHtmlItem } $htmlGroup = $null if ($htmlItems.length -gt 0) { $htmlGroup = WriteHtmlAccordionGroup($htmlItems)($global:HtmlObjectDetailsSectionTitle) } Write-Output $htmlGroup } Function GetContactObjectHtmlGroup { param ( [System.Collections.Generic.Dictionary[[String], [Object]]] [parameter(mandatory=$true)] [AllowNull()] $AdObject, [Microsoft.IdentityManagement.PowerShell.ObjectModel.CsObject] [parameter(mandatory=$true)] [AllowNull()] $AdCsObject, [Microsoft.IdentityManagement.PowerShell.ObjectModel.MvObject] [parameter(mandatory=$true)] [AllowNull()] $MvObject, [Microsoft.IdentityManagement.PowerShell.ObjectModel.CsObject] [parameter(mandatory=$true)] [AllowNull()] $AadCsObject, [Microsoft.Online.Administration.Contact] [parameter(mandatory=$true)] [AllowNull()] $AadContactObject ) $htmlItems = @() $tableHeaders = ("Attribute Name", "Attribute Value") # # On-Premises AD Object # if ($AdObject) { $AdObjectHashTable = ConvertADObjectToHashTable($AdObject) $AdObjectHtmlItem = WriteHtmlAccordionItemForTable($global:HtmlADObjectTitle)("ADObjectTable")($tableHeaders)($AdObjectHashTable)($global:HtmlADObjectType) $htmlItems += $AdObjectHtmlItem } # # Metaverse Object # if ($MvObject) { $MvObjectHashTable = ConvertMVObjectToHashTable($MvObject) $MvObjectHtmlItem = WriteHtmlAccordionItemForTable($global:HtmlAADConnectObjectTitle)("AADConnectObjectTable")($tableHeaders)($MvObjectHashTable)($global:HtmlAADConnectObjectType) $htmlItems += $MvObjectHtmlItem } # # Azure AD Object # if ($AadContactObject) { $AadContactObjectHashTable = ConvertAADContactObjectToHashTable($AadContactObject) $AadContactObjectHtmlItem = WriteHtmlAccordionItemForTable($global:HtmlAzureADObjectTitle)("AADObjectTable")($tableHeaders)($AadContactObjectHashTable)($global:HtmlAzureADObjectType) $htmlItems += $AadContactObjectHtmlItem } $htmlGroup = $null if ($htmlItems.length -gt 0) { $htmlGroup = WriteHtmlAccordionGroup($htmlItems)($global:HtmlObjectDetailsSectionTitle) } Write-Output $htmlGroup } Function GetConsoleMessageWithLink { param ( [string] [parameter(mandatory=$true)] $message, [string] [parameter(mandatory=$true)] $url ) $consoleMessage = $message $consoleMessage += " " $consoleMessage += $url Write-Output $consoleMessage } Function GetHtmlMessageWithLink { param ( [string] [parameter(mandatory=$true)] $message, [string] [parameter(mandatory=$true)] $url, [string] [parameter(mandatory=$true)] $text ) $hyperlink = WriteHyperlink($url)($text) $htmlMessage = $message $htmlMessage += " " $htmlMessage += $hyperlink Write-Output $htmlMessage } Function AskIfToolHelpful { param ( [string] [parameter(mandatory=$true)] $objectDN ) if ($isNonInteractiveMode) { return } foreach ($synchronizationIssue in $SynchronizationIssueList) { do { $answer = Read-Host "Did you find this tool helpful about the `"$($synchronizationIssue)`" issue? [y/n]" } while(($answer -ne 'y') -and ($answer -ne 'Y') -and ($answer -ne 'n') -and ($answer -ne 'N')) $eventMessage = "Object: " $eventMessage += $objectDN $eventMessage += "`n" $eventMessage += "Synchronization Issue: " $eventMessage += $synchronizationIssue $eventMessage += "`n" $eventMessage += "Is Tool Helpful: " $eventMessage += $answer WriteEventLog($EventIdIsToolHelpful)($eventMessage) Write-Host "`r`n" } } Function WriteTitle { param ( [string] [parameter(mandatory=$true)] $title ) $lineSizeWithoutText = 50 $lineSize = $title.Length + $lineSizeWithoutText # # Border line - top and bottom of the title # for ($i = 0; $i -lt $lineSize; $i++) { $borderLine += '=' } # # Middle line without text # $midLine = '=' for ($i = 0; $i -lt $lineSize-2; $i++) { $midLine += ' ' } $midLine += '=' # # Title line # $titleLine = '=' for ($i = 0; $i -lt ($lineSizeWithoutText-2)/2; $i++) { $titleLine += ' ' } $titleLine += $title for ($i = 0; $i -lt ($lineSizeWithoutText-2)/2; $i++) { $titleLine += ' ' } $titleLine += '=' # # Resulting Title # Write-Host $borderLine Write-Host $midLine Write-Host $titleLine Write-Host $midLine Write-Host $borderLine } # SIG # Begin signature block # MIInogYJKoZIhvcNAQcCoIInkzCCJ48CAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCC0H9lnZ1hwvjvm # sdKdzB3OUcmzXO/jyfBBpnMZZgRKvqCCDYIwggYAMIID6KADAgECAhMzAAADXJXz # SFtKBGrPAAAAAANcMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD # VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p # bmcgUENBIDIwMTEwHhcNMjMwNDA2MTgyOTIyWhcNMjQwNDAyMTgyOTIyWjB0MQsw # CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u # ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB # AQDijA1UCC84R0x+9Vr/vQhPNbfvIOBFfymE+kuP+nho3ixnjyv6vdnUpgmm6RT/ # pL9cXL27zmgVMw7ivmLjR5dIm6qlovdrc5QRrkewnuQHnvhVnLm+pLyIiWp6Tow3 # ZrkoiVdip47m+pOBYlw/vrkb8Pju4XdA48U8okWmqTId2CbZTd8yZbwdHb8lPviE # NMKzQ2bAjytWVEp3y74xc8E4P6hdBRynKGF6vvS6sGB9tBrvu4n9mn7M99rp//7k # ku5t/q3bbMjg/6L6mDePok6Ipb22+9Fzpq5sy+CkJmvCNGPo9U8fA152JPrt14uJ # ffVvbY5i9jrGQTfV+UAQ8ncPAgMBAAGjggF/MIIBezArBgNVHSUEJDAiBgorBgEE # AYI3TBMBBgorBgEEAYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQUXgIsrR+tkOQ8 # 10ekOnvvfQDgTHAwRQYDVR0RBD4wPKQ6MDgxHjAcBgNVBAsTFU1pY3Jvc29mdCBD # b3Jwb3JhdGlvbjEWMBQGA1UEBRMNMjMzMTEwKzUwMDg2ODAfBgNVHSMEGDAWgBRI # bmTlUAXTgqoXNzcitW2oynUClTBUBgNVHR8ETTBLMEmgR6BFhkNodHRwOi8vd3d3 # Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NybC9NaWNDb2RTaWdQQ0EyMDExXzIwMTEt # MDctMDguY3JsMGEGCCsGAQUFBwEBBFUwUzBRBggrBgEFBQcwAoZFaHR0cDovL3d3 # dy5taWNyb3NvZnQuY29tL3BraW9wcy9jZXJ0cy9NaWNDb2RTaWdQQ0EyMDExXzIw # MTEtMDctMDguY3J0MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggIBABIm # T2UTYlls5t6i5kWaqI7sEfIKgNquF8Ex9yMEz+QMmc2FjaIF/HQQdpJZaEtDM1Xm # 07VD4JvNJEplZ91A4SIxjHzqgLegfkyc384P7Nn+SJL3XK2FK+VAFxdvZNXcrkt2 # WoAtKo0PclJOmHheHImWSqfCxRispYkKT9w7J/84fidQxSj83NPqoCfUmcy3bWKY # jRZ6PPDXlXERRvl825dXOfmCKGYJXHKyOEcU8/6djs7TDyK0eH9ss4G9mjPnVZzq # Gi/qxxtbddZtkREDd0Acdj947/BTwsYLuQPz7SNNUAmlZOvWALPU7OOVQlEZzO8u # Ec+QH24nep/yhKvFYp4sHtxUKm1ZPV4xdArhzxJGo48Be74kxL7q2AlTyValLV98 # u3FY07rNo4Xg9PMHC6sEAb0tSplojOHFtGtNb0r+sioSttvd8IyaMSfCPwhUxp+B # Td0exzQ1KnRSBOZpxZ8h0HmOlMJOInwFqrCvn5IjrSdjxKa/PzOTFPIYAfMZ4hJn # uKu15EUuv/f0Tmgrlfw+cC0HCz/5WnpWiFso2IPHZyfdbbOXO2EZ9gzB1wmNkbBz # hj8hFyImnycY+94Eo2GLavVTtgBiCcG1ILyQabKDbL7Vh/OearAxcRAmcuVAha07 # WiQx2aLghOSaZzKFOx44LmwUxRuaJ4vO/PRZ7EzAMIIHejCCBWKgAwIBAgIKYQ6Q # 0gAAAAAAAzANBgkqhkiG9w0BAQsFADCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgT # Cldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29m # dCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3QgQ2VydGlmaWNh # dGUgQXV0aG9yaXR5IDIwMTEwHhcNMTEwNzA4MjA1OTA5WhcNMjYwNzA4MjEwOTA5 # WjB+MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMH # UmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSgwJgYDVQQD # Ex9NaWNyb3NvZnQgQ29kZSBTaWduaW5nIFBDQSAyMDExMIICIjANBgkqhkiG9w0B # AQEFAAOCAg8AMIICCgKCAgEAq/D6chAcLq3YbqqCEE00uvK2WCGfQhsqa+laUKq4 # BjgaBEm6f8MMHt03a8YS2AvwOMKZBrDIOdUBFDFC04kNeWSHfpRgJGyvnkmc6Whe # 0t+bU7IKLMOv2akrrnoJr9eWWcpgGgXpZnboMlImEi/nqwhQz7NEt13YxC4Ddato # 88tt8zpcoRb0RrrgOGSsbmQ1eKagYw8t00CT+OPeBw3VXHmlSSnnDb6gE3e+lD3v # ++MrWhAfTVYoonpy4BI6t0le2O3tQ5GD2Xuye4Yb2T6xjF3oiU+EGvKhL1nkkDst # rjNYxbc+/jLTswM9sbKvkjh+0p2ALPVOVpEhNSXDOW5kf1O6nA+tGSOEy/S6A4aN # 91/w0FK/jJSHvMAhdCVfGCi2zCcoOCWYOUo2z3yxkq4cI6epZuxhH2rhKEmdX4ji # JV3TIUs+UsS1Vz8kA/DRelsv1SPjcF0PUUZ3s/gA4bysAoJf28AVs70b1FVL5zmh # D+kjSbwYuER8ReTBw3J64HLnJN+/RpnF78IcV9uDjexNSTCnq47f7Fufr/zdsGbi # wZeBe+3W7UvnSSmnEyimp31ngOaKYnhfsi+E11ecXL93KCjx7W3DKI8sj0A3T8Hh # hUSJxAlMxdSlQy90lfdu+HggWCwTXWCVmj5PM4TasIgX3p5O9JawvEagbJjS4NaI # jAsCAwEAAaOCAe0wggHpMBAGCSsGAQQBgjcVAQQDAgEAMB0GA1UdDgQWBBRIbmTl # UAXTgqoXNzcitW2oynUClTAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTALBgNV # HQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBRyLToCMZBDuRQF # TuHqp8cx0SOJNDBaBgNVHR8EUzBRME+gTaBLhklodHRwOi8vY3JsLm1pY3Jvc29m # dC5jb20vcGtpL2NybC9wcm9kdWN0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFfMDNf # MjIuY3JsMF4GCCsGAQUFBwEBBFIwUDBOBggrBgEFBQcwAoZCaHR0cDovL3d3dy5t # aWNyb3NvZnQuY29tL3BraS9jZXJ0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFfMDNf # MjIuY3J0MIGfBgNVHSAEgZcwgZQwgZEGCSsGAQQBgjcuAzCBgzA/BggrBgEFBQcC # ARYzaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9kb2NzL3ByaW1hcnlj # cHMuaHRtMEAGCCsGAQUFBwICMDQeMiAdAEwAZQBnAGEAbABfAHAAbwBsAGkAYwB5 # AF8AcwB0AGEAdABlAG0AZQBuAHQALiAdMA0GCSqGSIb3DQEBCwUAA4ICAQBn8oal # mOBUeRou09h0ZyKbC5YR4WOSmUKWfdJ5DJDBZV8uLD74w3LRbYP+vj/oCso7v0ep # o/Np22O/IjWll11lhJB9i0ZQVdgMknzSGksc8zxCi1LQsP1r4z4HLimb5j0bpdS1 # HXeUOeLpZMlEPXh6I/MTfaaQdION9MsmAkYqwooQu6SpBQyb7Wj6aC6VoCo/KmtY # SWMfCWluWpiW5IP0wI/zRive/DvQvTXvbiWu5a8n7dDd8w6vmSiXmE0OPQvyCInW # H8MyGOLwxS3OW560STkKxgrCxq2u5bLZ2xWIUUVYODJxJxp/sfQn+N4sOiBpmLJZ # iWhub6e3dMNABQamASooPoI/E01mC8CzTfXhj38cbxV9Rad25UAqZaPDXVJihsMd # YzaXht/a8/jyFqGaJ+HNpZfQ7l1jQeNbB5yHPgZ3BtEGsXUfFL5hYbXw3MYbBL7f # QccOKO7eZS/sl/ahXJbYANahRr1Z85elCUtIEJmAH9AAKcWxm6U/RXceNcbSoqKf # enoi+kiVH6v7RyOA9Z74v2u3S5fi63V4GuzqN5l5GEv/1rMjaHXmr/r8i+sLgOpp # O6/8MO0ETI7f33VtY5E90Z1WTk+/gFcioXgRMiF670EKsT/7qMykXcGhiJtXcVZO # SEXAQsmbdlsKgEhr/Xmfwb1tbWrJUnMTDXpQzTGCGXYwghlyAgEBMIGVMH4xCzAJ # BgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25k # MR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jv # c29mdCBDb2RlIFNpZ25pbmcgUENBIDIwMTECEzMAAANclfNIW0oEas8AAAAAA1ww # DQYJYIZIAWUDBAIBBQCgga4wGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYK # KwYBBAGCNwIBCzEOMAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEIJwuY4Do # oqbDuVQhI1poQ+6ALeONOzCIN7+typiYrhFzMEIGCisGAQQBgjcCAQwxNDAyoBSA # EgBNAGkAYwByAG8AcwBvAGYAdKEagBhodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20w # DQYJKoZIhvcNAQEBBQAEggEAomik0BRSUlsWpp7ZS3TSMGUEetp/HYxSCj0Q8M9B # hUOa1TY09z58VUDCBImCpyFl3LJda2PdHk9ERX0BFC0A+GumGXXRrRtfpV+ebAzB # SL96Ocd3UnlkBgwfPYDTyHnQfjv9TGn2xe6TM1tOVKQhab5GtM3bttJzgtGMDvD3 # 6K+r1OnAsGKPMGf+6tebIpb6+VCzfMCHu+0EdLQ9RvtLswUdcFWBYqA95/04NDdJ # 1JvoIS4pymfO7NNNJSZlO7aPsXySqctlgSEmWl1U9clpaM7aTqKeTVbUC1jmoTwZ # ebN6rzWz9PLOKUrLHSDmEyo7fwebRKeQWZVfpl4jiZWo3qGCFwAwghb8BgorBgEE # AYI3AwMBMYIW7DCCFugGCSqGSIb3DQEHAqCCFtkwghbVAgEDMQ8wDQYJYIZIAWUD # BAIBBQAwggFRBgsqhkiG9w0BCRABBKCCAUAEggE8MIIBOAIBAQYKKwYBBAGEWQoD # ATAxMA0GCWCGSAFlAwQCAQUABCDA7BLvqJ7GfO4al0vyBFzYCCcaVh6jIDAD/biq # etbsrgIGZF1oa0FqGBMyMDIzMDUxNzIyNTcxNC43NzVaMASAAgH0oIHQpIHNMIHK # MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVk # bW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSUwIwYDVQQLExxN # aWNyb3NvZnQgQW1lcmljYSBPcGVyYXRpb25zMSYwJAYDVQQLEx1UaGFsZXMgVFNT # IEVTTjo4QTgyLUUzNEYtOUREQTElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3Rh # bXAgU2VydmljZaCCEVcwggcMMIIE9KADAgECAhMzAAABwvp9hw5UU0ckAAEAAAHC # MA0GCSqGSIb3DQEBCwUAMHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5n # dG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9y # YXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwMB4X # DTIyMTEwNDE5MDEyOFoXDTI0MDIwMjE5MDEyOFowgcoxCzAJBgNVBAYTAlVTMRMw # EQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVN # aWNyb3NvZnQgQ29ycG9yYXRpb24xJTAjBgNVBAsTHE1pY3Jvc29mdCBBbWVyaWNh # IE9wZXJhdGlvbnMxJjAkBgNVBAsTHVRoYWxlcyBUU1MgRVNOOjhBODItRTM0Ri05 # RERBMSUwIwYDVQQDExxNaWNyb3NvZnQgVGltZS1TdGFtcCBTZXJ2aWNlMIICIjAN # BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAtfEJvPKOSFn3petp9wco29/UoJmD # DyHpmmpRruRVWBF37By0nvrszScOV/K+LvHWWWC4S9cme4P63EmNhxTN/k2CgPnI # t/sDepyACSkya4ukqc1sT2I+0Uod0xjy9K2+jLH8UNb9vM3yH/vCYnaJSUqgtqZU # ly82pgYSB6tDeZIYcQoOhTI+M1HhRxmxt8RaAKZnDnXgLdkhnIYDJrRkQBpIgaht # ExtTuOkmVp2y8YCoFPaUhUD2JT6hPiDD7qD7A77PLpFzD2QFmNezT8aHHhKsVBuJ # MLPXZO1k14j0/k68DZGts1YBtGegXNkyvkXSgCCxt3Q8WF8laBXbDnhHaDLBhCOB # aZQ8jqcFUx8ZJSXQ8sbvEnmWFZmgM93B9P/JTFTF6qBVFMDd/V0PBbRQC2TctZH4 # bfv+jyWvZOeFz5yltPLRxUqBjv4KHIaJgBhU2ntMw4H0hpm4B7s6LLxkTsjLsajj # CJI8PiKi/mPKYERdmRyvFL8/YA/PdqkIwWWg2Tj5tyutGFtfVR+6GbcCVhijjy7l # 7otxa/wYVSX66Lo0alaThjc+uojVwH4psL+A1qvbWDB9swoKla20eZubw7fzCpFe # 6qs++G01sst1SaA0GGmzuQCd04Ue1eH3DFRDZPsN+aWvA455Qmd9ZJLGXuqnBo4B # XwVxdWZNj6+b4P8CAwEAAaOCATYwggEyMB0GA1UdDgQWBBRGsYh76V41aUCRXE9W # vD++sIfGajAfBgNVHSMEGDAWgBSfpxVdAF5iXYP05dJlpxtTNRnpcjBfBgNVHR8E # WDBWMFSgUqBQhk5odHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NybC9N # aWNyb3NvZnQlMjBUaW1lLVN0YW1wJTIwUENBJTIwMjAxMCgxKS5jcmwwbAYIKwYB # BQUHAQEEYDBeMFwGCCsGAQUFBzAChlBodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20v # cGtpb3BzL2NlcnRzL01pY3Jvc29mdCUyMFRpbWUtU3RhbXAlMjBQQ0ElMjAyMDEw # KDEpLmNydDAMBgNVHRMBAf8EAjAAMBMGA1UdJQQMMAoGCCsGAQUFBwMIMA0GCSqG # SIb3DQEBCwUAA4ICAQARdu3dCkcLLPfaJ3rR1M7D9jWHvneffkmXvFIJtqxHGWM1 # oqAh+bqxpI7HZz2MeNhh1Co+E9AabOgj94Sp1seXxdWISJ9lRGaAAWzA873aTB3/ # SjwuGqbqQuAvUzBFCO40UJ9anpavkpq/0nDqLb7XI5H+nsmjFyu8yqX1PMmnb4s1 # fbc/F30ijaASzqJ+p5rrgYWwDoMihM5bF0Y0riXihwE7eTShak/EwcxRmG3h+OT+ # Ox8KOLuLqwFFl1siTeQCp+YSt4J1tWXapqGJDlCbYr3Rz8+ryTS8CoZAU0vSHCOQ # cq12Th81p7QlHZv9cTRDhZg2TVyg8Gx3X6mkpNOXb56QUohI3Sn39WQJwjDn74J0 # aVYMai8mY6/WOurKMKEuSNhCiei0TK68vOY7sH0XEBWnRSbVefeStDo94UIUVTwd # 2HmBEfY8kfryp3RlA9A4FvfUvDHMaF9BtvU/pK6d1CdKG29V0WN3uVzfYETJoRpj # LYFGq0MvK6QVMmuNxk3bCRfj1acSWee14UGjglxWwvyOfNJe3pxcNFOd8Hhyp9d4 # AlQGVLNotaFvopgPLeJwUT3dl5VaAAhMwvIFmqwsffQy93morrprcnv74r5g3ejC # 39NYpFEoy+qmzLW1jFa1aXE2Xb/KZw2yawqldSp0Hu4VEkjGxFNc+AztIUWwmTCC # B3EwggVZoAMCAQICEzMAAAAVxedrngKbSZkAAAAAABUwDQYJKoZIhvcNAQELBQAw # gYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdS # ZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xMjAwBgNVBAMT # KU1pY3Jvc29mdCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAyMDEwMB4XDTIx # MDkzMDE4MjIyNVoXDTMwMDkzMDE4MzIyNVowfDELMAkGA1UEBhMCVVMxEzARBgNV # BAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jv # c29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAg # UENBIDIwMTAwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDk4aZM57Ry # IQt5osvXJHm9DtWC0/3unAcH0qlsTnXIyjVX9gF/bErg4r25PhdgM/9cT8dm95VT # cVrifkpa/rg2Z4VGIwy1jRPPdzLAEBjoYH1qUoNEt6aORmsHFPPFdvWGUNzBRMhx # XFExN6AKOG6N7dcP2CZTfDlhAnrEqv1yaa8dq6z2Nr41JmTamDu6GnszrYBbfowQ # HJ1S/rboYiXcag/PXfT+jlPP1uyFVk3v3byNpOORj7I5LFGc6XBpDco2LXCOMcg1 # KL3jtIckw+DJj361VI/c+gVVmG1oO5pGve2krnopN6zL64NF50ZuyjLVwIYwXE8s # 4mKyzbnijYjklqwBSru+cakXW2dg3viSkR4dPf0gz3N9QZpGdc3EXzTdEonW/aUg # fX782Z5F37ZyL9t9X4C626p+Nuw2TPYrbqgSUei/BQOj0XOmTTd0lBw0gg/wEPK3 # Rxjtp+iZfD9M269ewvPV2HM9Q07BMzlMjgK8QmguEOqEUUbi0b1qGFphAXPKZ6Je # 1yh2AuIzGHLXpyDwwvoSCtdjbwzJNmSLW6CmgyFdXzB0kZSU2LlQ+QuJYfM2BjUY # hEfb3BvR/bLUHMVr9lxSUV0S2yW6r1AFemzFER1y7435UsSFF5PAPBXbGjfHCBUY # P3irRbb1Hode2o+eFnJpxq57t7c+auIurQIDAQABo4IB3TCCAdkwEgYJKwYBBAGC # NxUBBAUCAwEAATAjBgkrBgEEAYI3FQIEFgQUKqdS/mTEmr6CkTxGNSnPEP8vBO4w # HQYDVR0OBBYEFJ+nFV0AXmJdg/Tl0mWnG1M1GelyMFwGA1UdIARVMFMwUQYMKwYB # BAGCN0yDfQEBMEEwPwYIKwYBBQUHAgEWM2h0dHA6Ly93d3cubWljcm9zb2Z0LmNv # bS9wa2lvcHMvRG9jcy9SZXBvc2l0b3J5Lmh0bTATBgNVHSUEDDAKBggrBgEFBQcD # CDAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTALBgNVHQ8EBAMCAYYwDwYDVR0T # AQH/BAUwAwEB/zAfBgNVHSMEGDAWgBTV9lbLj+iiXGJo0T2UkFvXzpoYxDBWBgNV # HR8ETzBNMEugSaBHhkVodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20vcGtpL2NybC9w # cm9kdWN0cy9NaWNSb29DZXJBdXRfMjAxMC0wNi0yMy5jcmwwWgYIKwYBBQUHAQEE # TjBMMEoGCCsGAQUFBzAChj5odHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpL2Nl # cnRzL01pY1Jvb0NlckF1dF8yMDEwLTA2LTIzLmNydDANBgkqhkiG9w0BAQsFAAOC # AgEAnVV9/Cqt4SwfZwExJFvhnnJL/Klv6lwUtj5OR2R4sQaTlz0xM7U518JxNj/a # ZGx80HU5bbsPMeTCj/ts0aGUGCLu6WZnOlNN3Zi6th542DYunKmCVgADsAW+iehp # 4LoJ7nvfam++Kctu2D9IdQHZGN5tggz1bSNU5HhTdSRXud2f8449xvNo32X2pFaq # 95W2KFUn0CS9QKC/GbYSEhFdPSfgQJY4rPf5KYnDvBewVIVCs/wMnosZiefwC2qB # woEZQhlSdYo2wh3DYXMuLGt7bj8sCXgU6ZGyqVvfSaN0DLzskYDSPeZKPmY7T7uG # +jIa2Zb0j/aRAfbOxnT99kxybxCrdTDFNLB62FD+CljdQDzHVG2dY3RILLFORy3B # FARxv2T5JL5zbcqOCb2zAVdJVGTZc9d/HltEAY5aGZFrDZ+kKNxnGSgkujhLmm77 # IVRrakURR6nxt67I6IleT53S0Ex2tVdUCbFpAUR+fKFhbHP+CrvsQWY9af3LwUFJ # fn6Tvsv4O+S3Fb+0zj6lMVGEvL8CwYKiexcdFYmNcP7ntdAoGokLjzbaukz5m/8K # 6TT4JDVnK+ANuOaMmdbhIurwJ0I9JZTmdHRbatGePu1+oDEzfbzL6Xu/OHBE0ZDx # yKs6ijoIYn/ZcGNTTY3ugm2lBRDBcQZqELQdVTNYs6FwZvKhggLOMIICNwIBATCB # +KGB0KSBzTCByjELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAO # BgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEl # MCMGA1UECxMcTWljcm9zb2Z0IEFtZXJpY2EgT3BlcmF0aW9uczEmMCQGA1UECxMd # VGhhbGVzIFRTUyBFU046OEE4Mi1FMzRGLTlEREExJTAjBgNVBAMTHE1pY3Jvc29m # dCBUaW1lLVN0YW1wIFNlcnZpY2WiIwoBATAHBgUrDgMCGgMVAMp1N1VLhPMvWXEo # ZfmF4apZlnRUoIGDMIGApH4wfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hp # bmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jw # b3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTAw # DQYJKoZIhvcNAQEFBQACBQDoD8/LMCIYDzIwMjMwNTE4MDYxMjI3WhgPMjAyMzA1 # MTkwNjEyMjdaMHcwPQYKKwYBBAGEWQoEATEvMC0wCgIFAOgPz8sCAQAwCgIBAAIC # Bv4CAf8wBwIBAAICEbswCgIFAOgRIUsCAQAwNgYKKwYBBAGEWQoEAjEoMCYwDAYK # KwYBBAGEWQoDAqAKMAgCAQACAwehIKEKMAgCAQACAwGGoDANBgkqhkiG9w0BAQUF # AAOBgQAOcEM/tGVYofgsVgmB7+n9LL+gyOSZJeGjbYdz/brGNvqgplomk5u+xahu # MEp+QnDQHmvhm2qIT1WX4XPuhOkJMCGoGaZwlYVDicgU4+FCl0xyidK8NLKdiLeD # 3BhHPYDHSBzXLc4kQjHmZl3qImC8rdx28Lq4wZKwsOhWiS8yhTGCBA0wggQJAgEB # MIGTMHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQH # EwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNV # BAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwAhMzAAABwvp9hw5UU0ck # AAEAAAHCMA0GCWCGSAFlAwQCAQUAoIIBSjAaBgkqhkiG9w0BCQMxDQYLKoZIhvcN # AQkQAQQwLwYJKoZIhvcNAQkEMSIEIK6TsxLODdTnHmLTFQYrvEyQS8yH6Mnw9EBw # u9fHlaV4MIH6BgsqhkiG9w0BCRACLzGB6jCB5zCB5DCBvQQgypNgW8fpsMV57r0F # 5beUuiEVOVe4BdmaO+e28mGDUBYwgZgwgYCkfjB8MQswCQYDVQQGEwJVUzETMBEG # A1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWlj # cm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFt # cCBQQ0EgMjAxMAITMwAAAcL6fYcOVFNHJAABAAABwjAiBCCb5c/0EjdIPNI3b3R2 # wqfJX2Pf2Bx1/pzWJ/cik6fIMDANBgkqhkiG9w0BAQsFAASCAgAURSgcZFmhlu76 # Rqtv5eyZqAs1tSgv2LLg/ZTh8WXUe8NWaqqLk2PfsItCryU/BdB7WA4SYEHPNQgQ # ZThMgouMbO7WDY/7cFzE3/12vXUFotXhXXqETIKOnHbqKRkZrXB4HkVI2iHqQ7dD # eNNYqqtYQuSW30MqtONaVOGu4liF0i69Fk4uaO2ZjLJmffx104nXHdw+0zxewPfn # n7p6JZKEjxQZW03B2T0PqaIJEUBfSleEkQ7S+USvswOVqqVy5mS8FlAE6lbcAx/X # SheWK1SEvR9kjzuZ93aeCqFsIMFonxThcnKNpS0hglf9VH/Yhp1hyJvc0cdh97wt # Dk+O39ksxUm4zEN3Ftvb/5fHxewOTawDNXSmk38trFwzoRuNgM53viJAps3fE18b # MOC+ZK5kz6NLJtvrM7eYXPIs3goJUhf5WMrPIhniqAhhlfkj+7CFJn5LiXeC7N8t # t4mQXqNhPdXifmxrMcQxv3OG6Q/tIh8lGwGvl7W6vbsjAe8ClfaXFNWoXDpLUvJQ # kNBdqfZvIq9WxeQbcFMP+8a2XDuR48S1ae4/8z+Tg9KKIijB4JuzKC86ZZPf0FQA # V74lZ8cvhKHSK9fcj2ugjWcPXZhblY/UOsTEXUJ17kFKphBCrONd+5TzdwOBUHHV # tsJaBJCBJE7GYfu1Qgg6iE+/P9TKcQ== # SIG # End signature block |