Автоматическое управление паролями в 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 =      "<html><body><font face='Arial'>This is an automatically generated message from Company IT Service.<br><br>      <b>Please note that the password for your account <i><u>domain\$upn</u></i> will expire on $expiryDate.</b><br><br>      System automatically generated a new password for you. <br>      You can use password - <b>$generated_password</b><br>      Please change your password immediately or at least before this date as you will be unable to access the service without contacting your administrator.<br>      If you will not change your password, System set it automatically.<br>      </font></body></html>"}  # Функция для отчёта администратору Function EmailStructureReport($to) { 	$msgr.IsBodyHtml = $true 	$msgr.From = "PasswordChecker@local.domain" 	$msgr.To.Add($to) 	$msgr.Subject = "Script running report" 	$msgr.Body =  "<html><body><font face='Arial'><b>This is a daily report.<br> <br>Script for check expiried passwords has successfully completed its work. <br>$NotificationCounter users have recieved notifications:<br> <br>$ListOfAccounts<br><br></b></font></body></html>"}  # Подключаем модуль для работы с 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<br>"			}                   		     } 			} 		} #Отправляем список новых паролей на сервер, который занимается отправкой 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             # Отправляем данные в модем             # Сначала номер  телефона в международном формате             # и символы  <CL> в конце             $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 } 

Скрипты проверены в боевых условиях и показали себя с наилучшей стороны.

Я не буду объяснять, почему сделал именно так, поскольку задача была достаточно конкретна. И решение получилось вполне конкретное.

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

ссылка на оригинал статьи http://habrahabr.ru/post/255197/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *