[Из песочницы] Автоматическое управление паролями в Active Directory

Однажды мне всё это надоело…Вероятно, в большинстве случаев именно с этой фразы начинается творчество системных администраторов. В результате мы видим (хотя, правильнее сказать, даже и не замечаем) появление множества маленьких программ, которые выполняют свои точные и строго определённые задачи в одной большой системе.Случилась (да и регулярно случается) со мной подобная история. Не скажу, что я изобрёл что-то новое и выдающееся. Скорее наоборот — воспользовался трудами коллег, найденными в просторах интернета и в кладезях премудрости Хабра. Но мне удалось объединить их для решения вполне конкретной и достаточно интересной задачи. Далее я опишу конкретное решение конкретной задачи по управлению паролями пользователей в Active Directory. Точнее, автоматизацию проверки срока действия этих паролей и генерации новых паролей. В качестве признательности коллегам я счёл необходимым опубликовать это решение здесь, в надежде, что оно кому-то пригодится или послужит источником новых идей.Итак, существует некая организация с могучей и разветвлённой филиальной сетью. Филиалов много по всей нашей необъятной Родине и все они разнокалиберны. Большая часть из них включена в корпоративную сеть с доменной структурой, но множество подключено по принципу home-office. В дополнение к тому многие сотрудники постоянно находятся в длительных командировках без возможности подключаться к доменной сети и к интернету вообще.

В результате часто возникает проблема просроченных паролей. Политикой компании определён запрет на бессрочные пароли, а требования с строгости паролей достаточно суровы, что вызывает у пользователей сложности с их придумыванием и заменой. Соответственно, ничтоже сумняшеся свою головную боль они радостно перекладывают на IT поддержку, звоня и требуя сменить их уже недействующий пароль. Регулярно. Надоело.

Итак, что же мне захотелось сделать? Мне нужно средство, которое: • само проверяло срок истечения действия пароля пользователя; • предварительно предупреждало его о дате смены пароля по электропочте; • предлагало пользователю вариант нового пароля; • если пользователь не успел сменить пароль, автоматически заменяло его на новый; • уведомляло пользователя о новом пароле посредством SMS.

Интерес состоял в том, чтобы решить эту задачу максимально подручными средствами, не привлекая сторонних услуг и сервисов. Ну вот не было никакого желания выбирать тарифы и пакеты. Зато был свободный GSM-модем. И всемогущий PowerShell.

В результате получился скрипт, а точнее — два скрипта. Почему так, объясняется просто — так сложилось исторически. Дело в том, что проверку паролей производит скрипт на виртуальной машине, расположенной в одном филиале, а рассылкой уведомлений по SMS занимается другая машина, расположенная в противоположной части страны. Из-за условий мобильного оператора иначе делать было нерентабельно.

Далее привожу оба скрипта целиком, которые я максимально прокомментировал. Выглядят они немного кучеряво. У меня не было особой потребности их причёсывать, поскольку работают они хорошо и в таком виде:

# Скрипт производит проверку паролей, срок действия которых истекает завтра, # отсылает владельцу новый пароль по email, # и автоматически заменяет, если срок действия паролей истёк. # # функция записи логов. $dt=Get-Date -Format «dd-MM-yyyy» $setupFolder = «c:\Active_Directory\Log» New-Item -ItemType directory -Path $setupFolder -Force | out-null #Создаю директорию для логов $global: logfilename=«C:\Active_Directory\Log\»+$dt+»_LOG.log» [int]$global: errorcount=0 #Ведем подсчет ошибок [int]$global: warningcount=0 #Ведем подсчет предупреждений function global: Write-log # Функция пишет сообщения в лог-файл и выводит на экран. {param ($message,[string]$type=«info»,[string]$logfile=$global: logfilename,[switch]$silent) $dt=Get-Date -Format «dd.MM.yyyy HH: mm: ss» $msg=$dt + »`t» + $type + »`t» + $message #формат: 01.01.2001 01:01:01 [tab] error [tab] Сообщение Out-File -FilePath $logfile -InputObject $msg -Append -encoding unicode if (-not $silent.IsPresent) { switch ($type.toLower ()) { «error» { $global: errorcount++ write-host $msg -ForegroundColor red } «warning» { $global: warningcount++ write-host $msg -ForegroundColor yellow } «completed» { write-host $msg -ForegroundColor green } «info» { write-host $msg } default { write-host $msg } } } }

#Функция генератора сложных паролей function global: Get-RandomPassword { <# Функция генератора паролей PasswordLength - длина пароля #> [CmdletBinding ()] param ( [Parameter (Position=0, Mandatory=$true, ValueFromPipeline=$true)] [ValidateRange (4,15)] [Int] $PasswordLength ) Begin{} Process{ $numberchars=0…9 | % {$_.ToString ()} $lochars = [char]'a' … [char]'z' | % {[char]$_} $hichars = [char]'A' … [char]'Z' | % {[char]$_} $punctchars = [char[]](33…47) $PasswordArray = Get-Random -InputObject @($hichars + $lochars + $numberchars + $punctchars) -Count $PasswordLength $char1 = Get-Random -InputObject $hichars $char2 = Get-Random -InputObject $lochars $char3 = Get-Random -InputObject $numberchars $char4 = Get-Random -InputObject $punctchars $RndIndexArray = Get-Random (0…($PasswordLength-1)) -Count 4 $PasswordArray[$RndIndexArray[0]] = $char1 $PasswordArray[$RndIndexArray[1]] = $char2 $PasswordArray[$RndIndexArray[2]] = $char3 $PasswordArray[$RndIndexArray[3]] = $char4 return [system.string]:: Join ('', $PasswordArray) } End{} }

#SMTP адрес почтового сервера $smtpServer = «mail.domain.local» #создаем объект письмо $msg = new-object Net.Mail.MailMessage $msgr = new-object Net.Mail.MailMessage #создаем объект почтовый сервер $smtp = new-object Net.Mail.SmtpClient ($smtpServer) # Функция для сообщения пользователю Function EmailStructure ($to,$expiryDate,$upn) { $msg.IsBodyHtml = $true $msg.From = «ITHelpDeskRussia@mantracvostok.com» $msg.To.Clear () $msg.To.Add ($to) $msg.Subject = «Password expiration notice» $msg.Body = »This is an automatically generated message from Company IT Service.

Please note that the password for your account domain\$upn will expire on $expiryDate.

System automatically generated a new password for you.
You can use password — $generated_password
Please change your password immediately or at least before this date as you will be unable to access the service without contacting your administrator.
If you will not change your password, System set it automatically.
»}

# Функция для отчёта администратору Function EmailStructureReport ($to) { $msgr.IsBodyHtml = $true $msgr.From = «PasswordChecker@local.domain» $msgr.To.Add ($to) $msgr.Subject = «Script running report» $msgr.Body = »This is a daily report.

Script for check expiried passwords has successfully completed its work.
$NotificationCounter users have recieved notifications:

$ListOfAccounts

»}

# Подключаем модуль для работы с Active Directory Import-Module activedirectory # получаем список всех активированных российских пользователей, у которых установлен срок действия пароля $NotificationCounter = 0 $OU = «OU=Russia, DC=local, DC=domain» $ADAccounts = Get-ADUser -LDAPFilter »(objectClass=user)» -searchbase $OU -properties PasswordExpired, employeeNumber, PasswordNeverExpires, PasswordLastSet, Mail, mobile, Enabled | Where-object {$_.Enabled -eq $true -and $_.PasswordNeverExpires -eq $false}

# для каждого пользователя foreach ($ADAccount in $ADAccounts) #проверяем политику сложности пароля { $accountFGPP = Get-ADUserResultantPasswordPolicy $ADAccount if ($accountFGPP -ne $null) { $maxPasswordAgeTimeSpan = $accountFGPP.MaxPasswordAge } else { $maxPasswordAgeTimeSpan = (Get-ADDefaultDomainPasswordPolicy).MaxPasswordAge } #Заполняем переменные пользовательскими данными $samAccountName = $ADAccount.samAccountName $userEmailAddress = $ADAccount.mail $userPrincipalName = $ADAccount.UserPrincipalName $userStorePassword = $ADAccount.employeeNumber $usermobile = $ADAccount.mobile # Для каждого из пользователей, не успевшего сменить пароль if ($ADAccount.PasswordExpired) { # Считываем пароль из атрибутного поля AD # Если нет ранее сохранённого пароля, устанавливаем пароль по умолчанию — Pa$$w0rd if ($userStorePassword -eq $NULL -or $useStorePassword -eq » ») { $userStorePassword = «Pa$$w0rd» } # Заменяем пароль на новый $newpwd = ConvertTo-SecureString -String $userStorePassword -AsPlainText –Force Set-ADAccountPassword -Identity $samAccountName -NewPassword $newpwd –Reset # Сохраняем новый пароль и номер мобильного телефона в TXT файл if ($usermobile -ne $NULL) { $SMSfile=«C:\ActiveDirectory\SMS_notice.txt» $SMSMessage=$usermobile + »,» + $userStorePassword Out-File -FilePath $SMSfile -InputObject $SMSMessage -Append -encoding unicode } # Делаем запись в журнале write-log «for $samAccountName will set a stored password — $userStorePassword. Message send to mobile — $usermobile» write-log »---------------------------------------------------------------------------------------------------------» # Очищаем атрибутное поле AD Set-ADUser $samAccountName -employeeNumber $null } else # Для всех тех, у кого пароль истекает завтра, то есть $DaysToExpireDD меньше 2 { $ExpiryDate = $ADAccount.PasswordLastSet + $maxPasswordAgeTimeSpan $TodaysDate = Get-Date $DaysToExpire = $ExpiryDate — $TodaysDate #Вычисляем дней до просрочки в DaysToExpireDD в формате дней $DaysToExpireDD = $DaysToExpire.ToString () -Split (»\S{17}$») if (($DaysToExpire.Days -le 2)) { Write-log «The password for account $samAccountName expires on: $ExpiryDate. Days left: $DaysToExpireDD # Генерируем новый пароль в переменную $generated_password $generated_password = Get-RandomPassword 10 write-log «Generated password: $samAccountName — $generated_password» write-log »-----------------------------------------------------------------------------------------»

# Записываем новый пароль в атрибутное полe AD. Будем пользоваться атрибутом employeeNumber Set-ADUser $samAccountName -employeeNumber $generated_password # отсылаем письмо с предупреждением пользователю if ($userEmailAddress) #проверяем наличие адреса электронной почты у пользователя. { EmailStructure $userEmailAddress $expiryDate $samAccountName $smtp.Send ($msg) write-log «NOTIFICATION — $samAccountName: e-mail was sent to $userEmailAddress» $NotificationCounter = $NotificationCounter + 1 $ListOfAccounts = $ListOfAccounts + $samAccountName + » — $DaysToExpireDD days left. Sent to $userEmailAddress
» } } } } #Отправляем список новых паролей на сервер, который занимается отправкой SMS # Если список существует If (Test-Path $SMSfile) { Copy-Item -Path $SMSfile -Destination \\SMS-Send-Server.local.domain\C$\ActiveDirectory\SMS_notice.txt # Удаляем файл со списком новых паролей Remove-Item $SMSfile } # отсылаем копию отчёта администратору Write-log «SENDING REPORT TO IT DEPARTMENT» EmailStructureReport («ITHelpdeskRussia@mantracvostok.com») $smtp.Send ($msgr) Этот скрипт добавим в Планировщик Заданий Windows, настроив его на выполнение в нужное нам время. Например, ночью.К сожалению, скрипт проверяет просроченные пароли в момент своего выполнения. Так что если срок действия пароля истекает днём, то он его не будет учитывать. Но ведь нам это и не требуется, ибо в рабочее время сотрудник может поменять пароль самостоятельно.

В результате мы получаем список мобильных номеров пользователей, которым установлен новый пароль. Этот список мы отправим на сервер, к которому подключен GSM-модем. А там этим списком займется уже следующий скрипт.

# #Скрипт получает список мобильный номеров и сообщений из файла и рассылает пользователям # # указываем, где хранится файл со списком $sms_text_filename = «SMS_notice.txt» $PathToSmsPrepareToSend = «C:\ActiveDirectory» + »\» + $sms_text_filename $dt=Get-Date -Format «dd.MM.yyyy» # указываем, куда мы будем сохранять журнал событий $of=«C:\ActiveDirectory\Log\»+$dt+»_LOG.log» # Проверяем наличие списка сообщений If (Test-Path $PathToSmsPrepareToSend) { $SMS = Import-Csv $PathToSmsPrepareToSend -Header mobile, newpassword # для каждой строки из списка сообщений foreach ($SM in $SMS) { # $mobileForSMS = $SM.mobile # $passwordFroSMS = $SM.newpassword # echo $mobileForSMS # Объявляем экземпляр класса SerialPort $serialPort = new-Object System.IO.Ports.SerialPort # Устанавливаем переменные настроек порта, к которому подключен модем <# !!!Важно!!! USB-модем использует три COM порта. Нам нужен тот, который отображается в Диспетчере устройств в настройках модема. Если воткнуть GSM-модем в другой USB порт, то номер COM порта изменится. #> $serialPort.PortName = «COM3» $serialPort.BaudRate = 115200 $serialPort.WriteTimeout = 500 $serialPort.ReadTimeout = 3000 $serialPort.DtrEnable = «true» # Открываем порт # $serialPort.Open () # Сохраняем номер телефона и сообщение в переменные # Удаляем лишние пробелы в номере телефона $phoneNumber = [Regex]:: replace ($SM.mobile,'\s','') $textMessage = «Your new password — » + $SM.newpassword try { $serialPort.Open () } catch { # Ждём 5 секунд и пытаемся снова Sleep -Milliseconds 500 $serialPort.Open () } If ($serialPort.IsOpen -eq $true) { # Указываем модему, что будем использовать режим AT-команд $serialPort.Write («AT+CMGF=1`r`n») Sleep -Milliseconds 500 # Отправляем данные в модем # Сначала номер телефона в международном формате # и символы в конце $serialPort.Write («AT+CMGS=`»$phoneNumber`»`r`n») # Даём модему время на обработку Sleep -Milliseconds 500 # Записываем в модем наше сообщение $serialPort.Write (»$textMessage`r`n») Sleep -Milliseconds 500 # отсылаем в модем Ctrl+Z в качестве завершения сообщения. $serialPort.Write ($([char] 26)) # подождём, пока модем отошлёт сообщение Sleep -Milliseconds 500 } # Закрываем порт $serialPort.Close () if ($serialPort.IsOpen -eq $false) { # записываем результат в журнал $dts=Get-Date -Format «dd.MM.yyyy HH: mm: ss» $msg=$dts+» : Message »+$textMessage+» send to »+ $phoneNumber Out-File -FilePath $of -InputObject $msg -Append -encoding unicode } Sleep -Milliseconds 1000 } #Конец цикла обработки строки из списка # переименовываем файл списка сообщений для сохранения в истории $newname =$dt+»_»+$sms_text_filename rename-item -path $PathToSmsPrepareToSend -newname $newname } #Конец проверки существования списка Else # Если списка сообщений не существует { # Делаем запись в журнале, что сообщений для отправки не было $dts=Get-Date -Format «dd.MM.yyyy HH: mm: ss» $msg=$dts + » : No data to send SMS» Out-File -FilePath $of -InputObject $msg -Append -encoding unicode } Скрипты проверены в боевых условиях и показали себя с наилучшей стороны.Я не буду объяснять, почему сделал именно так, поскольку задача была достаточно конкретна. И решение получилось вполне конкретное.

Но я буду рад любым советам по улучшению или оптимизации скриптов.

© Habrahabr.ru