Achieving Cross-Forest Coexistence for OCS or Lync During Active Directory Domain Migrations

Active Directory environments often host applications which QMM will not natively migrate. This creates the need to introduce a custom migration path to ensure that there is no loss of application access when migrating workstations and users to the target environment. One of the most common scenarios we encounter is the need to configure cross-forest coexistence for Microsoft Unified Communications products. This post will describe a simple approach to achieving coexistence for OCS or Lync between Active Directory domains and forests.

 

The key to a successful migration is to keep the migration path as simple as possible. This means reducing the number of changes at any one time. The more independent steps we try to introduce on migration night, the more complexity we create. By perfecting multiple migration steps and handling them independently, we ensure that there is minimal impact to the end user. Customers will often attempt to bundle multiple changes into one event. One of the most common “bundled” migration paths is to migrate OCS to Lync as part of the Active Directory user and workstation migration. Migrating OCS to Lync requires a minimum OCS client version which often means an upgrade, if not a new installation of the Lync client. Additionally, users need to be de-provisioned and re-provisioned between OCS and Lync environments and their contacts need to be backed-up and restored to the target. All of this adds up to more complexity, and more help desk calls.

The best way to say “No” to a customer is to provide them with a better solution. The better solution is to separate the OCS or Lync migration from the Active Directory user and workstation migrations. To do this we simply treat the source environment as a resource forest. This approach will allow the Active Directory account and Exchange mailbox to be switched to the target environment while preserving access to OCS and Lync for target users. By introducing a simple step into our migration path, we can set the msRTCSIP-OriginatorSid in the source OCS or Lync environment to the target objectSid. The below code is a PowerShell form which uses a migration session import file to link the accounts between source and target environments.

This code might seem intimidating, but the logic is as follows –

  1. Query the source domain using the sAMAccountNamefrom the QMM migration import file
  2. Bind to the source user object to retrieve the objectGuidin Native Guid format
  3. Query the target domain for the matching attribute equal to the source Guid
  4. Bind to the target user object to retrieve the objectSidfrom the target AD account
  5. Set the source msRTCSIP-OriginatorSidequal to the target objectSid
  6. Re-bind to the source object to validate that the msRTCSIP-OriginatorSidhas been set correctly

 

 

# PowerShell script to link OCS or Lync between domains or forests

# Comes without support from Matthew McKee or Coherence Inc.

 

[void][System.Reflection.Assembly]::LoadWithPartialName(“System.Drawing”)

[void][System.Reflection.Assembly]::LoadWithPartialName(“System.Windows.Forms”)

 

$sourceDomain = “<SourceDomain>” # FQDN of Source Domain Controller

$targetDomain = “<TargetDomain>” # FQDN of Target Domain Controller

$matchingAttribute = “<QMM_MatchingAttribute>” # For Example extensionAttribute15

 

$fileName = “$env:TEMP\DirSync_log_” + (Get-Date -Format yyyy_MM_dd_ss) + “.txt”

Out-File -InputObject ([string](Get-Date)) -FilePath $fileName -Append

Out-File -InputObject (“sAMAccountName” + “;” + “OCS State”) -FilePath $fileName -Append

 

$objForm = New-Object System.Windows.Forms.Form

$objForm.Text = “Migrate OCS Users”

$objForm.Size = New-Object System.Drawing.Size(403,210)

$objForm.StartPosition = “CenterScreen”

$objForm.FormBorderStyle = “FixedSingle”

$objForm.BackColor = “White”

$objForm.ForeColor = “Black”

$ObjForm.ShowIcon = $True

 

$oKButton = New-Object System.Windows.Forms.Button

$oKButton.Location = New-Object System.Drawing.Size(237,155)

$oKButton.Size = New-Object System.Drawing.Size(75,23)

$oKButton.Text = “OK”

$oKButton.Add_Click({

 

foreach($oCSUser in $userMigrationList)

 

{

 

$sourceDomainSearch = New-Object System.DirectoryServices.DirectorySearcher(“(&(sAMAccountName=$oCSUser)(msRTCSIP-UserEnabled=True))”)

$sourceDomainSearch.SearchRoot = New-Object System.DirectoryServices.DirectoryEntry(“LDAP://$sourceDomain”)

$sourceDomainSearch.PageSize = 1000

$sourceDomainSearch.SearchScope = “Subtree”

“distinguishedName” | %{$sourceDomainSearch.PropertiesToLoad.Add($_)}

 

$sourceDomainSearchResults = $sourceDomainSearch.FindAll()

 

foreach($sourceDomainSearchResult in $sourceDomainSearchResults)

 

{

 

[string]$sourceDN = $sourceDomainSearchResult.properties.distinguishedname

 

$userBindSource1 = New-Object system.directoryservices.directoryEntry(“LDAP://$sourceDomain/$sourceDN”)

 

[string]$sourceGuid = $userBindSource1.NativeGuid.ToUpper()

 

$targetDomainSearch = New-Object System.DirectoryServices.DirectorySearcher(“(&($matchingAttribute=$sourceGuid))”)

$targetDomainSearch.SearchRoot = New-Object System.DirectoryServices.DirectoryEntry(“LDAP://$targetDomain”)

$targetDomainSearch.PageSize = 1000

$targetDomainSearch.SearchScope = “Subtree”

“distinguishedName” | %{$targetDomainSearch.PropertiesToLoad.Add($_)}

 

$targetDomainSearchResults = $targetDomainSearch.FindAll()

 

foreach($targetDomainSearchResult in $targetDomainSearchResults)

 

{

 

[string]$targetDN = $targetDomainSearchResult.properties.distinguishedname

 

$userBindTarget1 = New-Object system.directoryservices.directoryEntry(“LDAP://$targetDomain/$targetDN”)

 

$targetObjectSid = $userBindTarget1.Properties.objectSid

 

$userBindSource1.putex(3,”msRTCSIP-OriginatorSid”,[array]$targetObjectSid)

$userBindSource1.setInfo()

 

$userBindSource2 = New-Object system.directoryservices.directoryEntry(“LDAP://$sourceDomain/$sourceDN”)

 

if([string]$userBindSource2.Properties.”msRTCSIP-OriginatorSid” -eq [string]$targetObjectSid){$result = “Success”}

else{$result = “Failed”}

 

}

 

}

 

$oCSUser + “;” + $result | Out-File $fileName -Append

 

}

 

$objForm.Close()})

 

$cancelButton = New-Object System.Windows.Forms.Button

$cancelButton.Location = New-Object System.Drawing.Size(312,155)

$cancelButton.Size = New-Object System.Drawing.Size(75,23)

$cancelButton.Text = “Cancel”

$cancelButton.Add_Click({$objForm.Close()})

$objForm.Controls.Add($cancelButton)

 

$objLabel1 = New-Object System.Windows.Forms.Label

$objLabel1.Location = New-Object System.Drawing.Size(10,10)

$objLabel1.Size = New-Object System.Drawing.Size(300,20)

$objLabel1.Text = “Migration Session Import File:”

$objForm.Controls.Add($objLabel1)

 

$objTextBox1 = New-Object System.Windows.Forms.TextBox

$objTextBox1.Location = New-Object System.Drawing.Size(10,30)

$objTextBox1.Size = New-Object System.Drawing.Size(300,20)

$objTextBox1.enabled = $false

$objTextBox1.text = “”

$objForm.Controls.Add($objTextBox1)

 

$browse = New-Object windows.Forms.OpenFileDialog

$browse.InitialDirectory = “C:\”

$browseButton = New-Object system.Windows.Forms.Button

$browseButton.Text = “Browse”

$browseButton.Location = New-Object System.Drawing.Size(312,29)

$browseButton.Add_Click({

$browse.ShowDialog()

$objTextBox1.Text = $browse.FileName

$userMigrationList = Get-Content $objTextBox1.Text

 

if($userMigrationList[0] -like “sAMAccountName”){$userMigrationList = $userMigrationList | Select-Object -Skip 1}

 

})

 

$objLabel2 = New-Object System.Windows.Forms.Label

$objLabel2.Location = New-Object System.Drawing.Size(10,80)

$objLabel2.Size = New-Object System.Drawing.Size(300,60)

$objLabel2.Text = “Use QMM migration session import file to set the msRTCSIP-OriginatorSid on the source object. Supports only sAMAccountName import files.”

$objForm.Controls.Add($objLabel2)

 

$objForm.Controls.Add($browseButton)

$objForm.Controls.Add($oKButton)

$objForm.Controls.Add($validateButton)

 

$objForm.Topmost = $False

$objForm.Add_Shown({$objForm.Activate()})

[void]$objForm.ShowDialog()

 

Customers will often fumble around with an OCS and Lync migration. Whether they are relying on cached UC client credentials at the desktop level, or attempting to copy the OCS and Lync attributes to the target domain in an intra-forest migration – both approaches are risky. In fact, migrating the OCS and Lync attributes intra-forest will damage the user database. UC users are mapped based on their objectGuid, and when you copy the SIP address, you have now introduced a duplicate SIP with a different Guid. This will damage the address book and contact lists. Whether intra or inter forest, users should always be de-provisioned from the source and re-provisioned to the target. In a later post, I might describe the actual OCS or Lync migration path. Let me know if there’s any interest!