In Part 1 of this series, we learned of the possible issues with matched objects in a multi-forest topology. In Part 2, we will expand on the design and explain how this can be deployed even in production.
Before we proceed, it must be stated that creating custom rules in AD Connect can have a profound impact on your environment. We recommend rigorous testing in a lab environment and further testing in pre-production. The procedure suggested here is provided as-is and we will not detail every step for deploying custom rules. As recommended by Microsoft please read Recommended Documents to gain expertise in synchronization rules.
Now that the legal stuff is out of the way, let’s remind ourselves why we are here?
When Azure AD Connect merges objects between forests such as a mail contact or a mailbox, only one value is sent to Azure AD. The impact? For users who have been utilizing the object in the other forest, they will encounter issues when their mailbox is migrated to Office 365.
In Part 1, we detailed the required steps to resolve this issue. First, create a new multi-value indexable attribute named onPremiseCustomAddresses in the metaverse. Second, populate this new attribute with x500 addresses from each forest by transforming the LegacyExchangeDN and striping the ProxyAddresses. Finally, we push these values back to all forests.
To implement the solution, we will need to create:
- A new attribute in the AD Connect Metaverse
- 3 new inbound rules per forest (Arrows in Yellow)
- 2 new Outbound rules per forest (Arrows in Green)
Step 1: Create a Metaverse Attribute
Open AD Connect Synchronization Service Manager. Select Metaverse Designer, person, Add Attribute then New Attribute.
Attribute name: onPremiseCustomAddresses
Attribute type: String (indexable)
Multi-valued: Enabled
Step 2: Inbound Custom Rules
- Let’s first disable the sync engine by running the below CMDlet on the AD Connect server
ADSyncScheduler -SyncCycleEnabled $false
- Let’s add the first of three rules for the forest corp.contoso.com and remember you will need to repeat these rules for each forest
- In from AD – Contact Custom Addresses
- From the Sync Rules Editor, select Inbound under Direction
- From the Sync Rules Editor, select the forest corp.contoso.com under Connector
- Click Add New Rule
- Enter a Name, we chose: ‘In from AD – Contact Custom Addresses’
- Click Description
- Under connected system, select the forest you would like to target ‘corp.contoso.com‘
- Connected System object type Contact
- Choose person as the Metaverse object type
- Select Join as the link type
- Enter a precedence value
- Click Next
- In the scoping filter area, select
- Add Clause. Under attribute, choose Mail and under Operator, select ISNOTNULL and true in the value Textbox
- Click Next
- Click Next on the Join rules page without entering anything
- Add a transformation:
- type: expression
- target attribute: onPremisesCustomAddresses
- source:
IIF(InStr([proxyAddresses],”smtp:”,1,vbTextCompare)>0 ||
InStr([proxyAddresses],”SMTP:”,1,vbTextCompare)>0 ||
InStr([proxyAddresses],”sip:”,1,vbTextCompare)>0 ||
InStr([proxyAddresses],”SIP:”,1,vbTextCompare)>0 ||
InStr([proxyAddresses],”x400:”,1,vbTextCompare)>0 ||
InStr([proxyAddresses],”X400:”,1,vbTextCompare)>0 ||
InStr([proxyAddresses],”ms:”,1,vbTextCompare)>0 ||
InStr([proxyAddresses],”MS:”,1,vbTextCompare)>0 ||
InStr([proxyAddresses],”ccmail:”,1,vbTextCompare)>0 ||
InStr([proxyAddresses],”CCMAIL:”,1,vbTextCompare)>0 ||
InStr([proxyAddresses],”eum:”,1,vbTextCompare)>0 ||
InStr([proxyAddresses],”EUM:”,1,vbTextCompare)>0 ||
InStr([proxyAddresses],”xmpp:”,1,vbTextCompare)>0 ||
InStr([proxyAddresses],”XMPP:”,1,vbTextCompare)>0 ||
InStr([proxyAddresses],”gwise:”,1,vbTextCompare)>0 ||
InStr([proxyAddresses],”GWISE:”,1,vbTextCompare)>0 ||
InStr([proxyAddresses],”fax:”,1,vbTextCompare)>0 ||
InStr([proxyAddresses],”FAX:”,1,vbTextCompare)>0 ||
InStr([proxyAddresses],”facsys:”,1,vbTextCompare)>0 ||
InStr([proxyAddresses],”FACSYS:”,1,vbTextCompare)>0 ||
InStr([proxyAddresses],”x500:/o=ExchangeLabs”,1,vbTextCompare)>0 ,NULL,[proxyAddresses])
- merge type: MergeCaseInsensitive
- Click Add
- Now for the second of three rules for the forest corp.contoso.com
- In from AD – User Custom Addresses
- From the Sync Rules Editor, select Inbound under Direction
- From the Sync Rules Editor, select the forest corp.contoso.com under Connector
- Click Add New Rule
- Enter a Name, we chose: ‘In from AD – Contact Custom Addresses’
- Description
- Under connected system, select the forest you would like to target ‘corp.contoso.com‘
- Connected System object type User
- Choose person as the Metaverse object type
- Select Join as the link type
- Enter a precedence value
- Click Next
- In the scoping filter area, select
- Add Clause. Under attribute, choose Mail and under Operator, select ISNOTNULL and true in the value Textbox
- Click Next
- Click Next on the Join rules page without entering anything
- Add a transformation:
- type: expression
- target attribute: onPremisesCustomAddresses
- source:
IIF(InStr([proxyAddresses],”smtp:”,1,vbTextCompare)>0 ||
InStr([proxyAddresses],”SMTP:”,1,vbTextCompare)>0 ||
InStr([proxyAddresses],”sip:”,1,vbTextCompare)>0 ||
InStr([proxyAddresses],”SIP:”,1,vbTextCompare)>0 ||
InStr([proxyAddresses],”x400:”,1,vbTextCompare)>0 ||
InStr([proxyAddresses],”X400:”,1,vbTextCompare)>0 ||
InStr([proxyAddresses],”ms:”,1,vbTextCompare)>0 ||
InStr([proxyAddresses],”MS:”,1,vbTextCompare)>0 ||
InStr([proxyAddresses],”ccmail:”,1,vbTextCompare)>0 ||
InStr([proxyAddresses],”CCMAIL:”,1,vbTextCompare)>0 ||
InStr([proxyAddresses],”eum:”,1,vbTextCompare)>0 ||
InStr([proxyAddresses],”EUM:”,1,vbTextCompare)>0 ||
InStr([proxyAddresses],”xmpp:”,1,vbTextCompare)>0 ||
InStr([proxyAddresses],”XMPP:”,1,vbTextCompare)>0 ||
InStr([proxyAddresses],”gwise:”,1,vbTextCompare)>0 ||
InStr([proxyAddresses],”GWISE:”,1,vbTextCompare)>0 ||
InStr([proxyAddresses],”fax:”,1,vbTextCompare)>0 ||
InStr([proxyAddresses],”FAX:”,1,vbTextCompare)>0 ||
InStr([proxyAddresses],”facsys:”,1,vbTextCompare)>0 ||
InStr([proxyAddresses],”FACSYS:”,1,vbTextCompare)>0 ||
InStr([proxyAddresses],”x500:/o=ExchangeLabs”,1,vbTextCompare)>0 ,NULL,[proxyAddresses])
- merge type: MergeCaseInsensitive
- Click Add
- Finally, for the last inbound rule, we will add the LegacyExchangeDN for contact objects as a x500 address in our custom value in the metaverse
- In from AD – LegacyExchangeDN
- From the Sync Rules Editor, select Inbound under Direction
- From the Sync Rules Editor, select the forest corp.contoso.com under Connector
- Click Add New Rule
- Enter a Name, we chose: ‘In from AD – Contact Custom Addresses’
- Go to Description
- Under the connected system, select the forest you would like to target ‘corp.contoso.com‘.
- Connected System object type Contact
- Choose person as the Metaverse object type
- Select Join as the link type
- Enter a precedence value
- Click Next
- In the scoping filter area, select
- Add Clause. Under attribute, choose LegacyExchangeDN and under Operator, select ISNOTNULL and true in the value Textbox
- Click Next
- Click Next on the Join rules page without entering anything
- Add a transformation:
- type: expression
- target attribute: onPremisesCustomAddresses
- source:
- IIF(IsNullOrEmpty([legacyExchangeDN]),NULL,”x500:” & [legacyExchangeDN])
- merge type: MergeCaseInsensitive
- Click Add
- Repeat these rules for each forest which requires matching
Step 3: Outbound Custom Rules
Now that the inbound rules have been created you may want to run a full sync and review the results in the metaverse search. Once you are happy with the inbound flows, it’s time to export your results to your forests.
- Create your first of two rules for the forest corp.contoso.com and remember once more you will need to repeat these rules for each forest
- Out to AD – Contact Custom Addresses
- From the Sync Rules Editor, select Outbound under Direction
- From the Sync Rules Editor, select the forest corp.contoso.com under Connector
- Click Add New Rule
- Enter a Name, we chose: ‘In from AD – Contact Custom Addresses’
- Click Description
- Under connected system, select the forest you would like to target ‘corp.contoso.com‘
- Connected System object type Contact
- Choose person as the Metaverse object type
- Select Join as the link type
- Enter a precedence value
- Click Next
- In the scoping filter area, select Add Clause
- Under attribute, choose onPremisesCustomAddresses and under Operator, select ISNOTNULL and true in the value Textbox
- Click Next
- Click Next on the Join rules page without entering anything
- Add a transformation:
- type: expression
- target attribute: proxyAddresses
- source:
- ‘IIF(IsNullOrEmpty([onPremisesCustomAddresses]),NULL,[onPremisesCustomAddresses])’
- merge type: MergeCaseInsensitive
- Click Add
- Out to AD – User Custom Addresses
- From the Sync Rules Editor, select Outbound under Direction.
- From the Sync Rules Editor, select the forest corp.contoso.com under Connector
- Click Add New Rule
- Enter a Name, we chose: ‘In from AD – User Custom Addresses’
- Click Description
- Under connected system, select the forest you would like to target ‘corp.contoso.com‘
- Connected System object type User
- Choose person as the Metaverse object type
- Select Join as the link type
- Enter a precedence value
- Click Next
- In the scoping filter area, select Add Clause. Under attribute, choose onPremisesCustomAddresses and under Operator, select ISNOTNULL and true in the value Textbox
- Click Next
- Click Next on the Join rules page without entering anything
- Add a transformation:
- type: expression
- target attribute: proxyAddresses
- source:
- ‘IIF(IsNullOrEmpty([onPremisesCustomAddresses]),NULL,[onPremisesCustomAddresses])’
- merge type: MergeCaseInsensitive
- Click Add
- Run full sync and review the results. If the results are as expected it’s time to enable the sync engine once more by running the below CMDlet on the AD Connect server
ADSyncScheduler -SyncCycleEnabled $True
So that’s about it from me until next time, take care and stay safe implementing your custom AD Connect Rules.
PS. For the adventurous types please see the following PowerShell scripts. You might be interested in the random precedence order that was chosen.
In from AD – Contact Custom Addresses
Change corp.contoso.com [string]$Connector = (Get-ADSyncConnector | ? { ($_.ConnectorTypeName -eq "AD")-and ($_.Name -eq 'corp.contoso.com')}).Identifier.ToString() New-ADSyncRule-Name 'In from AD - Contact Custom Addresses'
-Identifier $Identifier-
Description 'This Rule has been created to collect all Custom Addresses in each forest. This will select all values that are not smtp, SIP or x500 addresses from EOP. Please be aware that the any other custom addresses such as CcMAIL: or Ms: will also be imported by default. If you do not want these addresses please remove these addresses from the proxy addresses before applying the rule.'
-Direction 'Inbound'-Precedence 90
-PrecedenceAfter '00000000-0000-0000-0000-000000000000'
-PrecedenceBefore '00000000-0000-0000-0000-000000000000'
-SourceObjectType 'contact'-TargetObjectType 'person'
-Connector $Connector-LinkType 'Join'
-SoftDeleteExpiryInterval 0-ImmutableTag ''
-OutVariable syncRule Add-ADSyncAttributeFlowMapping
-SynchronizationRule $syncRule[0]
-Source @('proxyAddresses')-Destination 'onPremisesCustomAddresses'
-FlowType 'Expression'
-ValueMergeType 'MergeCaseInsensitive'
-Expression 'IIF(InStr([proxyAddresses],"smtp:",1,vbTextCompare)>0 || InStr([proxyAddresses],"SMTP:",1,vbTextCompare)>0 || InStr([proxyAddresses],"sip:",1,vbTextCompare)>0 || InStr([proxyAddresses],"SIP:",1,vbTextCompare)>0 || InStr([proxyAddresses],"x400:",1,vbTextCompare)>0 || InStr([proxyAddresses],"X400:",1,vbTextCompare)>0 || InStr([proxyAddresses],"ms:",1,vbTextCompare)>0 || InStr([proxyAddresses],"MS:",1,vbTextCompare)>0 || InStr([proxyAddresses],"ccmail:",1,vbTextCompare)>0 || InStr([proxyAddresses],"CCMAIL:",1,vbTextCompare)>0 || InStr([proxyAddresses],"eum:",1,vbTextCompare)>0 || InStr([proxyAddresses],"EUM:",1,vbTextCompare)>0 || InStr([proxyAddresses],"xmpp:",1,vbTextCompare)>0 || InStr([proxyAddresses],"XMPP:",1,vbTextCompare)>0 || InStr([proxyAddresses],"gwise:",1,vbTextCompare)>0 || InStr([proxyAddresses],"GWISE:",1,vbTextCompare)>0 || InStr([proxyAddresses],"fax:",1,vbTextCompare)>0 || InStr([proxyAddresses],"FAX:",1,vbTextCompare)>0 || InStr([proxyAddresses],"facsys:",1,vbTextCompare)>0 || InStr([proxyAddresses],"FACSYS:",1,vbTextCompare)>0 || InStr([proxyAddresses],"x500:/o=ExchangeLabs",1,vbTextCompare)>0 ,NULL,[proxyAddresses])
' ` -OutVariable syncRule New-Object `-TypeName 'Microsoft.IdentityManagement.PowerShell.ObjectModel.ScopeCondition'
-ArgumentList 'mail','True','ISNOTNULL' ` -OutVariable condition0 Add-ADSyncScopeConditionGroup
-SynchronizationRule $syncRule[0]
-ScopeConditions @($condition0[0]) ` -OutVariable syncRule Add-ADSyncRule ` -SynchronizationRule $syncRule[0] Get-ADSyncRule ` -Identifier $Identifier
In from AD – LegacyExchangeDN
[string]$Identifier = [Guid]::NewGuid().ToString() Change corp.contoso.com [string]$Connector = (Get-ADSyncConnector | ? { ($_.ConnectorTypeName -eq "AD")-and ($_.Name -eq 'corp.contoso.com')}).Identifier.ToString() New-ADSyncRule
-Name 'In from AD - LegacyExchangeDN'
-Identifier $Identifier-Description ''
-Direction 'Inbound'-Precedence 99
-PrecedenceAfter '00000000-0000-0000-0000-000000000000'-PrecedenceBefore '00000000-0000-0000-0000-000000000000'
-SourceObjectType 'contact'-TargetObjectType 'person'
-Connector $Connector-LinkType 'Join'
-SoftDeleteExpiryInterval 0-ImmutableTag ''
-OutVariable syncRule Add-ADSyncAttributeFlowMapping-SynchronizationRule $syncRule[0]
-Source @('legacyExchangeDN')-Destination 'onPremisesCustomAddresses'
-FlowType 'Expression'-ValueMergeType 'MergeCaseInsensitive'
-Expression 'IIF(IsNullOrEmpty([legacyExchangeDN]),NULL,"x500:" & [legacyExchangeDN])' ` -OutVariable syncRule New-Object
-TypeName 'Microsoft.IdentityManagement.PowerShell.ObjectModel.ScopeCondition'
-ArgumentList legacyExchangeDN,'True','ISNOTNULL' ` -OutVariable condition0 Add-ADSyncScopeConditionGroup
-SynchronizationRule $syncRule[0]
-ScopeConditions @($condition0[0]) ` -OutVariable syncRule Add-ADSyncRule ` -SynchronizationRule $syncRule[0] Get-ADSyncRule ` -Identifier $Identifier
In from AD – User Custom Addresses
[string]$Identifier = [Guid]::NewGuid().ToString() Change corp.contoso.com [string]$Connector = (Get-ADSyncConnector | ? { ($_.ConnectorTypeName -eq "AD")-and ($_.Name -eq 'corp.contoso.com')}).Identifier.ToString() New-ADSyncRule-Name 'In from AD - User Custom Addresses'
-Identifier $Identifier-Description 'This Rule has been created to collect all Custom Adddresses in each forest. This will select all values that are not smtp, SIP or x500 addresses from EOP. Please be aware that the any other custom addresses such as CCMAIL: or MS: will also be imported by default. If you do not want these addresses please remove these addresses from the proxy addresses before applying the rule.
-Direction 'Inbound'-Precedence 89
-PrecedenceAfter '00000000-0000-0000-0000-000000000000'
-PrecedenceBefore '00000000-0000-0000-0000-000000000000'
-SourceObjectType 'user'-TargetObjectType 'person'
-Connector $Connector-LinkType 'Join'
-SoftDeleteExpiryInterval 0-ImmutableTag ''
-OutVariable syncRule Add-ADSyncAttributeFlowMapping-SynchronizationRule $syncRule[0]
-Source @('proxyAddresses')-Destination 'onPremisesCustomAddresses'
-FlowType 'Expression'
-ValueMergeType 'MergeCaseInsensitive'
-Expression 'IIF(InStr([proxyAddresses],"smtp:",1,vbTextCompare)>0 || InStr([proxyAddresses],"SMTP:",1,vbTextCompare)>0 || InStr([proxyAddresses],"sip:",1,vbTextCompare)>0 || InStr([proxyAddresses],"SIP:",1,vbTextCompare)>0 || InStr([proxyAddresses],"x400:",1,vbTextCompare)>0 || InStr([proxyAddresses],"X400:",1,vbTextCompare)>0 || InStr([proxyAddresses],"ms:",1,vbTextCompare)>0 || InStr([proxyAddresses],"MS:",1,vbTextCompare)>0 || InStr([proxyAddresses],"ccmail:",1,vbTextCompare)>0 || InStr([proxyAddresses],"CCMAIL:",1,vbTextCompare)>0 || InStr([proxyAddresses],"eum:",1,vbTextCompare)>0 || InStr([proxyAddresses],"EUM:",1,vbTextCompare)>0 || InStr([proxyAddresses],"xmpp:",1,vbTextCompare)>0 || InStr([proxyAddresses],"XMPP:",1,vbTextCompare)>0 || InStr([proxyAddresses],"gwise:",1,vbTextCompare)>0 || InStr([proxyAddresses],"GWISE:",1,vbTextCompare)>0 || InStr([proxyAddresses],"fax:",1,vbTextCompare)>0 || InStr([proxyAddresses],"FAX:",1,vbTextCompare)>0 || InStr([proxyAddresses],"facsys:",1,vbTextCompare)>0 || InStr([proxyAddresses],"FACSYS:",1,vbTextCompare)>0 || InStr([proxyAddresses],"x500:/o=ExchangeLabs",1,vbTextCompare)>0 ,NULL,[proxyAddresses])
' ` -OutVariable syncRule New-Object
-TypeName 'Microsoft.IdentityManagement.PowerShell.ObjectModel.ScopeCondition'
-ArgumentList 'mail','True','ISNOTNULL' ` -OutVariable condition0 Add-ADSyncScopeConditionGroup
-SynchronizationRule $syncRule[0]
-ScopeConditions @($condition0[0]) ` -OutVariable syncRule Add-ADSyncRule ` -SynchronizationRule $syncRule[0] Get-ADSyncRule ` -Identifier $Identifier
Out to AD – Contact Custom Addresses
[string]$Connector = (Get-ADSyncConnector | ? { ($_.ConnectorTypeName -eq "AD")-and ($_.Name -eq 'corp.contoso.com')}).Identifier.ToString() New-ADSyncRule
-Name 'Out to AD - Contact Custom Addresses'
-Identifier $Identifier
-Description ' This Rule has been created to write back all Custom Addresses found in each forest.'
-Direction 'Outbound'-Precedence 62
-PrecedenceAfter '00000000-0000-0000-0000-000000000000'
-PrecedenceBefore '00000000-0000-0000-0000-000000000000'
-SourceObjectType 'person'
-TargetObjectType 'contact'
-Connector $Connector
-LinkType 'Join'
-SoftDeleteExpiryInterval 0
-ImmutableTag ''
-OutVariable syncRule Add-ADSyncAttributeFlowMapping
-SynchronizationRule $syncRule[0]
-Source @('onPremisesCustomAddresses')
-Destination 'proxyAddresses'
-FlowType 'Expression'-ValueMergeType 'MergeCaseInsensitive'
-Expression 'IIF(IsNullOrEmpty([onPremisesCustomAddresses]),NULL,[onPremisesCustomAddresses])' ` -OutVariable syncRule New-Object
-TypeName 'Microsoft.IdentityManagement.PowerShell.ObjectModel.ScopeCondition'
-ArgumentList 'onPremisesCustomAddresses','True','ISNOTNULL' ` -OutVariable condition0 Add-ADSyncScopeConditionGroup
-SynchronizationRule $syncRule[0]
-ScopeConditions @($condition0[0]) ` -OutVariable syncRule Add-ADSyncRule ` -SynchronizationRule $syncRule[0] Get-ADSyncRule ` -Identifier $Identifier
Out to AD – User Custom Addresses
[string]$Connector = (Get-ADSyncConnector | ? { ($_.ConnectorTypeName -eq "AD")-and ($_.Name -eq 'corp.contoso.com')}).Identifier.ToString() New-ADSyncRule-Name 'Out to AD - User Custom Addresses'
-Identifier $Identifier
-Description ' This Rule has been created to write back all Custom Addresses found in each forest.'
-Direction 'Outbound'
-Precedence 57
-PrecedenceAfter '00000000-0000-0000-0000-000000000000'
-PrecedenceBefore '00000000-0000-0000-0000-000000000000'
-SourceObjectType 'person'
-TargetObjectType 'user'
-Connector $Connector-LinkType 'Join'
-SoftDeleteExpiryInterval 0
-ImmutableTag ''
-OutVariable syncRule Add-ADSyncAttributeFlowMapping-SynchronizationRule $syncRule[0]
-Source @('onPremisesCustomAddresses')-Destination 'proxyAddresses'
-FlowType 'Expression'-ValueMergeType 'MergeCaseInsensitive'
-Expression 'IIF(IsNullOrEmpty([onPremisesCustomAddresses]),NULL,[onPremisesCustomAddresses])' ` -OutVariable syncRule New-Object
-TypeName 'Microsoft.IdentityManagement.PowerShell.ObjectModel.ScopeCondition'
-ArgumentList 'onPremisesCustomAddresses','True','ISNOTNULL' ` -OutVariable condition0 Add-ADSyncScopeConditionGroup
-SynchronizationRule $syncRule[0]
-ScopeConditions @($condition0[0]) ` -OutVariable syncRule Add-ADSyncRule ` -SynchronizationRule $syncRule[0] Get-ADSyncRule ` -Identifier $Identifier