08 · Active Directory & System Admin Cmdlets¶
Manual review only — not executed
This machine has no Windows domain controller and no ActiveDirectory
module (it's Windows-only and requires RSAT or a domain-joined
server), so none of the code in this module could be run for real. It
has been reviewed carefully by hand against the documented behavior of
the ActiveDirectory module's cmdlets, and the shapes of the commands
and their parameters are accurate — but no output below comes from an
actual execution, and you should treat this module as reference
material to validate against a real AD environment before relying on
it in production. This is the one module in the course where that
caveat applies; every other module's code was executed for real.
Active Directory administration is one of PowerShell's original reasons for existing — most of what a Windows AD admin does interactively in Active Directory Users and Computers has a scriptable equivalent.
Prerequisites¶
Install-WindowsFeature -Name RSAT-AD-PowerShell # on a Windows Server
# or, on Windows client:
Add-WindowsCapability -Online -Name Rsat.ActiveDirectory.DS-LDS.Tools~~~~0.0.1.0
Import-Module ActiveDirectory
Get-Command -Module ActiveDirectory | Measure-Object # hundreds of cmdlets
Every cmdlet in the module implicitly targets the domain your session is
authenticated to unless you pass -Server — worth being deliberate about
in a multi-domain forest.
Querying users¶
Get-ADUser -Filter "Department -eq 'Engineering'" -Properties Title, Mail |
Select-Object Name, Title, Mail
Name Title Mail
---- ----- ----
Jane Doe Senior Engineer jane.doe@corp.example.com
Sam Patel Engineer sam.patel@corp.example.com
-Filter uses PowerShell-style syntax (not raw LDAP), which is why it
looks like a normal comparison expression. Note -Properties is required
to get anything beyond the small default attribute set — Get-ADUser
without it won't return Title or Mail even if you ask for them in
Select-Object; the property has to actually be fetched from AD first.
The trap: -Filter string syntax vs -LDAPFilter¶
# Works: PowerShell-style filter, evaluated by the cmdlet
Get-ADUser -Filter "SamAccountName -eq 'jdoe'"
# Also works, but a different dialect entirely - raw LDAP syntax
Get-ADUser -LDAPFilter "(sAMAccountName=jdoe)"
# Common mistake: mixing the two syntaxes in one string silently returns nothing or errors
Get-ADUser -Filter "(sAMAccountName=jdoe)" # wrong: LDAP syntax inside -Filter
-Filter and -LDAPFilter are two separate parameters with two separate
grammars — copying an LDAP query you found somewhere into -Filter
verbatim is one of the most common AD scripting mistakes, and it fails
quietly (empty results) rather than with an obvious syntax error in most
cases.
Creating and modifying users¶
$password = ConvertTo-SecureString "TempP@ss123!" -AsPlainText -Force
New-ADUser -Name "Alex Kim" `
-SamAccountName "akim" `
-UserPrincipalName "akim@corp.example.com" `
-Path "OU=Engineering,DC=corp,DC=example,DC=com" `
-AccountPassword $password `
-Enabled $true `
-ChangePasswordAtLogon $true
Set-ADAccountPassword -Identity "akim" -NewPassword $password -Reset
Unlock-ADAccount -Identity "akim"
-Enabled $true at creation time matters: New-ADUser creates disabled
accounts by default unless you either set this or provide a password that
satisfies policy — a very common "why can't this new user log in"
support ticket traces back to this exact default.
Groups and membership¶
Add-ADGroupMember -Identity "Engineering-Team" -Members "akim", "jdoe"
Get-ADGroupMember -Identity "Engineering-Team" | Select-Object Name, SamAccountName
Remove-ADGroupMember -Identity "Engineering-Team" -Members "akim" -Confirm:$false
Remove-ADGroupMember prompts for confirmation by default (it's a
destructive membership change) — -Confirm:$false is what a non-interactive
script needs to run unattended, but only pass it once you're confident the
script's logic upstream is correct, since it removes the safety net entirely.
Computers and OUs¶
Get-ADComputer -Filter "OperatingSystem -like '*Server*'" -Properties OperatingSystem |
Select-Object Name, OperatingSystem
New-ADOrganizationalUnit -Name "Contractors" -Path "DC=corp,DC=example,DC=com" `
-ProtectedFromAccidentalDeletion $true
-ProtectedFromAccidentalDeletion $true sets AD's own delete-protection
flag on the OU object — a real safeguard worth setting on every OU you
create by script, since a stray Remove-ADOrganizationalUnit -Recursive
elsewhere in a larger automation run is exactly the kind of mistake this
flag exists to stop.
The trap: bulk operations without -WhatIf first¶
# Disable every account that hasn't logged in for 180+ days
$cutoff = (Get-Date).AddDays(-180)
Get-ADUser -Filter "Enabled -eq 'True'" -Properties LastLogonDate |
Where-Object { $_.LastLogonDate -lt $cutoff } |
Disable-ADAccount -WhatIf
Any cmdlet that supports SupportsShouldProcess (nearly everything that
changes AD state — Disable-ADAccount, Remove-ADUser,
Set-ADAccountPassword) accepts -WhatIf, which prints what would
happen without doing it. Running the dry-run version of a bulk filter is
non-negotiable before removing -WhatIf and running for real — a filter
typo here (-lt vs -gt, wrong property) can disable the wrong 5,000
accounts instead of the right 12.
Cheat sheet¶
| Cmdlet | Purpose |
|---|---|
Get-ADUser -Filter "..." -Properties ... |
query users; -Properties needed beyond defaults |
-Filter (PS syntax) vs -LDAPFilter (raw LDAP) |
different grammars, don't mix |
New-ADUser ... -Enabled $true |
create; disabled by default without this |
Set-ADUser / Set-ADAccountPassword |
modify attributes / reset password |
Add-ADGroupMember / Remove-ADGroupMember -Confirm:$false |
manage group membership |
Get-ADComputer -Filter ... |
query computer objects |
New-ADOrganizationalUnit -ProtectedFromAccidentalDeletion $true |
create OU with delete protection |
-WhatIf |
dry-run any state-changing AD cmdlet before running for real |
How It Actually Works¶
The ActiveDirectory module's cmdlets are a PowerShell-native layer over
ADSI (Active Directory Service Interfaces) and, underneath that, the
LDAP protocol — Get-ADUser -Filter {...} compiles your PowerShell
expression syntax filter into an actual LDAP search filter string
(parenthesized prefix-notation boolean expressions like
(&(objectClass=user)(sAMAccountName=jdoe))) before sending it to the
domain controller; this is why AD filter syntax has quirks that don't
match normal PowerShell semantics (certain operators are unsupported, and
some property comparisons require -LDAPFilter directly when the
translated filter can't express what you need) — you're not filtering an
in-memory PowerShell collection, you're generating a query the directory
server executes and only the matching results cross the wire.
Directory objects are returned as Microsoft.ActiveDirectory.Management.
ADAccount/ADUser/ADObject types that lazily hydrate: by default,
Get-ADUser returns only a small default property set, and accessing a
property outside that set (say, MemberOf without having requested it
via -Properties) returns $null even though the attribute has a real
value in the directory — this isn't a bug, it's ADSI's cost-control
design, since AD schema objects can carry hundreds of attributes and
fetching all of them for every result would be expensive at directory-
server scale; -Properties * or explicit property names are how you tell
the underlying LDAP query to actually retrieve those attributes.
Bulk operations without -WhatIf are dangerous specifically because AD
write operations are near-immediately replicated: a change committed
to one domain controller propagates to others via AD's multi-master
replication topology on its own schedule (seconds to minutes depending on
site links), so a mistaken bulk Remove-ADUser isn't a local, easily
reverted action — by the time you notice, the deletion may already have
replicated to other DCs, which is exactly the scenario -WhatIf's
ShouldProcess gate (built on the same mechanism from Module 02's cmdlet
design) is meant to prevent you from reaching irreversibly.
Exercise¶
On a real (or lab/test) AD environment: write a script that finds all
disabled user accounts in a given OU, exports them to CSV with
Name, SamAccountName, and whenChanged, then — after reviewing the
CSV — offers a -Confirm-gated bulk deletion of accounts disabled more
than a year ago. Test the query and export logic against a real
environment before ever running the deletion step, and keep -WhatIf on
until you've manually verified the CSV output is exactly the set you
expect.