Часто мы ищем готовые решения, качаем софт, просим доступы — а инструмент уже лежит под рукой. У меня была рутинная задача: проверять учетки пользователей в AD. Когда менялся пароль, есть ли блокировка, не истек ли срок действия. Каждый раз — открыть ADUC, найти учетку, прокликать вкладки. Минута-две на запрос, десять запросов в день — и вот уже часы уходят в никуда.

В какой-то момент я подумал: но ведь есть PowerShell. И написал скрипт, который помогает за секунду получить состояние учетки, дату смены пароля, блокировки, контакты, подразделение. Можно сразу снять временную блокировку. Вдруг и вам пригодится.

Что получится в итоге

Скрипт, который по логину выводит 30+ атрибутов пользователя из AD. Если у пользователя временная блокировка — скрипт предложит ее снять. Один инструмент вместо десятка кликов по консолям.

Смотрим, что есть в AD

Прежде чем писать скрипт, нужно понять, какие данные доступны. Открываем PowerShell ISE (он есть в Windows из коробки) и вытаскиваем все атрибуты своей учетки:

Get-ADUser -Identity Ваш_логин -Properties * | Select-Object -Property * | ForEach-Object { ($_ -split ',') } | Out-File -FilePath "c:\$login.txt"

В файле о��ажется масса информации: имя, дата создания учетки, последняя смена пароля, блокировки и десятки других полей. Чтобы понять, как работать с каждым атрибутом, нужны их свойства:

Get-ADUser -Identity Ваш_логин -Properties * | Get-Member | ForEach-Object { ($_ -split ',') } | Out-File -FilePath "c:\Properties.txt"

Например, строка System.DateTime AccountLockoutTime {get;set;} говорит, что AccountLockoutTime — это дата и время. Сам атрибут хранит момент временной блокировки учетной записи (той, которую вы запросили). Зная типы данных, можно правильно их обрабатывать и форматировать.

Каркас 

Начнем с основы. PowerShell, в отличие от того же Bash, поддерживает обработку исключений — грех этим не воспользоваться:

try {
    $login=Read-Host "Enter login"
    if (-not $login) {
        Write-Host "Пользователь не найден!"
    } else { GetAdUserInfo $login }
} catch { Write-Warning "Что-то пошло не так: $($_.Exception.Message)" }

Функцию GetAdUserInfo я назвал по правилам хорошего тона — с глагола Get. Теперь напишем ее.

Простая версия 

Для начала — базовый вариант, чтобы понять принцип:

function GetAdUserInfo {
    Param (
        [string]$login
    )

    try {    
    $user=Get-AdUser $login -Properties * 
    $user | Select-Object @{Name="Login";expression={$_.SamAccountName}},`
            @{Name="Отображаемое имя (ФИО)";expression={$_.DisplayName -replace "`n"," "}},`
            @{Name="Фамилия";expression={$_.Surname -replace "`n"," "}},`
            @{Name="Временная блокировка";expression={if ($_.AccountLockoutTime) { `
            Write-Output "Временная блокировка с "; $locktime=$_.AccountLockoutTime; Write-Output $locktime.ToString()  } else {`
            Write-Output "Отсутствует" }}}#, `
        #   @{ Другие значения }, ` бэктик нужен для переноса на новую строку, лучше чем писать в горизонт за широту монитора :) 
        #   @{ Другие значения } 
    } catch { Write-Warning "Что-то пошло не так в функции GetAdUserInfo: $($_.Exception.Message)" }
}

Здесь мы получаем все атрибуты пользователя и через хеш-таблицу формируем читаемый вывод. Со значениями можно работать сразу, например высчитать, сколько дней пользователю до смены пароля или когда он его сменил.

Если команда Get-ADUser не работает, добавьте в начало скрипта: Import-Module ActiveDirectory.

Полная версия

Универсальный скрипт для всех написать не получится — атрибуты и их свойства в ваших доменах могут отличаться. Так что открывайте ISE и построчно делайте для себя рабочий скрипт.

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

Вот что мне удалось вытянуть на своем рабочем месте:

Import-Module ActiveDirectory

function GetAdUserInfo {
    Param (
        [Parameter(Mandatory=$true)]
        [string]$login,
        $global:locktime=$null
    )

    try {    
    [int64]$nullDate=9223372036854775807
    $dftPwdPolicy=Get-ADDefaultDomainPasswordPolicy -ErrorAction Stop
    $dftPwdChangePeriod=$dftPwdPolicy.MaxPasswordAge.Days 
    $today=Get-Date

    $u=Get-AdUser $login -Properties *
    $u | select @{Name="Login";expression={$_.SamAccountName}},`
                @{Name="Отображаемое имя (ФИО)";expression={$_.DisplayName -replace "`n"," "}},`
                @{Name="Фамилия";expression={$_.Surname -replace "`n"," "}},`
                @{Name="Имя";expression={$_.GivenName  -replace "`n"," "}},`
                @{Name="Отчество";expression={$_.OtherName  -replace "`n"," "}},`
                @{Name="Состояние";expression={$expired=$false; if ($nullDate -ne [int64]$_.accountexpires -and 0 -ne [int64]$_.accountexpires){$expDate=[DateTime]::FromFileTime($_.accountexpires); if ($today -gt $expDate){$expired=$true}};if (!$_.Enabled){"отключена"} else {if ($expired) {"истек срок действия"} else {if ($_.PasswordLastSet -eq $null){"начальный пароль не менялся"} else { if ([datetime]($_.PasswordLastSet) -ge $today ){"просрочена смена пароля"}else {"OK"}}}}}},`
                @{Name="Пароль изменен (дней до смены пароля)";expression={`
                        if (!$_.PasswordNeverExpires) {`
                            if ($_.PasswordLastSet -ne $null){`
                                $pwdLastSet=[datetime]($_.PasswordLastSet);`

                                $maxPasswordAge = $dftPwdChangePeriod;`
                                
                                if ($_."msDS-ResultantPSO" -ne $null)`
                                {`
                                    $PasswordPol = $_ | Get-ADUserResultantPasswordPolicy -Server $Server; `    
                                    $maxPasswordAge = ($PasswordPol).MaxPasswordAge.Days`
                                }`

                                $diff=(New-TimeSpan -Start $today -End ($pwdLastSet).AddDays($maxPasswordAge)).Days; `
                                [string]$s=($pwdLastSet).ToString('dd.MM.yyyy HH:mm:ss'); `
                                $s+ " ($diff)"`
                            } else { "Не менялся" + " (0)" }`
                        } else {"Неистекаемый"}`
                    }`
                },`
                @{Name="Последний вход";expression={if ($null -ne $_.lastlogontimestamp ) {[DateTime]::FromFileTime($_.lastlogontimestamp).ToString('dd.MM.yyyy HH:mm:ss')}else {""}}},`
                @{Name="Номер телефона";expression={$_.telephoneNumber  -replace "`n"," "}},`
                @{Name="Идентификатор почты";expression={$_.extensionName[0]}},`
                @{Name="Внешний адрес";expression={$_.mail}},`
                @{Name="Идентификатор БОСС";expression={$_.EmployeeID}},`
                @{Name="Табельный номер";expression={$_.employeeNumber}},`
                @{Name="Дата рождения";expression={$_.ExtensionAttribute4}},`
                @{Name="Должность";expression={$_.title  -replace "`n"," "}},`
                @{Name="Подразделение";expression={$_.division  -replace "`n"," "}},`
                @{Name="Департамент";expression={$_.department  -replace "`n"," "}},`
                @{Name="Организация";expression={$_.company  -replace "`n"," "}},`
                @{Name="Срок действия";expression={if ($nullDate -ne [int64]$_.accountexpires  -and 0 -ne [int64]$_.accountexpires){[DateTime]::FromFileTime($_.accountexpires).ToString('dd.MM.yyyy HH:mm:ss')}else {""}}}, `
                @{Name="OrgUnit";expression={ if ($_.distinguishedName -match "^CN=.*?,(?<OU>(CN|OU)=.*)"){$Matches.OU} else {""} }}, `
                @{Name="SPN";expression={ if ($_.servicePrincipalName -ne $null){"задан"} else {""} }}, `
                @{Name="Дата создания";expression={([datetime]$_.whenCreated).ToString('dd.MM.yyyy HH:mm:ss')}}, `
                @{Name="Дата изменения";expression={([datetime]$_.whenChanged).ToString('dd.MM.yyyy HH:mm:ss')}}, `
                @{Name="Дата попытки входа с неверным паролем";expression={if ($nullDate -ne [int64]$_.badPasswordTime  -and 0 -ne [int64]$_.badPasswordTime ) {[DateTime]::FromFileTime($_.badPasswordTime).ToString('dd.MM.yyyy HH:mm:ss')}}}, `
                @{Name="Количество ошибок входа";expression={$_.badPwdCount}}, `
                @{Name="Количество входов";expression={$_.logonCount}}, `
                @{Name="Город";expression={$_.city}}, `
                @{Name="Почтовый адрес";expression={$_.postalAddress}}, `
                @{Name="Комната";expression={$_.roomNumber}}, `
                @{Name="IP телефон";expression={$_.ipPhone}}, `
                @{Name="Политика паролей (имя)";expression={if ($_."msDS-ResultantPSO" -eq $null){"Основная"}else{($_ | Get-ADUserResultantPasswordPolicy).name}}}, `
                @{Name="Временная блокировка";expression={if ($_.AccountLockoutTime) {Write-Output "блокировка с"; $global:locktime=$_.AccountLockoutTime; Write-Output $locktime.ToString() } else { Write-Output "Отсутствует" }}}
    } catch { Write-Warning "Что-то пошло не так в функции GetAdUserInfo: $($_.Exception.Message)" }
    
}


try {
    $login=Read-Host "Enter login"
    Write-Output "Результаты поиска атрибутов в АД у пользователя" 
    if (-not $login) {
        Write-Host "Пользователь не найден!"
    } else {
        GetAdUserInfo $login
        if ($global:locktime -ne $null) {
            $request = Read-Host "`nВведите y для снятия временной блокировки или любой символ для НЕТ"
                if ($request -eq 'y') {  Unlock-ADAccount -Identity $login
                                         Write-Output "Временная блокировка снята"
                } else { Write-Warning "`nУ пользователя блокировка осталась НЕ снятой $($_.Exception.Message)"}
        } 
     }
} catch { Write-Warning "Error: $($_.Exception.Message)" }
       

Read-Host "`nВведите любой символ или пробел и нажмите Enter для выхода"

Важно: у вас атрибуты могут называться по-другому или вообще отсутствовать!

Бонус

Раз уж мы делаем набор инструментов для AD, вот еще один полезный скрипт. Когда нужно найти группу, но помнишь только часть названия:

Import-Module ActiveDirectory
    
try {
    $group = Read-Host "Введите группу для поиска"
    $SearchGroups = Get-ADGroup -Filter "Name -like '*$group*'"
    if (-not $SearchGroups) {
            Write-Host "not search '$group' in AD"
    } else {
            $SearchGroups | Sort-Object | ForEach-Object { ($_ -split ',')[0] -replace '^CN=', '' } | ForEach-Object { Write-Host $_ }
        }

} catch {Write-Warning "Error: $($_.Exception.Message)" }

Результат приходит по конвейеру: массив → сортировка → разделили по запятой, заменив пустым символом ненужный префикс 'CN=' → построчно вывели.

Теперь то же самое с алиасами — заменим ForEach-Object на %и вывод в консоль на echo:

Import-Module ActiveDirectory
    
try {
    $group = Read-Host "Введите группу для поиска"
    $SearchGroups = Get-ADGroup -Filter "Name -like '*$group*'"
    if (-not $SearchGroups) {
            echo "not search '$group' in AD"
    } else {
            $SearchGroups | sort | % { ($_ -split ',')[0] -replace '^CN=', '' } | % { echo $_ }
        }

} catch {Write-Warning "Error: $($_.Exception.Message)" }

Работает так же, но стало лаконичнее. Посмотреть все доступные алиасы можно командой Get-Alias | Sort-Object Name.

Итого

Часто все необходимые инструменты уже под рукой. PowerShell — это мощная штука, которую многие игнорируют. А зря. Он есть на любой Windows-машине, умеет работать с AD, файлами, сетью, реестром и не требует ни установки, ни согласований. Просто открываете ISE и делаете, что нужно.

Если хотите углубиться в тему, вот полезные статьи на Хабре: