Windows Server для самых маленьких. Часть 13: Управляем Active Directory из PowerShell

Все части серии «Windows Server для самых маленьких»

Модулем ActiveDirectory мы пользуемся с части 4, и до сих пор он был просто набором команд, которые делают то, что написано в их имени. Пора разобрать его как инструмент: через что он говорит с контроллером, как устроены его фильтры, почему -Properties * — плохая привычка, и что делать с теми объектами каталога, для которых отдельного командлета нет. Эта часть — справочник, к которому стоит возвращаться; большинство примеров из следующих частей будут опираться на неё.


ADWS

Модуль не говорит по LDAP. Он обращается к Active Directory Web Services — службе ADWS на контроллере, порт TCP 9389, которая принимает запросы по протоколам WS-Transfer и WS-Enumeration (SOAP поверх транспорта net.tcp из .NET) и уже сама читает и пишет каталог. Служба появилась в Windows Server 2008 R2 вместе с модулем, и на контроллеры старше её ставили отдельно под именем Active Directory Management Gateway Service.

Из этого три следствия. -Server у командлетов модуля должен указывать на контроллер с работающей ADWS; если службы там нет или она остановлена, ответ будет «Unable to contact the server. This may be because this server does not exist, it is currently down, or it does not have the Active Directory Web Services running.», а если -Server не указан и DC Locator не нашёл ни одного контроллера с ADWS — «Unable to find a default server with Active Directory Web Services running». Модуль не умеет работать с LDAP-серверами, которые не Active Directory (OpenLDAP, 389 Directory Server): для них — классы System.DirectoryServices из .NET; экземпляры AD LDS он обслуживает, у них своя ADWS. И запросы модуля не видны в LDAP-захвате: смотреть их нужно либо на порту 9389, где они зашифрованы, либо по событию 1644 на контроллере, как в части 5, — ADWS внутри контроллера всё равно выполняет обычный LDAP-поиск.

PS C:\> Get-Service ADWS | Select-Object Status, StartType      # на dc1

 Status StartType
 ------ ---------
Running Automatic

PS C:\> (Get-Command -Module ActiveDirectory).Count
151

PS C:\> Get-Command -Module ActiveDirectory -Noun ADObject, ADServiceAccount, ADAccount, ADUser, ADGroupMember | Group-Object Noun | Sort-Object Count -Descending | Select-Object Count, Name, @{n='Verbs'; e={$_.Group.Verb -join ','}}

Count Name             Verbs
----- ----             -----
    8 ADObject         Get,Move,New,Remove,Rename,Restore,Set,Sync
    7 ADServiceAccount Get,Install,New,Remove,Set,Test,Uninstall
    4 ADAccount        Disable,Enable,Search,Unlock
    4 ADUser           Get,New,Remove,Set
    3 ADGroupMember    Add,Get,Remove

Get-Service здесь выполнен на самом контроллере: в PowerShell 7 у него нет параметра -ComputerName, а как выполнять команды на другом сервере — тема следующей части. Командлетов в модуле Windows Server 2025 — 151, и число растёт от версии к версии: у старых систем его меньше.

Модуль ставится вместе с инструментами роли (Install-WindowsFeature RSAT-AD-PowerShell на сервере) или с RSAT на рабочей станции, о чём в части 15. В PowerShell 7 он начиная с Windows Server 2019 и Windows 10 1809 загружается нативно — в его манифесте CompatiblePSEditions перечислены и Desktop, и Core. На более старых системах PowerShell 7 подгружает его через слой совместимости с Windows PowerShell, и объекты приходят десериализованными; включить этот режим явно можно через Import-Module ActiveDirectory -UseWindowsPowerShell.


Identity

У каждого Get-, Set-, Remove- есть параметр -Identity, первый позиционный, и он принимает любое из четырёх имён из части 3:

Get-ADUser alice                                                          # sAMAccountName
Get-ADUser 'CN=Alice Smith,OU=Sales,OU=Departments,DC=corp,DC=example,DC=com'   # DN
Get-ADUser 9c3f1a2b-7d4e-4f60-8a1b-2c3d4e5f6a7b                           # objectGUID
Get-ADUser S-1-5-21-3623811015-3361044348-30300820-1104                   # objectSid

UPN в -Identity не принимается: Get-ADUser alice@corp.example.com ответит «Cannot find an object with identity: 'alice@corp.example.com' under: 'DC=corp,DC=example,DC=com'.», а Get-ADUser -Filter 'UserPrincipalName -eq "alice@corp.example.com"' найдёт пользователя. У компьютеров -Identity принимает имя и без знака доллара, и с ним: Get-ADComputer ws1 и Get-ADComputer 'ws1$' найдут одну и ту же учётную запись. И -Identity принимает объект по конвейеру, поэтому Get-ADUser alice | Set-ADUser -Title 'Engineer' и Get-ADGroupMember Sales | Disable-ADAccount работают без промежуточных переменных.


Фильтры

-Filter — главный способ найти несколько объектов. Его язык похож на PowerShell, но это не PowerShell: строка разбирается модулем и переводится в LDAP-фильтр перед отправкой.

Get-ADUser -Filter 'Enabled -eq $true -and Department -eq "Sales"'
Get-ADComputer -Filter 'OperatingSystem -like "*Server*" -and Enabled -eq $true'
Get-ADUser -Filter 'PasswordNeverExpires -eq $true'
Get-ADUser -Filter 'LastLogonDate -lt $cutoff' -Properties LastLogonDate
Get-ADGroup -Filter 'GroupScope -eq "Universal"'
Get-ADUser -Filter *

Операторы: -eq, -ne, -like, -notlike (с *, других подстановочных знаков нет), -gt, -ge, -lt, -le, -approx, -and, -or, -not, и ещё -bor, -band для битовых полей, -recursivematch для вложенного членства. Регулярных выражений нет: -Filter 'Name -match "smith"' ответит «Error parsing query: 'Name -match "smith"' Error Message: 'Operator Not supported: -match' at position: '6'.». Три правила, на которых спотыкаются:

Переменные модуль ищет сам, но не все. Фильтр в одинарных кавычках с $cutoff внутри работает, потому что модуль сам ищет переменные в области вызова; свойство объекта самого модуля — $user.SamAccountName у результата Get-ADUser — тоже. А свойство строки из Import-Csv или хеш-таблицы уже нет: -Filter 'SamAccountName -eq $_.Login' в цикле по CSV ответит «Property: 'Login' not found in object of type: 'System.Management.Automation.PSCustomObject'.», а индексы ($list[0]) и выражения ($((Get-Date).AddDays(-90))) не разбираются вовсе. Надёжнее собирать строку: -Filter "Name -like '$prefix*'", или сначала положить значение в простую переменную.

Имена свойств — те, что показывает модуль, не всегда LDAP. Enabled и PasswordNeverExpires вычисляются из userAccountControl, и модуль переводит их в битовые LDAP-правила. Но не каждое вычисленное свойство годится для фильтра: -Filter 'LockedOut -eq $true' ответит «Searching on extended attribute 'LockedOut' is not supported with this cmdlet. Use Search-ADAccount instead.», так же откажет CannotChangePassword. LastLogonDate — это lastLogonTimestamp, переведённый в DateTime; PasswordLastSet — pwdLastSet; Created и Modified — whenCreated и whenChanged. Любой LDAP-атрибут тоже можно писать напрямую: -Filter 'pwdLastSet -eq 0'.

Сложное — в -LDAPFilter. Поиск по дате в FILETIME, цепочка членства, ANR, сравнение битов без именованного свойства — всё это проще написать LDAP-фильтром из части 5, чем подбирать синтаксис модуля:

Get-ADUser -LDAPFilter '(&(objectCategory=person)(memberOf:1.2.840.113556.1.4.1941:=CN=Domain Admins,CN=Users,DC=corp,DC=example,DC=com))'
Get-ADObject -LDAPFilter '(anr=ali)'

Область поиска: -SearchBase (DN, с которого начинать; без него — корень домена) и -SearchScope (Base, OneLevel, Subtree). Постраничный вывод включён всегда, страница — -ResultPageSize, 256 по умолчанию; -ResultSetSize ограничивает общее число результатов, когда нужны «первые десять». ADWS держит открытый перечислитель не дольше 30 минут: очень долгий конвейер, который медленно обрабатывает каждый объект, может оборваться на середине, и тогда результаты сначала собирают в переменную.


Properties

Командлет возвращает не все атрибуты, а небольшой набор по умолчанию — у Get-ADUser десять: DistinguishedName, Enabled, GivenName, Name, ObjectClass, ObjectGUID, SamAccountName, SID, Surname, UserPrincipalName. Всё остальное нужно запрашивать:

PS C:\> Get-ADUser alice | Get-Member -MemberType Property | Measure-Object | Select-Object -ExpandProperty Count
10
PS C:\> (Get-ADUser alice -Properties *).PropertyCount
106
PS C:\> Get-ADUser alice -Properties Mail, Department, PasswordLastSet, LastLogonDate, MemberOf | Select-Object Mail, Department, PasswordLastSet, LastLogonDate

Mail              Department PasswordLastSet    LastLogonDate
----              ---------- ---------------    -------------
alice@example.com Sales      15.10.2026 9:30:11 15.10.2026 9:02:11

-Properties * на одном объекте безвреден, на десяти тысячах — это десять тысяч объектов по сотне атрибутов, включая фотографии и сертификаты, через SOAP. Правило: перечислять то, что нужно. Разница измерима и растёт вместе с доменом; на нескольких тысячах учётных записей она выглядит так:

PS C:\> (Measure-Command { Get-ADUser -Filter * -Properties * }).TotalSeconds
14.8
PS C:\> (Measure-Command { Get-ADUser -Filter * -Properties Mail, Department }).TotalSeconds
1.9

Server

Без -Server модуль берёт контроллер у DC Locator: ближайший в сайте с ADWS, и для сеанса запоминает его. С -Server можно указать контроллер (dc2, dc2.corp.example.com), домен (corp.example.com — выбор снова за Locator, но в этом домене) или глобальный каталог (corp.example.com:3268 — поиск по всему лесу с частичным набором атрибутов).

Когда это важно. Чтение после записи: объект, созданный на dc1, на dc2 появится через 15 секунд, и скрипт, который создаёт пользователя одной командой и добавляет его в группу другой, может попасть на разные контроллеры и получить «объект не найден». В скриптах, которые пишут, контроллер фиксируют — чаще всего PDC-эмулятор:

$dc = (Get-ADDomain).PDCEmulator
New-ADUser -Server $dc ...
Add-ADGroupMember -Server $dc ...

Нереплицируемые атрибуты: lastLogon, badPwdCount нужно спрашивать у каждого контроллера, как в части 5. Блокировки: Search-ADAccount -LockedOut надёжнее спрашивать у PDC, которому блокировки реплицируются срочно. И другой домен или лес: -Server partner.example.net -Credential (Get-Credential).


Создание и правка

PS C:\> $params = @{
    Name              = 'Carol White'
    GivenName         = 'Carol'
    Surname           = 'White'
    SamAccountName    = 'carol'
    UserPrincipalName = 'carol@corp.example.com'
    Path              = 'OU=Sales,OU=Departments,DC=corp,DC=example,DC=com'
    Department        = 'Sales'
    AccountPassword   = (Read-Host -AsSecureString 'Password')
    ChangePasswordAtLogon = $true
    Enabled           = $true
    OtherAttributes   = @{ extensionAttribute1 = 'EMP-1042' }
}
PS C:\> New-ADUser @params -PassThru | Select-Object DistinguishedName, Enabled

DistinguishedName                                                Enabled
-----------------                                                -------
CN=Carol White,OU=Sales,OU=Departments,DC=corp,DC=example,DC=com    True

Хеш-таблица с @params (splatting) — стандартный способ передать два десятка параметров без строки на три экрана. -OtherAttributes принимает любые LDAP-атрибуты, для которых нет именованного параметра. Без -AccountPassword учётная запись создаётся отключённой, а с -Enabled $true команда завершится ошибкой «The password does not meet the length, complexity, or history requirement of the domain.» и учётную запись не создаст: пустой пароль нарушает политику.

Массовое создание — из CSV по конвейеру:

Import-Csv .\users.csv -Encoding UTF8 | ForEach-Object {
    New-ADUser -Server $dc -Name "$($_.First) $($_.Last)" -GivenName $_.First -Surname $_.Last -SamAccountName $_.Login `
        -UserPrincipalName "$($_.Login)@corp.example.com" -Path $_.OU -Department $_.Dept `
        -AccountPassword (ConvertTo-SecureString $_.Password -AsPlainText -Force) -ChangePasswordAtLogon $true -Enabled $true
}

Правка — Set-ADUser с именованными параметрами для частых атрибутов (-Title, -Department, -Manager, -EmailAddress, -Enabled, -PasswordNeverExpires) и четыре параметра для остальных, те же, что у Set-ADObject в части 5: -Replace, -Add, -Remove, -Clear:

Set-ADUser carol -Title 'Account Manager' -Manager alice
Set-ADUser carol -Replace @{ extensionAttribute1 = 'EMP-1043'; info = 'Перевод 2026-10' }
Set-ADUser carol -Add @{ otherTelephone = '+1 555 0102' }
Set-ADUser carol -Clear info
Get-ADUser -Filter 'Department -eq "Sales"' | Set-ADUser -Office 'Building A, floor 3'

Перенос и переименование — Move-ADObject -TargetPath и Rename-ADObject -NewName (меняет CN; имя входа — Set-ADUser -SamAccountName). Удаление — Remove-ADUser с подтверждением. Защищённые от случайного удаления объекты сначала Set-ADObject -ProtectedFromAccidentalDeletion $false, иначе удаление ответит «Access is denied.»; New-ADOrganizationalUnit защищает каждую OU, поэтому перед Remove-ADOrganizationalUnit -Recursive защиту снимают и со всех вложенных OU: Get-ADOrganizationalUnit -SearchBase $ou -Filter * | Set-ADObject -ProtectedFromAccidentalDeletion $false.


Группы и пароли

Add-ADGroupMember -Identity Sales -Members carol, bob
Add-ADGroupMember -Identity Sales -Members (Get-ADUser -Filter 'Department -eq "Sales"')
Remove-ADGroupMember -Identity Sales -Members bob -Confirm:$false
Get-ADGroupMember -Identity Sales -Recursive | Select-Object Name, ObjectClass
Get-ADPrincipalGroupMembership carol | Select-Object Name, GroupScope

-Members принимает список: добавить сто пользователей одним вызовом быстрее, чем сто вызовов. Список только должен быть однородным: имена вперемешку с результатом Get-ADUser в одном вызове (-Members carol, bob, (Get-ADUser …)) дают «Cannot convert 'System.Object[]' to the type 'Microsoft.ActiveDirectory.Management.ADPrincipal' required by parameter 'Members'.». Get-ADGroupMember -Recursive раскрывает вложенность, но у ADWS есть предел на размер ответа (5000 членов по умолчанию, MaxGroupOrMemberEntries в конфигурации службы; тот же предел у Get-ADPrincipalGroupMembership); у групп крупнее читают атрибут member напрямую: (Get-ADGroup Sales -Properties member).member. Для «в каких группах состоит, включая вложенные» — tokenGroups из части 5 или LDAP-фильтр с цепочкой.

Пароли и состояния:

Set-ADAccountPassword carol -Reset -NewPassword (Read-Host -AsSecureString)   # сбросить, не зная старого
Set-ADAccountPassword carol -OldPassword $old -NewPassword $new                 # сменить, зная старый: право самого пользователя
Set-ADUser carol -ChangePasswordAtLogon $true
Unlock-ADAccount carol
Disable-ADAccount carol; Enable-ADAccount carol
Search-ADAccount -LockedOut | Select-Object Name, LockedOut, LastLogonDate
Search-ADAccount -AccountInactive -TimeSpan 90.00:00:00 -UsersOnly | Select-Object Name, LastLogonDate
Search-ADAccount -PasswordExpired
Search-ADAccount -AccountExpiring -TimeSpan 7.00:00:00

Смена с -OldPassword подчиняется политике паролей целиком: раньше минимального срока пароля сервер её отклонит той же ошибкой, что и слабый пароль, — «The password does not meet the length, complexity, or history requirement of the domain.».

Search-ADAccount — командлет для вопросов «кто заблокирован, кто не входил три месяца, у кого истёк пароль», и он покрывает то, что иначе пришлось бы писать фильтрами по FILETIME; -AccountInactive считает неактивными и тех, кто не входил ни разу, — у них LastLogonDate пуст. Политика паролей домена — Get-ADDefaultDomainPasswordPolicy; гранулярные политики для отдельных групп (строже для администраторов, мягче для служебных учётных записей) — New-ADFineGrainedPasswordPolicy с приоритетом и Add-ADFineGrainedPasswordPolicySubject; что в итоге действует на пользователя — Get-ADUserResultantPasswordPolicy carol. Если ни одна гранулярная политика на пользователя не действует, эта команда ничего не выводит: действует политика домена.


Диск AD:

Модуль добавляет провайдер, и каталог становится файловой системой:

PS C:\> Set-Location 'AD:\DC=corp,DC=example,DC=com'
PS AD:\DC=corp,DC=example,DC=com> Get-ChildItem | Select-Object Name, ObjectClass

Name               ObjectClass
----               -----------
Builtin            builtinDomain
Computers          container
Departments        organizationalUnit
Domain Controllers organizationalUnit
...

PS AD:\DC=corp,DC=example,DC=com> Get-ItemProperty 'OU=Sales,OU=Departments' -Name description, whenCreated

Пути — DN; корень диска, Set-Location AD:\, показывает разделы: домен, Configuration, Schema, ForestDnsZones и DomainDnsZones. Главная польза провайдера — **права**: Get-Acl и Set-Acl работают с дескрипторами безопасности объектов каталога, и через них делается делегирование. Делегировать группе Branch Admins сброс паролей в OU филиала:

PS C:\> $ou  = 'AD:\OU=Branch,OU=Departments,DC=corp,DC=example,DC=com'
PS C:\> $acl = Get-Acl $ou
PS C:\> $sid = (Get-ADGroup 'Branch Admins').SID
PS C:\> $resetPassword = [Guid]'00299570-246d-11d0-a768-00aa006e0529'      # расширенное право Reset Password
PS C:\> $userClass     = [Guid]'bf967aba-0de6-11d0-a285-00aa003049e2'      # класс user
PS C:\> $acl.AddAccessRule((New-Object System.DirectoryServices.ActiveDirectoryAccessRule($sid, 'ExtendedRight', 'Allow', $resetPassword, 'Descendents', $userClass)))
PS C:\> Set-Acl $ou -AclObject $acl
PS C:\> (Get-Acl $ou).Access | Where-Object IdentityReference -like '*Branch Admins*' | Select-Object ActiveDirectoryRights, ObjectType, InheritanceType

ActiveDirectoryRights ObjectType                           InheritanceType
--------------------- ----------                           ---------------
        ExtendedRight 00299570-246d-11d0-a768-00aa006e0529     Descendents

GUID расширенных прав лежат в CN=Extended-Rights,CN=Configuration (атрибут rightsGuid), GUID классов и атрибутов — в схеме (schemaIDGUID). Это та же операция, что мастер делегирования в оснастке, и тот же результат, что dsacls "OU=Branch,…" /I:S /G "CORP\Branch Admins:CA;Reset Password;user", только воспроизводимый.


Get-ADObject

Для пользователей, компьютеров, групп, OU, контроллеров, сайтов, подсетей, связей сайтов, доверий, политик паролей и управляемых учётных записей у модуля есть свои командлеты. Для всего остального — Get-ADObject, New-ADObject, Set-ADObject, Remove-ADObject, которые работают с любым классом по DN и фильтру:

Get-ADObject -SearchBase 'CN=Policies,CN=System,DC=corp,DC=example,DC=com' -Filter 'objectClass -eq "groupPolicyContainer"' -Properties displayName, versionNumber
Get-ADObject -SearchBase (Get-ADRootDSE).configurationNamingContext -LDAPFilter '(objectClass=nTDSDSA)' -Properties options     # объекты NTDS Settings всех контроллеров
Get-ADObject -SearchBase (Get-ADRootDSE).schemaNamingContext -Filter 'lDAPDisplayName -eq "mail"' -Properties *
New-ADObject -Name 'Printers' -Type container -Path 'OU=Branch,OU=Departments,DC=corp,DC=example,DC=com'
Get-ADObject -Filter 'isDeleted -eq $true -and Name -like "Bob*"' -IncludeDeletedObjects | Restore-ADObject

Последняя строка — восстановление из корзины AD из части 5: удалённый объект возвращается на прежнее место со всеми атрибутами и членством. Restore-ADObject -TargetPath кладёт его в другой контейнер, если прежнего уже нет.


Управляемые учётные записи служб

В части 6 SQL Server работал под svc-sql с паролем, который кто-то знает и который никогда не меняется. Модуль умеет лучше: групповые управляемые учётные записи (gMSA, Windows Server 2012): пароль длиной 240 байт генерирует и меняет раз в 30 дней сам контроллер, а получают его только те компьютеры, которым разрешено. Человек пароль не знает никогда.

PS C:\> Add-KdsRootKey -EffectiveImmediately                   # один раз на лес; корневой ключ службы распространения ключей (модуль Kds)
PS C:\> New-ADServiceAccount -Name svc-web -DNSHostName svc-web.corp.example.com -PrincipalsAllowedToRetrieveManagedPassword web1$ -ServicePrincipalNames 'HTTP/web.corp.example.com'
PS C:\> Install-ADServiceAccount svc-web      # на web1
PS C:\> Test-ADServiceAccount svc-web         # на web1
True

-EffectiveImmediately делает ключ действующим сразу только на том контроллере, где он создан: остальные получат его с репликацией, и контроллеры выжидают до 10 часов от создания ключа, прежде чем работать с gMSA, — запас на эту репликацию. В лаборатории с одним контроллером дату ставят в прошлое: Add-KdsRootKey -EffectiveTime ((Get-Date).AddHours(-10)). После этого службу, пул IIS или задачу планировщика запускают под CORP\svc-web$ с пустым паролем, и web1 получает актуальный пароль у контроллера сам. SPN на gMSA регистрируется так же, как на обычной учётной записи, и всё из части про Kerberos работает без изменений.


Как не положить контроллер

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

  • Фильтровать на сервере, а не в PowerShell. Get-ADUser -Filter * | Where-Object Department -eq Sales вытягивает весь домен, чтобы оставить сорок человек; -Filter 'Department -eq "Sales"' отдаёт сорок. Индексированные атрибуты (sAMAccountName, objectGUID, objectSid, displayName, mail, userPrincipalName, всё с битом 1 в searchFlags) работают быстро; поиск с подстрокой в начале (-like "*smith") по неиндексированному атрибуту перебирает все объекты, и событие 1644 из части 5 это покажет.
  • -Properties по списку, не *.
  • Пишущие операции — на одном контроллере, с -Server.
  • -WhatIf перед массовым Set- или Remove-: Get-ADUser -Filter 'Department -eq "Sales"' | Remove-ADUser -WhatIf напечатает, что было бы удалено, и это единственный способ узнать, что в фильтре опечатка, до того, как она сработает.
  • Запуск под обычной учётной записью с -Credential для привилегированных команд, а не под администратором домена целый день: пароль спрашивается один раз в $cred = Get-Credential, и дальше -Credential $cred там, где нужны права.

Смотрим руками

Что модуль отправляет и куда:

PS C:\> Get-ADUser carol | Out-Null
PS C:\> Get-NetTCPConnection -RemotePort 9389 | Select-Object LocalAddress, RemoteAddress, State, OwningProcess

LocalAddress  RemoteAddress       State OwningProcess
------------  -------------       ----- -------------
192.168.1.100 192.168.1.10  Established          5124
192.168.1.100 192.168.1.10  Established          5124
192.168.1.100 192.168.1.10  Established          5124
192.168.1.100 192.168.1.10  Established          5124

Четыре соединения с dc1 на 9389 от процесса PowerShell: модуль открывает их при первой команде и переиспользует — после следующих Get-ADUser в том же сеансе остаются те же четыре, с теми же локальными портами. В Wireshark это .NET Message Framing поверх TCP с согласованием через Negotiate — внутри SOAP, и прочитать его в захвате не получится. Зато на контроллере с включённым событием 1644 виден LDAP-запрос, в который превратился фильтр:

Filter:
     ( &  (objectCategory=CN=Person,CN=Schema,CN=Configuration,DC=corp,DC=example,DC=com)  (objectClass=user)  (department=Sales) )
Search scope:
    subtree
Attribute selection:
    distinguishedName,name,objectClass,objectGUID,objectSid,sAMAccountName,userPrincipalName,givenName,sn,userAccountControl,department

Модуль добавил objectCategory=person и objectClass=user сам — именно поэтому Get-ADUser не возвращает компьютеры, хотя класс computer наследует user. Отсюда же видно, что -Properties Department стал одним атрибутом в списке запроса, а не всеми.

Что возвращается:

PS C:\> Get-ADUser carol | Get-Member | Select-Object -First 4 TypeName, Name, MemberType

TypeName                                    Name          MemberType
--------                                    ----          ----------
Microsoft.ActiveDirectory.Management.ADUser Contains          Method
Microsoft.ActiveDirectory.Management.ADUser Equals            Method
Microsoft.ActiveDirectory.Management.ADUser GetEnumerator     Method
Microsoft.ActiveDirectory.Management.ADUser GetHashCode       Method

ADUser — объект со словарём атрибутов внутри: $u.Department возвращает значение атрибута, $u['department'] — то же значение в коллекции ADPropertyValueCollection (у многозначных атрибутов в ней и лежит весь список), а $u.PropertyNames перечисляет всё, что было запрошено. ADUser и ADComputer наследуют ADAccount, тот вместе с ADGroup — ADPrincipal, а все они и ADOrganizationalUnit — общий ADObject; поэтому любой из них можно передать в Set-ADObject или Move-ADObject.

И последнее — пример скрипта, который объединяет сказанное: отчёт о неактивных пользователях с последним входом по всем контроллерам, без -Properties *, с фильтром на сервере:

$dcs = (Get-ADDomainController -Filter *).HostName
$cutoff = (Get-Date).AddDays(-90)
Get-ADUser -Filter 'Enabled -eq $true -and LastLogonDate -lt $cutoff' -Properties LastLogonDate, Department |
    ForEach-Object {
        $u = $_
        $last = ($dcs | ForEach-Object { (Get-ADUser $u -Server $_ -Properties lastLogon).lastLogon } | Measure-Object -Maximum).Maximum
        [PSCustomObject]@{ Name = $u.Name; Department = $u.Department; LastLogon = [DateTime]::FromFileTime($last) }
    } | Sort-Object LastLogon | Export-Csv .\inactive.csv -NoTypeInformation -Encoding UTF8

Первый фильтр отсекает по реплицируемому LastLogonDate с точностью до двух недель, и только для оставшихся скрипт опрашивает каждый контроллер за точным lastLogon. Учётные записи, которые не входили ни разу, в отчёт не попадут: атрибута lastLogonTimestamp у них нет, а сравнение с отсутствующим атрибутом в LDAP ложно. Их ищут отдельно — Get-ADUser -Filter 'LastLogonDate -notlike "*"' — или берут Search-ADAccount -AccountInactive, который их включает.


Источники


Всё в этой части мы выполняли с рабочей станции, а модуль сам находил контроллер и знал, как до него добраться. У остальных модулей — DnsServer, DhcpServer, Storage, NetTCPIP — встроенного транспорта нет: их -ComputerName и -CimSession ходят через WinRM, а обычный Get-Process или Restart-Service на другом сервере без него не выполнить вовсе.

Дальше — часть 14: WinRM и PowerShell Remoting. Что такое WS-Management и как устроен слушатель; Enable-PSRemoting и что он на самом деле меняет; Enter-PSSession и Invoke-Command, сеансы и параллельное выполнение на десятках серверов; аутентификация Kerberos и почему вне домена нужен TrustedHosts; HTTPS-слушатель с сертификатом; проблема второго прыжка и её решения; и JEA — конечные точки, в которых можно выполнять только разрешённые команды.

Оставить комментарий могут только зарегистрированные пользователи.

Войдите на сайт или зарегистрируйтесь, чтобы оставить комментарий.