Многие помнят то чувство, когда компания расширяется до тех размеров, когда рабочих групп недостаточно, и поднимается первый домен 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 Подправил маленькую ошибочку, перенес обнуление флажков во внутрь цикла