📜 ⬆️ ⬇️

Synchronize contacts between forests

During the activities of any organization, temporary allies arise and disappear from it, it is friendly and breaks friendship with partners, it reorganizes, absorbs other organizations and vice versa, is divided into several. From an IT point of view, it is often necessary to create shared resources, trust relationships between Active Directory forests, to establish vpn-network-to-network tunnels and other “unifying” procedures. Below I will consider one such procedure, copying contact information about users of another trusted domain.

It happens, users swear that they "do not receive letters." Why is that? Yes, because it was sent to one address, and another is relevant, because the girl got married, changed her last name, and the good admins did not keep the previous email address. Sometimes employees make mistakes by saying “es!” Instead of “es like a dollar!”, And their interlocutor on the other side makes a mistake by typing the Latin letter “C”. Users can write .com instead of .ru and even .ru instead of .com, and when there are a lot of users, such errors turn into an endless stream of complaints. Since all sorts of nonsense need to oppose the ability, there is a need to have the most current address book of all partners in their organization.

About the structure

In our organization, the Exchange server is used as the mail system, as the client software for sending Outlook emails, so in order for our employees to see the contact information of partners in their address book, it is enough to create the corresponding contact objects in Active Directory. Our partners use what they like, some sit on Lotus Domino, others twist sendmail, others work with Kerio Mail Server, but for all of them there is always one thing: each Active Directory user has a valid email address in the mail attribute. Do not ask how they do it, somehow they do it. And if so, this can be used by turning “their users” into “our contacts”:


')
Thanks to trusting relationships, we can read their users as our own (of course, if selective trust of authenticity is not included in the trust, in this case it is a bit more complicated), and we have the right to write to our domain.
In general, for the export-import procedure you can use, for example, Forefront Identity Manager , but, first of all, we are economical people and don’t throw money away, and secondly, we are not inclined to beat mosquitoes from a hot-weaver, so we’ll manage with another resource - With PowerShell, moreover, for compatibility without the ADPowerShell hinge, the .NET classes System.DirectoryServices.DirectoryEntry and System.DirectoryServices.DirectorySearcher will be more than enough for us.

Technique

After searching all the necessary users in Active Directory, we copy their properties into memory, filling the array. PowerShell allows you to add properties to existing objects literally on the fly, using the Add-Member cmdlet, which we will use. As a result, we get a well-structured list, in which, moreover, it will be easy to subsequently search using another cmdlet, Where-Object . We also copy into memory all the contacts that we have in the container of external contacts, and compare two lists. The result of the comparison will be three arrays: new contacts, contacts that need to be deleted, and contacts that have only changed some properties. It is clear that after the first launch all contacts found will be new.
After we imported contacts for the first time, we need to take care of their relevance, and not the way to “delete everything, fill in with a new one”, but to change only those fields that have changed. Why is that? Yes, because, firstly, Exchange itself is a thin piece and it takes time to form the address book, and secondly, if the RID master fails, if you try to create any objects in the Active Directory in batch, disgusting things can happen. We know that each Active Directory object uses the objectGUID attribute to store a unique object identifier in it, it is set when created by the system and never changes, we will remember it, convert it to a hexadecimal string and store it in the info field of a contact. However, if info is used by someone for other purposes, he can store the identifier in any other attribute, believe me , they are enough.
You should also not forget about the userCertificate attribute ; for the objects of the “contact” class, it is not visible in the Active Directory Users and Computers snap-in, but Outlook uses it and sends / receives encrypted / signed emails. This business, of course, will work only if one organization trusts the certificates of another organization, but, by the way, this question is beyond the scope of this topic. Another attribute to pay attention to is called otherTelephone . It is interesting because it is multivalued, and therefore we have a special relationship with it. In fact, there are a lot of such attributes, but only one is used in the script, so creating a procedure specifically for extracting / writing multi-line attributes is a kind of script modification in the future.
And, of course, we remember about security. It is better to delegate the creation, modification, deletion of objects of the “contact” class only to a specific account and only in a specific container, in which case a potential attacker, having gained control of the machine with the script, cannot do anything particularly serious. And now the script with comments:

#      #   #      function Write-LogFile([string]$logFileName) { Process { $_ $dt = Get-Date $str = $dt.DateTime + " " + $_ $str | Out-File -FilePath $logFileName -Append } } #  ,     #  ArrayList,      # ,  ,     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 } } #      ,   (),   (). : #   #   # , ,    Exchange #     # ,   :    function Load-FromDomain([string] $DomainName, [string] $UnitName, [bool]$flagExchangeDomain, [ref]$A_Entries, [bool]$flagContacts) { if (!$flagContacts) { if (!$flagExchangeDomain) { #   LDAP,     ,    #    .   !userAccountControl:1.2.840.113556.1.4.803:=2, #   ,        # ,      (,        AD ) $strFilter = "(&(objectClass=user)(!objectClass=computer)(mail=*)(company=*))"#(!userAccountControl:1.2.840.113556.1.4.803:=2) } else { #  Exchange-   msExchHideFromAddressLists,      $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" #   ,   ,   , #    -    . $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 #    ,     ,  ,    #   ,      . $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) } #    ,      , ..   . #  , ,  - ,      , #       ,         $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)) } } #   ,        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 } } #  ,    . #            ,  : #    #    #    #    #    $A_Users = $A_Contacts = $A_NewContacts = $A_ChangedContacts = $A_ContactsToDelete = @() $LogFileName = "./GetUserContacts.log" $flagExchangeOrganization = $false #         #      : # ("< >","<>") ("< >","<  >","< exchange>") ("< 1>","< >","< Exchange>") ("< 2>"... #  # ("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 } #   ,    , #    ,     , #   .   ,    #  - ,       #      . 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 } } } #    ,       foreach ($Contact in $A_Contacts) { $User = $A_Users | Where-Object {$_.GUID -eq $Contact.GUID} if ($User -eq $null) { $A_ContactsToDelete += $Contact } } # $A_NewContacts # $A_ChangedContacts # $A_ContactsToDelete #   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]) "" + $Contact.name | Write-LogFile $LogFileName } #   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() " " + $Contact.name | Write-LogFile $LogFileName } # , ,  ,  . #        ,    #    .   ,     CN (canonical name) #  ,        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)) "  " + $ChangedContactProperty.Name + "  " + $Contact.distinguishedName | Write-LogFile $LogFileName } } if ($Contact.userCertificate -ne $null) { $ChangedContact.PutEx(1, "userCertificate", 0) $ChangedContact.PutEx(2, "userCertificate", [Array]$Contact.userCertificate) "  userCertificate  " + $Contact.distinguishedName | Write-LogFile $LogFileName } if ($Contact.otherTelephone -ne $null) { $ChangedContact.PutEx(1, "otherTelephone", 0) $ChangedContact.PutEx(2, "otherTelephone", [Array]$Contact.otherTelephone) "  otherTelephone  " + $Contact.distinguishedName | Write-LogFile $LogFileName } if ($Contact.CN -ne $null) { " " + $ChangedContact.distinguishedName + "   " + $Contact.CN | Write-LogFile $LogFileName $ChangedContact.Rename("CN="+$Contact.CN) } $ChangedContact.SetInfo() } 


Then we can only form the command line and create a schedule in the scheduler to run the script. Well, if our partners need our contacts, then we can certainly share this script with them.

Source: https://habr.com/ru/post/136773/


All Articles