В рамках проекта по созданию нового леса/домена Active Directory на 10к+ пользователей, возникла необходимость в создании инструмента, удовлетворяющего следующим требованиям:
- Созданием пользователей будут заниматься региональные администраторы;
- Необходима транслитерация имен и фамилий пользователей согласно постановлению кабинета министров Украины;
- Должен быть достаточный создан уровень удобства для администраторов при автоматическом заведении пользователей.
За не продолжительное время был написал PowerShell скрипт, листинг которого ниже:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 | # Импорт модуля Active Directory Domain Services Import-Module activedirectory $FileBrowser = New-Object System.Windows.Forms.OpenFileDialog -Property @{ InitialDirectory = [Environment]::GetFolderPath('Desktop') Filter = 'Text (*.csv)|*.csv' } $null = $FileBrowser.ShowDialog() $ADUsers = Import-Csv ($FileBrowser.FileName) -Delimiter ";" #Так как кадровый департемент отдавал списки сотрудников используя разделитель ";", он явно указан. Если используется стандартный ",", параметр "-Delimiter ";"" необходимо убрать. #Функция транслитерации function Translit { param([string]$inString) #Создаем хэш-таблицу соответствия украинских и латинских символов $Translit = @{ [char]'а' = "a" [char]'А' = "A" [char]'б' = "b" [char]'Б' = "B" [char]'в' = "v" [char]'В' = "V" [char]'г' = "h" [char]'Г' = "G" [char]'ґ' = "g" [char]'Ґ' = "G" [char]'д' = "d" [char]'Д' = "D" [char]'е' = "e" [char]'Е' = "E" [char]'Є' = "Ye" [char]'є' = "ie" [char]'ж' = "zh" [char]'Ж' = "Zh" [char]'з' = "z" [char]'З' = "Z" [char]'и' = "y" [char]'И' = "Y" [char]'І' = "I" [char]'і' = "i" [char]'Ї' = "Yi" [char]'ї' = "i" [char]'й' = "i" [char]'Й' = "Y" [char]'к' = "k" [char]'К' = "K" [char]'л' = "l" [char]'Л' = "L" [char]'м' = "m" [char]'М' = "M" [char]'н' = "n" [char]'Н' = "N" [char]'о' = "o" [char]'О' = "O" [char]'п' = "p" [char]'П' = "P" [char]'р' = "r" [char]'Р' = "R" [char]'с' = "s" [char]'С' = "S" [char]'т' = "t" [char]'Т' = "T" [char]'У' = "U" [char]'у' = "u" [char]'ф' = "f" [char]'Ф' = "F" [char]'х' = "kh" [char]'Х' = "Kh" [char]'ц' = "ts" [char]'Ц' = "Ts" [char]'ч' = "ch" [char]'Ч' = "Ch" [char]'ш' = "sh" [char]'Ш' = "Sh" [char]'щ' = "shch" [char]'Щ' = "Shch" [char]'ь' = "" [char]'Ь' = "" [char]'э' = "e" [char]'Э' = "E" [char]'ю' = "iu" [char]'Ю' = "Yu" [char]'я' = "ia" [char]'Я' = "Ya" [char]' ' = " " #пробел } $outString = ""; $chars = $inString.ToCharArray(); foreach ($char in $chars) {$outString += $Translit[$char]} return $outString; } # Функция была взята с https://gist.github.com/supercheetah/b68023f3254dfc9a6497 function Browse-AD() { $dc_hash = @{} $selected_ou = $null Import-Module ActiveDirectory $forest = Get-ADForest [System.Reflection.Assembly]::LoadWithPartialName("System.Drawing") | Out-Null [System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms") | Out-Null function Get-NodeInfo($sender, $dn_textbox) { $selected_node = $sender.Node $dn_textbox.Text = $selected_node.Name } function Add-ChildNodes($sender) { $expanded_node = $sender.Node if ($expanded_node.Name -eq "root") { return } $expanded_node.Nodes.Clear() | Out-Null $dc_hostname = $dc_hash[$($expanded_node.Name -replace '((OU|CN)=[^,]+,)*((DC=\w+,?)+)','$3')] $child_OUs = Get-ADObject -Server $dc_hostname -Filter 'ObjectClass -eq "organizationalUnit" -or ObjectClass -eq "container"' -SearchScope OneLevel -SearchBase $expanded_node.Name if($child_OUs -eq $null) { $sender.Cancel = $true } else { foreach($ou in $child_OUs) { $ou_node = New-Object Windows.Forms.TreeNode $ou_node.Text = $ou.Name $ou_node.Name = $ou.DistinguishedName $ou_node.Nodes.Add('') | Out-Null $expanded_node.Nodes.Add($ou_node) | Out-Null } } } function Add-ForestNodes($forest, [ref]$dc_hash) { $ad_root_node = New-Object Windows.Forms.TreeNode $ad_root_node.Text = $forest.RootDomain $ad_root_node.Name = "root" $ad_root_node.Expand() $i = 1 foreach ($ad_domain in $forest.Domains) { Write-Progress -Activity "Querying AD forest for domains and hostnames..." -Status $ad_domain -PercentComplete ($i++ / $forest.Domains.Count * 100) $dc = Get-ADDomainController -Server $ad_domain $dn = $dc.DefaultPartition $dc_hash.Value.Add($dn, $dc.Hostname) $dc_node = New-Object Windows.Forms.TreeNode $dc_node.Name = $dn $dc_node.Text = $dc.Domain $dc_node.Nodes.Add("") | Out-Null $ad_root_node.Nodes.Add($dc_node) | Out-Null } return $ad_root_node } $main_dlg_box = New-Object System.Windows.Forms.Form $main_dlg_box.ClientSize = New-Object System.Drawing.Size(400,600) $main_dlg_box.MaximizeBox = $false $main_dlg_box.MinimizeBox = $false $main_dlg_box.FormBorderStyle = 'FixedSingle' # widget size and location variables $ctrl_width_col = $main_dlg_box.ClientSize.Width/20 $ctrl_height_row = $main_dlg_box.ClientSize.Height/15 $max_ctrl_width = $main_dlg_box.ClientSize.Width - $ctrl_width_col*2 $max_ctrl_height = $main_dlg_box.ClientSize.Height - $ctrl_height_row $right_edge_x = $max_ctrl_width $left_edge_x = $ctrl_width_col $bottom_edge_y = $max_ctrl_height $top_edge_y = $ctrl_height_row # setup text box showing the distinguished name of the currently selected node $dn_text_box = New-Object System.Windows.Forms.TextBox # can not set the height for a single line text box, that's controlled by the font being used $dn_text_box.Width = (14 * $ctrl_width_col) $dn_text_box.Location = New-Object System.Drawing.Point($left_edge_x, ($bottom_edge_y - $dn_text_box.Height)) $main_dlg_box.Controls.Add($dn_text_box) # /text box for dN # setup Ok button $ok_button = New-Object System.Windows.Forms.Button $ok_button.Size = New-Object System.Drawing.Size(($ctrl_width_col * 2), $dn_text_box.Height) $ok_button.Location = New-Object System.Drawing.Point(($right_edge_x - $ok_button.Width), ($bottom_edge_y - $ok_button.Height)) $ok_button.Text = "Ok" $ok_button.DialogResult = 'OK' $main_dlg_box.Controls.Add($ok_button) # /Ok button # setup tree selector showing the domains $ad_tree_view = New-Object System.Windows.Forms.TreeView $ad_tree_view.Size = New-Object System.Drawing.Size($max_ctrl_width, ($max_ctrl_height - $dn_text_box.Height - $ctrl_height_row*1.5)) $ad_tree_view.Location = New-Object System.Drawing.Point($left_edge_x, $top_edge_y) $ad_tree_view.Nodes.Add($(Add-ForestNodes $forest ([ref]$dc_hash))) | Out-Null $ad_tree_view.Add_BeforeExpand({Add-ChildNodes $_}) $ad_tree_view.Add_AfterSelect({Get-NodeInfo $_ $dn_text_box}) $main_dlg_box.Controls.Add($ad_tree_view) # /tree selector $main_dlg_box.ShowDialog() | Out-Null return $dn_text_box.Text } $OU = Browse-AD #Запись DN в переменную OU, куда будут созданы новые пользователи. foreach ($User in $ADUsers) { $FirstnameEN = Translit($User.firstname) $Firstname = $User.firstname $LastnameEN = Translit ($User.lastname) $Lastname = $User.lastname $OtherNameEN = Translit ($User.othername) $OtherName = $User.othername $Username = $($FirstnameEN.Substring(0,1) + "." + $LastnameEN;).ToLower() $UserPrincipalName = $Username + "@corp.ait.in.ua" $Password = $User.password $Name = $FirstnameEN + " " + $LastnameEN $Description = $Lastname + " " + $Firstname + " " + $OtherName $Streetaddress = $User.streetaddress $City = $User.city $Office = $User.office $State = $User.state $Country = $User.country $Telephone = $User.telephone $Jobtitle = $User.jobtitle $Company = $User.company $Department = $User.department #Проверка на присуствие пользователя в каталоге с создаваемым SamAccountName if (Get-ADUser -F {SamAccountName -eq $Username}) { #В случае сработки, выдача предупреждения в консоль Write-Warning "A user account with username $Username already exist in Active Directory." } else { #Если все хорошо, создание нового пользователя New-ADUser -SamAccountName $Username ` -UserPrincipalName $UserPrincipalName ` -Name $Name ` -GivenName $FirstnameEN ` -AccountPassword (ConvertTo-SecureString $Password -AsPlainText -Force) ` -DisplayName $Name ` -Path $OU ` -Surname $LastnameEN ` -Enabled $True ` -City $City ` -Company $Company ` -State $State ` -StreetAddress $Streetaddress ` -OfficePhone $Telephone ` -Title $Jobtitle ` -Department $Department ` -ChangePasswordAtLogon $True ` -Description $Description ` -Country $Country ` -OtherAttributes @{'middleName'="$OtherNameEN"} } } |
Теперь разберем наших баранов:
- Путь к файлу csv содержащий данные пользователей задается в вызываемом стандартном окне проводника. В качестве защиты от дурака, сделан фильтр по маске расширения файла. Пример файла вы можете скачать тут. Необходимо обратить внимание на параметр Delimiter, так как он опционален.
- Транслитерация выполнена в виде отдельной функции. Во время ее работы имена и фамилии пропускаются через хэш-таблицу соответствия украинских и латинских символов. Источником служит постановление кабинета министров Украины от 27.01.2010 №55
- Для определение конечного организационного подразделения куда будут созданы пользователи, используется функция. Она создает графический интерфейс каталога Active Directory с помощью PowerShell. Функция была взята у пользователя supercheetah, ссылка на источник – Active Directory (AD) PowerShell picker/GUI · GitHub
- Пользователи создаются простым циклом. В качестве предохранителя внедрена проверка наличия создаваемого пользователя. Хочу обратить внимание на строку формирования имени пользователя – $Username = $($FirstnameEN.Substring(0,1) + “.” + $LastnameEN;).ToLower(). В качестве принятого стандарта имени пользователя согласовано использование первой буквы имени и фамилии полностью, разделяемые точкой. В масштабах организации это приемлемая политика именования, так как могут возникать дубликаты. Если такие будут, достаточно использовать две буквы имени и фамилию.
Подводим итоги. Представленный выше скрипт позволит значительно автоматизировать процесс создания пользователей Active Directory попутно выполняя транслитерацию имен и фамилий. Как писал выше, причины побудившие на его написания колоссальные объемы создаваемых пользователей. Но это никто не мешает его использовать и в меньших организациях.