Как стать автором
Поиск
Написать публикацию
Обновить

Актуализируем учетные данные Active Directory

Время на прочтение10 мин
Количество просмотров83K
Многие помнят то чувство, когда компания расширяется до тех размеров, когда рабочих групп недостаточно, и поднимается первый домен 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, для чего именно, сообщу чуть позже.

Также для заполнения полей пользователя мы будем использовать атрибуты:
  • displayName и CN для хранения ФИО
  • department для хранения подразделения
  • company для хранения организации
  • title для хранения должности
  • employeeType для хранения типа сотрудника
  • postalCode для хранения индекса
Педанты могут, конечно, дополнительно использовать givenName, initials и sn для хранения имени, инициалов и фамилии соответственно, но я думаю, что это уже тонкости.

Итак, наше приложение будет работать таким образом:
  1. Перечислять учетные записи, у которых заполнен employeeID
  2. Искать в кадровой системе для каждой учетной записи изменившиеся данные
  3. Обновлять данные в Active Directory
  4. Протоколировать изменения в файле

К делу


Первым делом следует проставить 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 Подправил маленькую ошибочку, перенес обнуление флажков во внутрь цикла
Теги:
Хабы:
Всего голосов 30: ↑29 и ↓1+28
Комментарии21

Публикации

Ближайшие события