Многие помнят то чувство, когда компания расширяется до тех размеров, когда рабочих групп недостаточно, и поднимается первый домен Active Directory: «О, уж теперь-то все будет как следует!» Ан нет, домен потихонечку разрастается, создаются новые учетки, блокируются старые, добавляются, удаляются компьютеры, девушки выходят замуж, меняют фамилии и, в конце концов, база данных службы каталогов выглядит, как полный швах. В этом топике мы наладим связь между базой Active Directory и кадровой системой предприятия, а также создадим механизм для поддержания данных сотрудников в AD в актуальном состоянии.
Первым делом, мы опишем требования, которые мы должны предъявить к учетным записям сотрудников. А эти требования мы постараемся прикинуть, исходя из потребностей пользователя. Не секрет, что многие корпоративные системы, использующие аутентификацию через Active Directory, для отображения и в своих админках, и просто в ходе работы зачастую используют разнообразные поля учетных записей AD: это и Sharepoint, и Citrix, и многие-многие другие. В качестве примера такой системы я возьму известный всем MS Outlook, да не полностью, а лишь его адресную книгу, которая черпает свои данные напрямую из Active Directory.

Что использует пользователь? У нас в организации он зачастую ищет по имени телефон, адрес электронной почты и название подразделения. Конечно, неплохо заполнить еще и адресную информацию, но в связи с тем, что топик у нас о связке с абстрактной кадровой системой, мы адреса и телефоны оставим за скобками.
Первое, мы выписываем список полей, которые мы желаем забирать из кадровой системы, у нас это будут:
Само собой ясно, что для того, чтобы связать персону из кадровой системы и персону из Active Directory, необходимо иметь некий идентификатор, связывающий эти две записи. Обычно таким идентификатором является табельный номер сотрудника, он присваивается при приеме на работу и более никогда не меняется, вместе с тем, я встречался с ситуациями, когда и табельный номер не статичен, в этом случае такой идентификатор следует выдумать.
Сведения о пользователе Active Directory не исчерпываются сведениями, которые можно увидеть в оснастке Active Directory Users and Computers (устоявшееся сокращение ADUC), причем очень далеко не исчерпываются. На самом деле объект пользователя имеет триллион атрибутов, и эти атрибуты даже могут быть добавлены администратором схемы. Например, есть такой атрибут, как carLicense, содержащий информацию о водительском удостоверении, или drink, характеризующий любимый напиток пользователя. В общем, Microsoft в этом смысле предусмотрела многое.
Использовать в моем примере я буду атрибуты employeeID для хранения идентификатора пользователя, и flags, для чего именно, сообщу чуть позже.
Также для заполнения полей пользователя мы будем использовать атрибуты:
Итак, наше приложение будет работать таким образом:
Первым делом следует проставить employeeID, который у нас представляет табельный номер, всем пользователям. Если пользователей мало, то сделать это проще всего через ADSI Edit, если их чуть больше, то можно прикрутить скрипт для прописывания, например вот так. А если пользователей много, расстановку идентификаторов необходимо делегировать, хочется приятный интерфейс и используются дополнительные фенечки, то можно создать вот такую дополнительную вкладку в ADUC:

впрочем, создание такой вкладки это само по себе тема для отдельного топика.
Второе тонкое место в том, что иногда случается так, что для некоторых людей менять следует только некоторые атрибуты. Есть, например, у нас сотрудник, назовем его Кудрымунбеков Садруддин Фатхулларович, но все его называют просто Сан Саныч. А есть генеральный директор, должность которого в кадрах записана не иначе, как Генеральный Директор Открытого Акционерного Общества Дальней Космической Связи «Рога И Копыта», которого в AD лучше бы просто оставить точно со связью, но точно без рогов и копыт. Таким образом, мы видим необходимость в закладывании в логику работы нашего приложения некоторых исключений, а хранить эти исключения мы будем там же, в Active Directory в атрибуте flags. Этот атрибут имеет величину в четыре байта, а значит, устанавливая тот или иной бит в то или иное значен��е, мы сможем при необходимости запомнить аж 32 исключения. Впрочем, использовать мы все равно будем только шесть.
Переходим к реализации на powershell:
Создадим тестовую среду, абсолютно произвольно присвоим имена учетным записям:

Теперь запускаем скрипт с параметрами. Разумеется, его можно зашедулить и выполнять по расписанию, только не забыть учетной записи, от имени которой выполняется задание, присвоить право входа в качестве пакетного задания:

Как видим, после выполнения, мы получили хорошие читаемые имена, отличные должности и великолепные наименования компаний:

Конечно, путем неглубокой модификации скрипта, мы можем заполнять пользователю все: от телефонов и адресов до пресловутых любимых напитков, был бы источник. А если скрипт запускать с определенной регулярностью, то мы добиваемся того, что все данные о пользователях будут актуальны.
upd Подправил маленькую ошибочку, перенес обнуление флажков во внутрь цикла
Первым делом, мы опишем требования, которые мы должны предъявить к учетным записям сотрудников. А эти требования мы постараемся прикинуть, исходя из потребностей пользователя. Не секрет, что многие корпоративные системы, использующие аутентификацию через Active Directory, для отображения и в своих админках, и просто в ходе работы зачастую используют разнообразные поля учетных записей AD: это и Sharepoint, и Citrix, и многие-многие другие. В качестве примера такой системы я возьму известный всем MS Outlook, да не полностью, а лишь его адресную книгу, которая черпает свои данные напрямую из Active Directory.

Что использует пользователь? У нас в организации он зачастую ищет по имени телефон, адрес электронной почты и название подразделения. Конечно, неплохо заполнить еще и адресную информацию, но в связи с тем, что топик у нас о связке с абстрактной кадровой системой, мы адреса и телефоны оставим за скобками.
Первое, мы выписываем список полей, которые мы желаем забирать из кадровой системы, у нас это будут:
- Фамилия Имя Отчество
- Должность
- Организация
- Подразделение
- Почтовый индекс
- Тип занятости
Механизм
Само собой ясно, что для того, чтобы связать персону из кадровой системы и персону из Active Directory, необходимо иметь некий идентификатор, связывающий эти две записи. Обычно таким идентификатором является табельный номер сотрудника, он присваивается при приеме на работу и более никогда не меняется, вместе с тем, я встречался с ситуациями, когда и табельный номер не статичен, в этом случае такой идентификатор следует выдумать.
Сведения о пользователе Active Directory не исчерпываются сведениями, которые можно увидеть в оснастке Active Directory Users and Computers (устоявшееся сокращение ADUC), причем очень далеко не исчерпываются. На самом деле объект пользователя имеет триллион атрибутов, и эти атрибуты даже могут быть добавлены администратором схемы. Например, есть такой атрибут, как carLicense, содержащий информацию о водительском удостоверении, или drink, характеризующий любимый напиток пользователя. В общем, Microsoft в этом смысле предусмотрела многое.
Использовать в моем примере я буду атрибуты employeeID для хранения идентификатора пользователя, и flags, для чего именно, сообщу чуть позже.
Также для заполнения полей пользователя мы будем использовать атрибуты:
- displayName и CN для хранения ФИО
- department для хранения подразделения
- company для хранения организации
- title для хранения должности
- employeeType для хранения типа сотрудника
- postalCode для хранения индекса
Итак, наше приложение будет работать таким образом:
- Перечислять учетные записи, у которых заполнен employeeID
- Искать в кадровой системе для каждой учетной записи изменившиеся данные
- Обновлять данные в Active Directory
- Протоколировать изменения в файле
К делу
Первым делом следует проставить employeeID, который у нас представляет табельный номер, всем пользователям. Если пользователей мало, то сделать это проще всего через ADSI Edit, если их чуть больше, то можно прикрутить скрипт для прописывания, например вот так. А если пользователей много, расстановку идентификаторов необходимо делегировать, хочется приятный интерфейс и используются дополнительные фенечки, то можно создать вот такую дополнительную вкладку в ADUC:

впрочем, создание такой вкладки это само по себе тема для отдельного топика.
Второе тонкое место в том, что иногда случается так, что для некоторых людей менять следует только некоторые атрибуты. Есть, например, у нас сотрудник, назовем его Кудрымунбеков Садруддин Фатхулларович, но все его называют просто Сан Саныч. А есть генеральный директор, должность которого в кадрах записана не иначе, как Генеральный Директор Открытого Акционерного Общества Дальней Космической Связи «Рога И Копыта», которого в AD лучше бы просто оставить точно со связью, но точно без рогов и копыт. Таким образом, мы видим необходимость в закладывании в логику работы нашего приложения некоторых исключений, а хранить эти исключения мы будем там же, в Active Directory в атрибуте flags. Этот атрибут имеет величину в четыре байта, а значит, устанавливая тот или иной бит в то или иное значен��е, мы сможем при необходимости запомнить аж 32 исключения. Впрочем, использовать мы все равно будем только шесть.
Переходим к реализации на powershell:
# Пример изменения учетных записей пользователей в Active Directory # Егор Иванов param($strServer, $strContainer, $strUserName, $strPassword, $strFileName, $strLogName) function Write-LogFile([string]$logFileName) { Process { $_ $dt = Get-Date $str = $dt.DateTime + " " + $_ $str | Out-File -FilePath $logFileName -Append } } # Эта функция на самом деле заглушка, ее реализация зависит # от той или иной кадровой системы. Тут может быть и соединение с 1С, # и запрос в веб, у меня лично тут ковыряние Oracle e-Buisness suite, # но демонстрации ради мы ограничимся чтением из csv-файла. # Понятное дело, что вызывать каждый раз Import-CSV глупо, # но как я и говорил, функция - заглушка, она лишь демонстрирует возможность function Get-Employee($employeeID, $fileName, [ref]$title, [ref]$department, [ref]$displayName, [ref]$company, [ref]$postalCode, [ref]$employeeType) { $records = $fileName | Import-CSV -Delimiter ";" $employee = $records | where-object {$_.EmployeeID -eq $employeeID} if ($employee -eq $null) {return $false} $title.Value = [string]$employee.Title $department.Value = [string]$employee.Department $displayName.Value = [string]$employee.Name $company.Value = [string]$employee.Company $postalCode.Value = [string]$employee.PostalCode $employeeType.Value = [string]$employee.EmployeeType return $true } # Будем писать в лог "---" | Write-LogFile $strLogName "Запускаю с параметрами:" | Write-LogFile $strLogName "Сервер: " + $strServer | Write-LogFile $strLogName "Контейнер: " + $strContainer | Write-LogFile $strLogName "Имя пользователя: " + $strUserName | Write-LogFile $strLogName "Пароль: " + $strPassword | Write-LogFile $strLogName "Имя файла: " +$strFileName | Write-LogFile $strLogName "Имя файла лога: " + $strLogName | Write-LogFile $strLogName # Это наши константы, которые переводят те или иные атрибуты в режим только чтения # Нетрудно заметить, что они имеют значения 000001, 000010, 000100, 001000, 010000 и 100000 # в двоичной системе. Это значит, что их комбинация однозначно определит, # какие поля запретить изменять New-Variable -Option constant -Name C_COMPANY_FLAG -Value 1 New-Variable -Option constant -Name C_POSTALCODE_FLAG -Value 2 New-Variable -Option constant -Name C_TITLE_FLAG -Value 4 New-Variable -Option constant -Name C_DEPARTMENT_FLAG -Value 8 New-Variable -Option constant -Name C_NAME_FLAG -Value 16 New-Variable -Option constant -Name C_EMPLOYEETYPE_FLAG -Value 32 # Ниже заглушка для атрибута title. Атрибут title к примеру # http://msdn.microsoft.com/en-us/library/windows/desktop/ms680037(v=VS.85).aspx # имеет 64 символа максимальный размер в Windows Server 2003 # или 128 символов максимальный размер в Windows Server 2008 # поэтому если не обрезать значение, случится конфуз New-Variable -Option constant -Name C_PARAMETERS_LENGTH -Value 64 # (!userAccountControl:1.2.840.113556.1.4.803:=2) читать как "и при этом учетка не заблокирована" $strFilter = "(&(objectClass=user)(!objectClass=computer)(employeeID=*)(!userAccountControl:1.2.840.113556.1.4.803:=2))" # Можно, конечно, использовать навески для Active Directory под Windows Server 2008 # http://blogs.msdn.com/adpowershell # Но я решил сделать приложение совместимым с Windows Server 2003 и Windows XP, # поэтому обойдемся сухим дотнетом $objDomain = New-Object System.DirectoryServices.DirectoryEntry("LDAP://"+$strServer+"/"+$strContainer) $objSearcher = New-Object System.DirectoryServices.DirectorySearcher $objSearcher.SearchRoot = $objDomain $objSearcher.PageSize = 1000 $objSearcher.Filter = $strFilter $objSearcher.SearchScope = "Subtree" $colProplist = "employeeID","postalCode","title","department", "displayName", "cn", "employeeType" foreach ($i in $colPropList) { $objSearcher.PropertiesToLoad.Add($i) } $colResults = $objSearcher.FindAll() # Теперь в colResults мы имеем все необходимые учетки $startTime = Get-Date $totalCount = $colResults.Count $i = 0 foreach ($objResult in $colResults) { $objItem = $objResult.Properties $aDEmployeeID = $objItem.employeeid # Тут ясно, мы смотрим в атрибут flags, если у нас есть запрет на изменение # поля, то мы это на будущее запоминаем, поднимая тот или иной флажок $flagProtectCompany = $false $flagProtectPostalCode = $false $flagProtectTitle = $false $flagProtectDepartment = $false $flagProtectName = $false $flagProtectEmployeeType = $false if (!($objItem.flags -eq $null)) { $flags = $objItem.flags if (($flags[0] -band $C_COMPANY_FLAG) -ne 0) {$flagProtectCompany = $true} if (($flags[0] -band $C_POSTALCODE_FLAG) -ne 0) {$flagProtectPostalCode = $true} if (($flags[0] -band $C_TITLE_FLAG) -ne 0) {$flagProtectTitle = $true} if (($flags[0] -band $C_DEPARTMENT_FLAG) -ne 0) {$flagProtectDepartment = $true} if (($flags[0] -band $C_NAME_FLAG) -ne 0) {$flagProtectName = $true} if (($flags[0] -band $C_EMPLOYEETYPE_FLAG) -ne 0) {$flagProtectEmployeeType = $true} } # Это не обязательно, но я предпочитаю все обнулить $cSVName = "" $cSVTitle = "" $cSVDepartment = "" $cSVCompany = "" $cSVPostalCode = "" $cSVEmployeeType = "" # Тут следует обратить внимание на вызов функции в PowerShell, он не совсем # такой, как в привычных языках $rc = Get-Employee $aDEmployeeID $strFileName ([ref]$cSVTitle) ([ref]$cSVDepartment) ([ref]$cSVName) ([ref]$cSVCompany) ([ref]$cSVPostalCode) ([ref]$cSVEmployeeType) if ($rc) { # Здесь мы соединяемся со службой каталогов уже под другим именем и паролем, нежели был # запущен сценарий. Неразумно осуществлять изменения от имени администратора домена, # разумнее делегировать изменения того, того, того и сего атрибута служебной учетке # с ограниченными правами $objDirectoryEntry = new-object System.DirectoryServices.DirectoryEntry($objItem.adspath, $strUsername, $strPassword, [System.DirectoryServices.AuthenticationTypes]::Secure) $oTitle = $cSVTitle if ($oTitle.Length -gt $C_PARAMETERS_LENGTH) {$oTitle = $oTitle.Substring(0,$C_PARAMETERS_LENGTH)} $oDepartment = $cSVDepartment if ($oDepartment.Length -gt $C_PARAMETERS_LENGTH) {$oDepartment = $oDepartment.Substring(0,$C_PARAMETERS_LENGTH)} $newEmployeeType = $cSVEmployeeType # Здесь и далее мы проверяем, совпадает ли то значение, которое мы хотим присвоить, тому значению, # которое уже присвоено (и не забываем про флажок). Это чтоб не напрягать службу каталогов if (($newEmployeeType -ne $objItem.employeetype) -and -not $flagProtectEmployeeType) { "Изменение EmployeeType пользователя """ + $objDirectoryEntry.name + """" | Write-LogFile $strLogName "с """ + $objDirectoryEntry.employeetype + """ на """ + $newEmployeeType + """" | Write-LogFile $strLogName $objDirectoryEntry.employeetype = [string]$newEmployeeType $objDirectoryEntry.CommitChanges() } if (($cSVCompany -ne $objItem.company) -and -not $flagProtectCompany) { "Изменение организации пользователя """ + $objDirectoryEntry.name + """" | Write-LogFile $strLogName "с """ + $objDirectoryEntry.company + """ на """ + $cSVCompany + """" | Write-LogFile $strLogName $objDirectoryEntry.company = [string]$cSVCompany $objDirectoryEntry.CommitChanges() } if (($cSVPostalCode -ne $objItem.postalcode) -and -not $flagProtectPostalCode) { "Изменение индекса пользователя """ + $objDirectoryEntry.name + """" | Write-LogFile $strLogName "с """ + $objDirectoryEntry.postalCode + """ на """ + $cSVPostalCode + """" | Write-LogFile $strLogName $objDirectoryEntry.postalCode = $cSVPostalCode $objDirectoryEntry.CommitChanges() } if (($oTitle -ne $objItem.title) -and -not $flagProtectTitle) { "Изменение должности пользователя """ + $objDirectoryEntry.name + """" | Write-LogFile $strLogName "с """ + $objDirectoryEntry.title + """ на """ + $cSVTitle + """" | Write-LogFile $strLogName if ($title.Length -gt $C_PARAMETERS_LENGTH) { $objDirectoryEntry.title = $cSVTitle.Substring(0,$C_PARAMETERS_LENGTH) } else { $objDirectoryEntry.title = $cSVTitle.ToString() } $objDirectoryEntry.CommitChanges() } if (($oDepartment -ne $objItem.department) -and -not $flagProtectDepartment) { "Изменение подразделения пользователя """ + $objDirectoryEntry.name + """" | Write-LogFile $strLogName "с """ + $objDirectoryEntry.department + """ на """ + $cSVDepartment + """" | Write-LogFile $strLogName if ($department.Length -gt $C_PARAMETERS_LENGTH) { $objDirectoryEntry.department = $cSVDepartment.Substring(0,$C_PARAMETERS_LENGTH) } else { $objDirectoryEntry.department = $cSVDepartment.ToString() } $objDirectoryEntry.description = $cSVDepartment.ToString() $objDirectoryEntry.CommitChanges() } if ((($cSVName -ne $objItem.displayname) -or ($cSVName -ne $objItem.cn)) -and -not $flagProtectName) { "Изменение имени пользователя """ + $objDirectoryEntry.name + """" | Write-LogFile $strLogName "с """ + $objDirectoryEntry.displayname + """ на """ + $cSVName + """" | Write-LogFile $strLogName $objDirectoryEntry.displayName = $cSVName $objDirectoryEntry.CommitChanges() $objDirectoryEntry.Rename("cn="+$cSVName) } $i++ # Здесь мы сформируем статусную строку и будем ее демонстрировать, простые математические действия # укажут нам, когда же, наконец, процесс прочесывания миллиардов наших пользователей прекратится $status = $i.ToString() + " of " + $totalCount.ToString() + " complete - " + $objDirectoryEntry.name $currentTime = Get-Date $diffTime = [int][System.Math]::Round(($currentTime - $startTime).Ticks / $i) $delta = $diffTime*$totalCount $endTime = $startTime.Add([int64]($delta)) $activityString = "Перебор пользователей. Расчетное время завершения " + $endTime Write-Progress -Activity $activityString -Status $status -PercentComplete (($i / $totalCount) * 100) } } "Работа окончена" | Write-LogFile $strLogName # И не забыть пропищать из динамика, мало ли Write-Host `a
Создадим тестовую среду, абсолютно произвольно присвоим имена учетным записям:

Теперь запускаем скрипт с параметрами. Разумеется, его можно зашедулить и выполнять по расписанию, только не забыть учетной записи, от имени которой выполняется задание, присвоить право входа в качестве пакетного задания:

Как видим, после выполнения, мы получили хорошие читаемые имена, отличные должности и великолепные наименования компаний:

Конечно, путем неглубокой модификации скрипта, мы можем заполнять пользователю все: от телефонов и адресов до пресловутых любимых напитков, был бы источник. А если скрипт запускать с определенной регулярностью, то мы добиваемся того, что все данные о пользователях будут актуальны.
upd Подправил маленькую ошибочку, перенес обнуление флажков во внутрь цикла
