Однажды мне всё это надоело…
Вероятно, в большинстве случаев именно с этой фразы начинается творчество системных администраторов. В результате мы видим (хотя, правильнее сказать, даже и не замечаем) появление множества маленьких программ, которые выполняют свои точные и строго определённые задачи в одной большой системе.
Случилась (да и регулярно случается) со мной подобная история. Не скажу, что я изобрёл что-то новое и выдающееся. Скорее наоборот – воспользовался трудами коллег, найденными в просторах интернета и в кладезях премудрости Хабра. Но мне удалось объединить их для решения вполне конкретной и достаточно интересной задачи. Далее я опишу конкретное решение конкретной задачи по управлению паролями пользователей в Active Directory. Точнее, автоматизацию проверки срока действия этих паролей и генерации новых паролей. В качестве признательности коллегам я счёл необходимым опубликовать это решение здесь, в надежде, что оно кому-то пригодится или послужит источником новых идей.
Итак, существует некая организация с могучей и разветвлённой филиальной сетью. Филиалов много по всей нашей необъятной Родине и все они разнокалиберны. Большая часть из них включена в корпоративную сеть с доменной структурой, но множество подключено по принципу home-office. В дополнение к тому многие сотрудники постоянно находятся в длительных командировках без возможности подключаться к доменной сети и к интернету вообще.
В результате часто возникает проблема просроченных паролей. Политикой компании определён запрет на бессрочные пароли, а требования с строгости паролей достаточно суровы, что вызывает у пользователей сложности с их придумыванием и заменой. Соответственно, ничтоже сумняшеся свою головную боль они радостно перекладывают на IT поддержку, звоня и требуя сменить их уже недействующий пароль. Регулярно. Надоело.
Итак, что же мне захотелось сделать? Мне нужно средство, которое:
• само проверяло срок истечения действия пароля пользователя;
• предварительно предупреждало его о дате смены пароля по электропочте;
• предлагало пользователю вариант нового пароля;
• если пользователь не успел сменить пароль, автоматически заменяло его на новый;
• уведомляло пользователя о новом пароле посредством SMS.
Интерес состоял в том, чтобы решить эту задачу максимально подручными средствами, не привлекая сторонних услуг и сервисов. Ну вот не было никакого желания выбирать тарифы и пакеты. Зато был свободный GSM-модем. И всемогущий PowerShell.
В результате получился скрипт, а точнее — два скрипта. Почему так, объясняется просто – так сложилось исторически. Дело в том, что проверку паролей производит скрипт на виртуальной машине, расположенной в одном филиале, а рассылкой уведомлений по SMS занимается другая машина, расположенная в противоположной части страны. Из-за условий мобильного оператора иначе делать было нерентабельно.
Далее привожу оба скрипта целиком, которые я максимально прокомментировал. Выглядят они немного кучеряво. У меня не было особой потребности их причёсывать, поскольку работают они хорошо и в таком виде:
Этот скрипт добавим в Планировщик Заданий Windows, настроив его на выполнение в нужное нам время. Например, ночью.
К сожалению, скрипт проверяет просроченные пароли в момент своего выполнения. Так что если срок действия пароля истекает днём, то он его не будет учитывать. Но ведь нам это и не требуется, ибо в рабочее время сотрудник может поменять пароль самостоятельно.
В результате мы получаем список мобильных номеров пользователей, которым установлен новый пароль. Этот список мы отправим на сервер, к которому подключен GSM-модем. А там этим списком займется уже следующий скрипт.
Скрипты проверены в боевых условиях и показали себя с наилучшей стороны.
Я не буду объяснять, почему сделал именно так, поскольку задача была достаточно конкретна. И решение получилось вполне конкретное.
Но я буду рад любым советам по улучшению или оптимизации скриптов.
UPD:
Вылезла интересная ошибка.
Импорт паролей из текстового файла основан на функции Import-Csv, которая справедливо считает запятую разделителем полей.
А генератор паролей вполне активно использует запятую в качестве одного из спецсимволов.
В итоге пароль устанавливался нормальной длины, как и предполагалось, а вот в SMS пользователю приходил «обрезанный» пароль.
Решение незатейливое: раз нельзя использовать запятую, будем использовать звёздочку (её больше любят, чем знаки препинания)
Добавляем строчку сразу после генерации пароля:
Досадная мелочь, непродуманная заранее (mea culpa), а доверие пользователей подкосило здорово.
Вероятно, в большинстве случаев именно с этой фразы начинается творчество системных администраторов. В результате мы видим (хотя, правильнее сказать, даже и не замечаем) появление множества маленьких программ, которые выполняют свои точные и строго определённые задачи в одной большой системе.
Случилась (да и регулярно случается) со мной подобная история. Не скажу, что я изобрёл что-то новое и выдающееся. Скорее наоборот – воспользовался трудами коллег, найденными в просторах интернета и в кладезях премудрости Хабра. Но мне удалось объединить их для решения вполне конкретной и достаточно интересной задачи. Далее я опишу конкретное решение конкретной задачи по управлению паролями пользователей в 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 = "ITHelpDesk@domain.local"
$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@domain.local"
$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.domain.local\C$\ActiveDirectory\SMS_notice.txt
# Удаляем файл со списком новых паролей
Remove-Item $SMSfile
}
# отсылаем копию отчёта администратору
Write-log "SENDING REPORT TO IT DEPARTMENT"
EmailStructureReport("ITHelpdesk@domain.local")
$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
}
Скрипты проверены в боевых условиях и показали себя с наилучшей стороны.
Я не буду объяснять, почему сделал именно так, поскольку задача была достаточно конкретна. И решение получилось вполне конкретное.
Но я буду рад любым советам по улучшению или оптимизации скриптов.
UPD:
Вылезла интересная ошибка.
Импорт паролей из текстового файла основан на функции Import-Csv, которая справедливо считает запятую разделителем полей.
А генератор паролей вполне активно использует запятую в качестве одного из спецсимволов.
В итоге пароль устанавливался нормальной длины, как и предполагалось, а вот в SMS пользователю приходил «обрезанный» пароль.
Решение незатейливое: раз нельзя использовать запятую, будем использовать звёздочку (её больше любят, чем знаки препинания)
Добавляем строчку сразу после генерации пароля:
$generated_password = $generated_password_comma -replace ",","*"
Досадная мелочь, непродуманная заранее (mea culpa), а доверие пользователей подкосило здорово.