Synchronize contacts between forests

In the course of any organization, it appear and disappear temporary allies, she makes friends and breaks friendship with partner, it reorganized, absorb other organisations and Vice versa, is divided into several. From the point of view, it is often necessary to create shared resources, trust relationships between Active Directory forests, establishing vpn tunnels site-to-site and other "unification" procedures. Below I will consider one such procedure, copy of the contact information about the users of another trusted domain.

Happens, users swear that they have "not getting emails". Why? Yes, because sent to one address, and another because the girl got married, changed her name, and the good admins have not saved the previous email address. Sometimes employees right in saying into the phone: "es!", instead of "es as the dollar!", and the person on the other side is wrong, typing the letter "C". Users can write .com instead .EN and even .EN instead .com and when users are many, such mistakes turn into an endless stream of complaints. As any foolishness, you need to counter the ability, there is a need to have up-to-date address book of all partners of the organization.

About the structure

In our organization as the mail system is Exchange server as client software for transferring Outlook e-mail, so in order for our employees to see in your address book the contact information of the partners, is sufficient to create corresponding contact objects in Active Directory. Our partners use what they like, some sitting on Lotus Domino, the other twist sendmail, and others are working with Kerio Mail Server, but they all have one thing in common: each Active Directory user in the attribute mail listed the actual email address. How they do it, don't ask how to do something and everything. And if so, it can be used, turning "users" in the "our contacts":



Through a trust relationship, we can read their users as their own (of course, if the trust is not included digest authentication, in this case a little harder), and the right to write in its domain we have.
Generally for the procedure of export-import can be used, for example, Forefront Identity Manager but, first, we the people are frugal and not throw money to the wind, and secondly, we are not inclined to peel on the mosquitoes of hotwater, so do other resource — PowerShell'ω, and for compatibility. ADPowerShell, .NET classes System.DirectoryServices.DirectoryEntry and System.DirectoryServices.DirectorySearcher we will be more than enough.

Methods

After searching all required users in Active Directory, we copy their properties in memory, filling the array. PowerShell allows you to add existing objects properties on the fly, using the Add-Member than what we use. And the result is a well-structured list in which, moreover, subsequently it will be easy to search using another cmdlet Where-Object. Also we copy in memory of all the contacts that lie in the container external contacts, and compare the two lists. The result of the comparison will be three arrays: new contacts, contacts that you want to delete, and contacts that have only changed some properties. It is clear that after the first run, all found contacts will be new.
After we imported the contacts first, we need to care for their relevance to, and not a way to "delete all, pour in the new", and change only those fields that have changed. Why? Yes, because, first, the Exchange of a delicate thing and the formation of the address book takes time, and secondly, in case of failure of the RID master when trying to batch create any objects in Active Directory can happen horrible things. We know that every Active Directory object uses the attribute objectGUID in order to store in it the unique identifier of the object, it is set when you create a system and never change, we are going to remember to convert to hex string and stored in the field info contact. However, if info someone used for other purposes, it can store the ID in any other attribute, they are trust enough.
And, of course, we are mindful of security. It is better to delegate the creation, modification, deletion of objects of class "contact" is only a specific account and only in a specific container, and in this case, the potential attacker gaining control over the machine the script will not be able to do anything particularly serious. And now the script with comments:

the
 
# Retrieve contacts of the trusted domain 
# Yegor Ivanov 

# A function writes the log to a file 

function Write-LogFile([string]$logFileName) 
{ 
Process 
{ 
$_ 
$dt = Get-Date 
$str = $dt.DateTime + "" + $_ 
$str | Out-File -FilePath $logFileName -Append 
} 
} 

# A helper function that compares two objects 
# type ArrayList, we need to verify the identity of a multiline 
# parameters as string and byte arrays 

function Compare-ArrayLists([System.Collections.ArrayList] $ListA [System.Collections.ArrayList] $ListB) 
{ 
if ($ListA.Count-ne $ListB.Count) 
{ 
return $false 
} 
else 
{ 
$CompListA = New-Object System.Collections.ArrayList($null) 
$CompListB = New-Object System.Collections.ArrayList($null) 
for ($i=0;$i -lt $ListA.Count;$i++) 
{ 
if ($ListA[$i].GetType() -ne [String]) 
{ 
$rc = $CompListA.Add([System.BitConverter]::ToString($ListA[$i])) 
} 
else 
{ 
$rc = $CompListA.Add($ListA[$i]) 
} 
if ($ListB[$i].GetType() -ne [String]) 
{ 
$rc = $CompListB.Add([System.BitConverter]::ToString($ListB[$i])) 
} 
else 
{ 
$rc = $CompListB.Add($ListB[$i]) 
} 

} 
for ($i=0;$i -lt $CompListA.Count;$i++) 
{ 
if ($CompListB.IndexOf($CompListA[$i]) -lt 0) {return $false} 
} 
return $true 
} 
} 

# This function loads the contacts from the domain, or someone else's (users) or their (contacts). Parameters: 
# Domain name 
# Organizational unit name 
# A flag indicating that the domain of the Exchange organization 
# Ukazatel on an array of records 
# Flag indicating that shipping: of users or contacts 

function Load-FromDomain([string] $DomainName, [string] $UnitName, [bool]$flagExchangeDomain, [ref]$A_Entries, [bool]$flagContacts) 
{ 
if (!$flagContacts) 
{ 
if (!$flagExchangeDomain) 
{ 
# Filter for LDAP, I have loaded all the contacts who have filled 
# fields mail or company. In the tail !userAccountControl:1.2.840.113556.1.4.803:=2, 
# if this is recommendit will also be checked for blockages 
# user, but this is not always justified (for example, account for the equipment mailbox or room AD disabled) 
$strFilter = "(&(objectClass=user)(!objectClass=computer)(mail=*)(company=*))"#(!userAccountControl:1.2.840.113556.1.4.803:=2) 
} 
else 
{ 
# Exchange-organization is the msExchHideFromAddressLists attribute for hiding the mailbox from the address book 
$strFilter = "(&(objectClass=user)(!objectClass=computer)(mail=*)(company=*)(!msExchHideFromAddressLists=TRUE))" #(!userAccountControl:1.2.840.113556.1.4.803:=2) 
} 
} 
else 
{ 
$strFilter = "(&(objectClass=contact))" 
} 
$objDomain = New-Object System.DirectoryServices.DirectoryEntry("LDAP://"+$DomainName+"/"+$UnitName) 
$objSearcher = New-Object System.DirectoryServices.DirectorySearcher 
$objSearcher.SearchRoot = $objDomain 
$objSearcher.PageSize = 1000 
$objSearcher.Filter = $strFilter 
$objSearcher.SearchScope = "Subtree" 

# This the important line here perechisleny attributes that should be copied, 
# here you can add anything, or to get out of here. 

$colProplist = "employeeID", "employeeType", "objectGUID", "CN", "givenName", "name", 
"sn", "legacyExchangeDN", "displayName", "mail", 
"wWWHomePage", "l", "postalCode", "initials", "physicalDeliveryOfficeName", 
"st", "streetAddress", "ipPhone", "title", "mobile", "department", 
"pager", "homePhone", "facsimileTelephoneNumber", "userAccountControl", 
"distinguishedName", "company", 
"Description", "otherTelephone", "telephoneNumber", "userCertificate" 
if ($flagExchangeDomain) 
{ 
$colProplist += "mailNickname" 
$colProplist += "msExchHideFromAddressLists" 
} 
if ($flagContacts) 
{ 
$colProplist += "info" 
} 
foreach ($i in $colPropList) 
{ 
$rc = $objSearcher.PropertiesToLoad.Add($i) 
} 
$colResults = $objSearcher.FindAll() 
$colResults.Count 
foreach ($objResult in $colResults) 
{ 
$objItem = $objResult.Properties 

# Depending on that, we  pull  contacts or users, we  choose  what attribute 
# extract the unique identifier to it later to compare notes. 

$Entry = New-Object -TypeName System.Object 
if (!$flagContacts) 
{ 
$Entry | Add-Member -type NoteProperty -name "GUID" -Value ([System.BitConverter]::ToString($objItem.objectguid[0])).Replace('-',") 
} 
else 
{ 
$Entry | Add-Member -type NoteProperty -name "GUID" -Value ([string]$objItem.info) 
} 

# Compile a list of all attributes, but exclude certificates and other phone numbers, because these attributes are multiline. 

# but since I'm using only two, I decided for them just to do special handling 

$UserProperties = $colProplist | Where-Object {($_ -ne "objectGUID") -and ($_ -ne "userCertificate") -and ($_ -ne "otherTelephone")} 

foreach ($UserProperty in $UserProperties) 
{ 
if ($objItem.Item($UserProperty) -ne $null) 
{ 
$Entry | Add-Member -type NoteProperty -name $UserProperty -Value ([string]$objItem.Item($UserProperty)) 
} 
} 

# Processing for certificates and below a treatment for other phone numbers 

if ($objItem.usercertificate -ne $null) 
{ 
$Certificates = New-Object System.Collections.ArrayList($null) 
foreach ($Certificate in $objItem.usercertificate) 
{ 
$rc = $Certificates.Add($Certificate) 
} 
$Entry | Add-Member -type NoteProperty -name "userCertificate" -Value ($Certificates) 
} 

if ($objItem.othertelephone -ne $null) 
{ 
$Telephones = New-Object System.Collections.ArrayList($null) 
foreach ($Telephone in $objItem.othertelephone) 
{ 
$rc = $Telephones.Add($Telephone) 
} 
$Entry | Add-Member -type NoteProperty -name "otherTelephone" -Value ($Telephones) 
} 
$A_Entries.Value += $Entry 

} 

} 

# The function is over, the script starts here. 
# First create while zero arrays to store our users and contacts, in order: 
# An array of all palihawadana 
# An array of all contacts 
# Contacts adding 
# Contacts to change 
# Contacts delete 

$A_Users = $A_Contacts = $A_NewContacts = $A_ChangedContacts = $A_ContactsToDelete = @() 

$LogFileName = "./GetUserContacts.log" 
$flagExchangeOrganization = $false 

# Here we will examine the command line and extract the data 
# Command line parameters should be: 
# ("<username>","<password>") ("<your domain>","<unit contacts>","<organization of exchange>") ("<foreign domain1>","<someone else's unit>","<organization of Exchange>") ("<foreign domain2>"... 
# for example 
# ("admin@litware.inc","password") ("litware.inc","ou=contacts,dc=litware,dc=inc",$true) ("contoso.com","dc=contoso,dc=com",$false) 

if ($args.Count-lt 3) 
{ 
break 
} 
$UserName = $args[0][0] 
$Password = $args[0][1] 

$UserName | Write-LogFile $LogFileName 
$Password | Write-LogFile $LogFileName 

$Domain = $args[1][0] 
$ContactsOU = $args[1][1] 
$flagExchangeOrganization = $args[1][2] 

$Domain | Write-LogFile $LogFileName 
$ContactsOU | Write-LogFile $LogFileName 
$flagExchangeOrganization | Write-LogFile $LogFileName 

Load-FromDomain $Domain $ContactsOU $flagExchangeOrganization ([ref]$A_Contacts) $true 

for ($i=2;$i -lt $args.Count;$i++) 
{ 
$SrcDomain = $args[$i][0] 
$SrcOU = $args[$i][1] 
$SrcExchangeFlag = $args[$i][2] 

$SrcDomain | Write-LogFile $LogFileName 
$SrcOU | Write-LogFile $LogFileName 
$SrcExchangeFlag | Write-LogFile $LogFileName 

Load-FromDomain $SrcDomain $SrcOU $SrcExchangeFlag ([ref]$A_Users) $false 
} 

# Loop through all users, and compare with existing contacts 
# if the user is not found, add it to the list of contacts 
# you want to add. If the user is, but he 
# change any attribute, then a recorded user and the attribute 
# or attributes in the array changes. 

foreach ($User in $A_Users) 
{ 
$Contact = $A_Contacts | Where-Object {$_.GUID -eq $User.GUID} 
if ($Contact-eq $null) 
{ 
$A_NewContacts += $User 
$A_NewContacts[$A_NewContacts.Length-1].distinguishedName = "" 
} 
else 
{ 
$flagContactAdded = $false 
$UserProperties = ($User | Get-member -MemberType NoteProperty | Where-Object {($_.Name-ne "distinguishedName") -and ($_.Name-ne "mailNickname")}) 
foreach ($UserProperty in $UserProperties) 
{ 
if ($Contact.($UserProperty.Name) -ne $null) 
{ 
if ($User.($UserProperty.Name).GetType() -ne [System.Collections.ArrayList]) 
{ 
if ($User.($UserProperty.Name) -ne $Contact.($UserProperty.Name)) 
{ 
if (!$flagContactAdded) 
{ 
$NewEntry = New-Object -TypeName System.Object 
$NewEntry | Add-Member -type NoteProperty -name "distinguishedName" -Value ($Contact.distinguishedname) 
$flagContactAdded = $true 
} 
$NewEntry | Add-Member -type NoteProperty -name ($UserProperty.Name) -Value $User.($UserProperty.Name) 
} 
} 
else 
{ 
if (!(Compare-ArrayLists $User.($UserProperty.Name) $Contact.($UserProperty.Name))) 
{ 
if (!$flagContactAdded) 
{ 
$NewEntry = New-Object -TypeName System.Object 
$NewEntry | Add-Member -type NoteProperty -name "distinguishedName" -Value ($Contact.distinguishedname) 
$flagContactAdded = $true 
} 
$NewEntry | Add-Member -type NoteProperty -name ($UserProperty.Name) -Value $User.($UserProperty.Name) 

} 
} 
} 

} 
if ($flagContactAdded) 
{ 
$A_ChangedContacts += $NewEntry 
} 
} 
} 

# Now run through the contacts list contacts delete 

foreach ($Contact in $A_Contacts) 
{ 
$User = $A_Users | Where-Object {$_.GUID -eq $Contact.GUID} 

{ 
$A_ContactsToDelete += $Contact 
} 

} 

# $A_NewContacts 
# $A_ChangedContacts 
# $A_ContactsToDelete 

# Remove unneeded 

foreach ($Contact in $A_ContactsToDelete) 
{ 
$ContactsOUDN = "LDAP://" + $Domain + "/" + $ContactsOU 
$objContactsOU = new-object System.DirectoryServices.DirectoryEntry($ContactsOUDN, $Username, $Password, [System.DirectoryServices.AuthenticationTypes]::Secure) 
$objContactsOU.Delete("contact", $Contact.distinguishedName.Split(",")[0]) 
"Deleted" + $Contact.name | Write-LogFile $LogFileName 
} 

# Create the right 

foreach ($Contact in $A_NewContacts) 
{ 
$ContactsOUDN = "LDAP://" + $Domain + "/" + $ContactsOU 
$objContactsOU = new-object System.DirectoryServices.DirectoryEntry($ContactsOUDN, $Username, $Password, [System.DirectoryServices.AuthenticationTypes]::Secure) 
$NewContact = $objContactsOU.Children.Add("CN="+$Contact.CN,"contact") 
$NewContactProperties = ($Contact | Get-member -MemberType NoteProperty | Where-Object {($_.Name-ne "distinguishedName") ` 
-and ($_.Name-ne "GUID") -and ($_.Name-ne "CN") -and ($_.Name-ne "otherTelephone")` 
-and ($_.Name-ne "name") -and ($_.Name-ne "userAccountControl") -and ($_.Name-ne "userCertificate")}) 
if ($NewContactProperties -ne $null) 
{ 
foreach ($NewContactProperty in $NewContactProperties) 
{ 
$NewContact.Put($NewContactProperty.Name,$Contact.($NewContactProperty.Name)) 
} 
} 
if ($Contact.mail -ne $null) 
{ 
if ($flagExchangeOrganization) 
{ 
$NewContact.Put("targetAddress", "SMTP:" + $Contact.mail) 
$NewContact.Put("mailNickname", $Contact.mail.Split("@")[0]) 
$NewContact.Put("msExchPoliciesExcluded", "{26491CFC-9E50-4857-861B-0CB8DF22B5D7}") 
} 
$NewContact.Put("proxyAddresses", "SMTP:" + $Contact.mail) 

} 
if ($Contact.userCertificate -ne $null) 
{ 
$NewContact.PutEx(2, "userCertificate", [Array]$Contact.userCertificate) 
} 
if ($Contact.otherTelephone -ne $null) 
{ 
$NewContact.PutEx(2, "otherTelephone", [Array]$Contact.otherTelephone) 
} 

$NewContact.Put("info",$Contact.GUID) 
$NewContact.SetInfo() 
"Created" + $Contact.name | Write-LogFile $LogFileName 
} 

# Finally, change that has changed. 
# Here, in addition to special handling multiline phones and certificates, there is a reaction 
# to change the contact's name. The fact that just a property of the CN (canonical name) 
# we can't change, to change it you must call a special method Rename() 

foreach ($Contact in $A_ChangedContacts) 
{ 
$ContactDN = "LDAP://" + $Domain + "/" + $Contact.distinguishedName 
$ChangedContact = new-object System.DirectoryServices.DirectoryEntry($ContactDN, $Username, $Password, [System.DirectoryServices.AuthenticationTypes]::Secure) 
$ChangedContactProperties = ($Contact | Get-member -MemberType NoteProperty | Where-Object {($_.Name-ne "distinguishedName") ` 
-and ($_.Name-ne "GUID") -and ($_.Name-ne "CN") -and ($_.Name-ne "otherTelephone") ` 
-and ($_.Name-ne "name") -and ($_.Name-ne "userAccountControl") -and ($_.Name-ne "userCertificate")}) 
if ($ChangedContactProperties -ne $null) 
{ 
foreach ($ChangedContactProperty in $ChangedContactProperties) 
{ 
$ChangedContact.Put($ChangedContactProperty.Name,$Contact.($ChangedContactProperty.Name)) 
The "changed attribute" + $ChangedContactProperty.Name + "contact" + $Contact.distinguishedName | Write-LogFile $LogFileName 
} 
} 
if ($Contact.userCertificate -ne $null) 
{ 
$ChangedContact.PutEx(1, "userCertificate", 0) 
$ChangedContact.PutEx(2, "userCertificate", [Array]$Contact.userCertificate) 
"Changed the userCertificate attribute of the contact" + $Contact.distinguishedName | Write-LogFile $LogFileName 
} 
if ($Contact.otherTelephone -ne $null) 
{ 
$ChangedContact.PutEx(1, "otherTelephone", 0) 
$ChangedContact.PutEx(2, "otherTelephone", [Array]$Contact.otherTelephone) 
"Modified contact attribute otherTelephone" + $Contact.distinguishedName | Write-LogFile $LogFileName 
} 
if ($Contact.CN-ne $null) 
{ 
"Contact" + $ChangedContact.distinguishedName + "renamed" + $Contact.CN | Write-LogFile $LogFileName 
$ChangedContact.Rename("CN="+$Contact.CN) 
} 
$ChangedContact.SetInfo() 
} 


Next, we can only form the command line and schedule sheduler to run the script. Well, if our partners will need our contacts, we, of course, this script can to share with them.
Article based on information from habrahabr.ru

Комментарии

Популярные сообщения из этого блога

Integration of PostgreSQL with MS SQL Server for those who want faster and deeper

Custom database queries in MODx Revolution

2 years Kartavykh reviews — the story of an Amateur show Old-Hard