前面 Day 13~Day 16,我們一直都在做:
Query
Audit
Report
例如:
Get-ADUser
Get-ADComputer
Get-ADGroup
Get-ADGroupMember
這些操作大部分都屬於:
Read Only
查錯了,最多就是報表不正確。
但從 Day 17 開始,事情不一樣了。
今天第一次會碰到:
New-ADUser
Set-ADUser
Disable-ADAccount
Move-ADObject
Remove-ADGroupMember
這些 Cmdlet 會真正修改 Active Directory。
所以今天真正的主題不是:
「怎麼用 PowerShell 停用帳號?」
而是:
怎麼讓 AD Change Automation 有足夠的安全機制,不會因為 Script 寫錯就一次改壞一批帳號?
Read Automation 跟 Change Automation 差很多
前幾天:
Get-ADUser -Filter *
即使 Query 寫得不好,大多就是:
抓太多資料
報表不好看
跑比較久
但今天如果寫:
Disable-ADAccount
條件寫錯,結果可能直接影響使用者登入。
所以從今天開始,我希望所有修改 AD 的 Script 都先遵守這個流程:
Input
↓
Validation
↓
Preview
↓
Approval
↓
Action
↓
Verification
↓
Log
而不是:
CSV
↓
直接修改 AD
今天第一個重要指令:-WhatIf
PowerShell 很多會修改系統的 Cmdlet 都支援:
-WhatIf
例如:
Disable-ADAccount -Identity "user01"
-WhatIf
它不會真的停用帳號。
而是告訴你:
如果真的執行,我準備做什麼?
這對 Change Automation 非常重要。
先查,再改
假設今天 HR 通知:
user01
2026/09/30 離職
我不會第一行就:
Disable-ADAccount user01
而是先:
$User = Get-ADUser -Identity "user01"
-Properties `
Department,
Description,
Manager,
MemberOf,
LastLogonDate,
Enabled
然後確認:
$User |
Select-Object `
Name,
SamAccountName,
Enabled,
Department,
LastLogonDate,
DistinguishedName
可能:
Name : User One
SamAccountName : user01
Enabled : True
Department : Sales
LastLogonDate : 2026/09/24
DistinguishedName : CN=User One,OU=Sales,OU=Taipei,...
第一步先確認:
我即將修改的,真的是我要修改的那個帳號。
帳號生命週期其實不只有離職
企業 AD User 大概會經歷:
建立
↓
啟用
↓
屬性修改
↓
Department / Title / Manager 異動
↓
Group Membership 異動
↓
停用
↓
保留
↓
最後才可能刪除
所以真正的:
User Lifecycle Automation
至少可以分成:
Onboarding
Maintenance
Offboarding
今天會先把這三個階段串起來。
第一階段:Onboarding
新人進公司時,IT 可能收到:
姓名:王小明
帳號:ming.wang
Department:IT
Title:System Engineer
Email:ming.wang@contoso.com
OU:Taipei\Users
人工流程可能是:
開 ADUC
↓
New User
↓
填姓名
↓
填帳號
↓
設定密碼
↓
填 Department
↓
填 Email
↓
加入 Group
↓
確認
人少沒有問題。
但如果:
每週很多新人
多個辦公室
固定帳號格式
固定 Group
就很適合做 Automation。
先確認帳號不存在
假設要建立:
ming.wang
第一步不要直接:
New-ADUser
先查:
$ExistingUser = Get-ADUser -Filter 'SamAccountName -eq "ming.wang"'
-ErrorAction SilentlyContinue
然後:
if ($null -ne $ExistingUser) {
Write-Host "Account already exists."
}
因為:
Automation 最怕假設輸入永遠正確。
OU 也要先確認
例如:
$TargetOU =
"OU=Users,OU=Taipei,DC=contoso,DC=com"
先:
Get-ADOrganizationalUnit -Identity $TargetOU
-ErrorAction Stop
如果 OU 根本不存在,就應該:
停止
↓
修正設定
而不是讓 Script 繼續建立到錯誤的位置。
密碼不要寫在 Script 裡
不建議:
$Password = "Company123!"
尤其不要把:
Password
Admin Password
Service Account Password
放在:
.ps1
.csv
.txt
這種純文字檔案裡。
Lab 可以先:
$Password = Read-Host "Enter temporary password"
-AsSecureString
使用者輸入時不會直接顯示明文。
建立第一個 AD User
例如:
$Password = Read-Host "Enter temporary password"
-AsSecureString
然後先用:
New-ADUser -Name "Ming Wang"
-GivenName "Ming" -Surname "Wang"
-SamAccountName "ming.wang" -UserPrincipalName "ming.wang@contoso.com"
-Department "IT" -Title "System Engineer"
-Path "OU=Users,OU=Taipei,DC=contoso,DC=com" -AccountPassword $Password
-Enabled $true -ChangePasswordAtLogon $true
-WhatIf
注意最後:
-WhatIf
所以現在只是 Preview。
Preview 看起來正確,再執行
確認:
User Name 正確
SamAccountName 正確
UPN 正確
OU 正確
Department 正確
之後,按照公司變更流程核准,再進行正式執行。
這個概念很重要:
Script 寫完
≠
可以直接跑 Production
中間應該有:
Review
Test
Approval
建立完要 Verification
不要看到 Cmdlet 沒有跳 Error,就認為工作結束。
可以重新查:
Get-ADUser -Identity "ming.wang"
-Properties Department, Title, Enabled | Select-Object
Name,
SamAccountName,
Enabled,
Department,
Title,
DistinguishedName
這叫:
Post-Change Verification
也就是:
Change
↓
重新 Query
↓
確認結果
Maintenance:人員異動
User Lifecycle 不只有建立跟離職。
例如:
Sales
↓
IT
Department 改變。
可以:
Set-ADUser -Identity "user01"
-Department "IT" `
-WhatIf
職稱:
Set-ADUser -Identity "user01"
-Title "Senior System Engineer" `
-WhatIf
Description:
Set-ADUser -Identity "user01"
-Description "IT Department" `
-WhatIf
一樣先:
Query
Preview
Approval
Change
Verify
Group Membership 也屬於 Lifecycle
例如新人需要:
IT-Team
VPN-Users
FileServer-IT-RW
可以使用:
Add-ADGroupMember -Identity "IT-Team"
-Members "ming.wang" `
-WhatIf
VPN:
Add-ADGroupMember -Identity "VPN-Users"
-Members "ming.wang" `
-WhatIf
但權限 Group 跟一般 Attribute 不一樣。
因為:
加入 Group 本身可能就代表取得系統權限。
所以正式流程最好知道:
誰申請?
誰核准?
加入哪個 Group?
什麼時間?
誰執行?
接下來進入 Offboarding
離職流程通常比建立帳號風險更高。
假設:
user01
要離職。
第一件事情,我建議先做:
Before-State Snapshot
也就是在修改以前,把帳號現況保存下來。
保存帳號資訊
例如:
$User = Get-ADUser -Identity "user01"
-Properties `
Department,
Title,
Description,
LastLogonDate,
MemberOf,
Enabled
再保存:
$User |
Select-Object Name, SamAccountName, Enabled, Department, Title, Description, LastLogonDate, DistinguishedName | Export-Csv
-Path "C:\Temp\user01_Before.csv" -NoTypeInformation
-Encoding UTF8
Group Membership 也先存起來
Day 16 已經學過:
Get-ADPrincipalGroupMembership
所以:
$Groups =
Get-ADPrincipalGroupMembership `
-Identity "user01"
保存:
$Groups |
Select-Object Name, GroupCategory, GroupScope, DistinguishedName | Export-Csv
-Path "C:\Temp\user01_Groups_Before.csv" -NoTypeInformation
-Encoding UTF8
為什麼?
因為幾個月後可能有人問:
user01 離職以前原本在哪些 Group?
至少我們有 Change Evidence。
離職第一個真正的動作:Disable
Preview:
Disable-ADAccount -Identity "user01"
-WhatIf
確認後才執行正式變更。
接著馬上確認:
Get-ADUser -Identity "user01" | Select-Object
Name,
SamAccountName,
Enabled
預期:
Enabled : False
這就是:
Action
↓
Verification
更新 Description
停用後,也可以依公司規範留下資訊。
例如:
Disabled 2026-09-25 - Offboarding
Preview:
Set-ADUser -Identity "user01"
-Description "Disabled 2026-09-25 - Offboarding" `
-WhatIf
但 Description 的格式最好由公司統一。
不要每個 IT:
A 寫離職
B 寫Disabled
C 寫Delete later
最後沒辦法查。
Move 到 Disabled OU
假設公司建立:
OU=DisabledUsers
可以:
$User = Get-ADUser `
-Identity "user01"
Preview:
Move-ADObject -Identity $User.DistinguishedName
-TargetPath "OU=DisabledUsers,DC=contoso,DC=com" `
-WhatIf
這樣可以把:
Active Users
跟:
Disabled Users
分開管理。
也方便套用不同:
GPO
Delegation
Retention Policy
Administrative Process
不要一停用就直接 Remove-ADUser
不建議離職流程變成:
Disable
↓
Delete
比較合理通常是:
Disable
↓
Move Disabled OU
↓
保留一段時間
↓
確認不再需要
↓
依 Policy 清理
因為刪除跟停用最大的差別是:
Disable
→ Object 還在
Delete
→ Object 被刪除
保留帳號一段時間通常更有利於:
稽核
追蹤
還原
後續確認
實際保存多久則應由公司的帳號與資料保留政策決定。
Group Membership 可以全部清掉嗎?
這裡也不要太快。
網路上很容易看到做法:
取得所有 Group
↓
全部 Remove
但我不建議無條件這麼做。
因為可能存在:
稽核用途 Group
License Group
特殊 Workflow Group
歷史保留需求
比較安全的做法是:
先產生 Group Review List。
例如:
$Groups =
Get-ADPrincipalGroupMembership `
-Identity "user01"
查看:
$Groups |
Select-Object Name
再依公司的 Offboarding Policy 決定:
哪些 Group 立即移除?
哪些保留?
哪些需要 Owner 核准?
針對 Sensitive Group 移除
假設公司規定離職時必須立即移除:
VPN-Users
Server-Admins
FileServer-Admins
可以先定義:
$SensitiveGroups = @(
"VPN-Users"
"Server-Admins"
"FileServer-Admins"
)
然後:
foreach ($GroupName in $SensitiveGroups) {
$IsMember = Get-ADGroupMember `
-Identity $GroupName `
-ErrorAction SilentlyContinue |
Where-Object {
$_.SamAccountName -eq "user01"
}
if ($null -ne $IsMember) {
Remove-ADGroupMember `
-Identity $GroupName `
-Members "user01" `
-WhatIf
}
}
這樣比:
把所有 Group Membership 全刪掉
更容易控制。
Direct Membership 跟 Nested Membership 又出現了
假設:
user01
↓
IT-Admins
↓
Server-Admins
user01 可能不是:
Server-Admins 的 Direct Member
所以:
Remove-ADGroupMember -Identity "Server-Admins"
-Members "user01"
並不能解決所有 Nested Permission 問題。
真正需要查:
user01
到底在哪個 Direct Group?
哪些 Direct Group 又帶來高權限?
這就是 Day 16 為什麼要先學:
Direct
Recursive
兩種 Group Membership。
Domain Users 不要當成一般 Group 全部清
一般 AD User 通常會有 Primary Group。
多數情況是:
Domain Users
它的處理機制跟一般 memberOf Membership 不完全相同。
所以不要寫一支:
遍歷所有群組
↓
無條件全部 Remove
的 Offboarding Script。
帳號生命週期 Automation 越重要,越應該:
Explicit > Magic
明確定義要移除哪些權限,比「我猜所有 Group 都可以刪」安全。
把 Offboarding 流程做成 Preview
可以先建立:
$UserName = "user01"
$DisabledOU =
"OU=DisabledUsers,DC=contoso,DC=com"
取得 User:
$User = Get-ADUser -Identity $UserName
-Properties `
Enabled,
Department,
Title,
Description,
LastLogonDate
顯示:
$User |
Select-Object `
Name,
SamAccountName,
Enabled,
Department,
Title,
LastLogonDate,
DistinguishedName
接著全部先:
Disable-ADAccount -Identity $UserName
-WhatIf
Description:
Set-ADUser -Identity $UserName
-Description "Disabled $(Get-Date -Format 'yyyy-MM-dd') - Offboarding" `
-WhatIf
Move:
Move-ADObject -Identity $User.DistinguishedName
-TargetPath $DisabledOU `
-WhatIf
這就是:
Dry Run / Preview。
不要只靠 Write-Host
真正執行 Change,最好有 Log。
Day 6 我們已經寫過:
function Write-Log {
param (
[string]$Message,
[string]$Level = "INFO"
)
$Time =
Get-Date -Format "yyyy-MM-dd HH:mm:ss"
"$Time [$Level] $Message" |
Add-Content `
-Path $LogFile `
-Encoding UTF8
}
今天 Change Log 至少可以記:
Timestamp
Account
Action
Result
Operator
Message
例如:
2026-09-25 05:30:00 [INFO] user01 - Begin offboarding
2026-09-25 05:30:01 [INFO] user01 - Account disabled
2026-09-25 05:30:02 [INFO] user01 - Removed from VPN-Users
2026-09-25 05:30:03 [INFO] user01 - Moved to DisabledUsers
2026-09-25 05:30:04 [INFO] user01 - Verification completed
幾個月後才知道:
這個帳號到底被誰的 Automation 做過什麼。
Error Handling 更重要了
以前 Report Script 錯一筆:
少一筆資料
已經不好。
但 Change Script 如果:
Disable 成功
Move OU 失敗
Group Remove 做一半
就可能留下:
Partial State
所以今天一定要:
try
catch
例如:
try {
Disable-ADAccount `
-Identity $UserName `
-ErrorAction Stop
Write-Log `
"$UserName disabled successfully"
}
catch {
Write-Log `
"$UserName disable failed: $($_.Exception.Message)" `
"ERROR"
}
但 try/catch 不能讓你誤以為有 Transaction
這裡值得特別注意。
假設 Script:
Step 1 Disable → Success
Step 2 Remove Group → Success
Step 3 Move OU → Failed
catch 可以知道 Step 3 失敗。
但 Active Directory 不會因為 Step 3 失敗,就自動:
把 Step 1、2 Undo
所以:
這不是 Database Transaction。
Change Automation 最好每一步:
可驗證
可記錄
知道失敗後怎麼處理
Verification 要重新從 AD 查
例如 Disable 後:
$VerifyUser = Get-ADUser -Identity $UserName
-Properties `
Enabled,
Description
確認:
$VerifyUser |
Select-Object `
SamAccountName,
Enabled,
Description,
DistinguishedName
如果期待:
Enabled = False
但結果:
Enabled = True
那整個 Change 就不應該被標記:
Success
我會把 Status 分得更清楚
例如:
Preview
Success
Failed
Partial
Skipped
而不是只有:
OK
NG
因為實際 Change Automation 很容易有:
帳號找不到
→ Skipped
Disable 成功但 Move 失敗
→ Partial
所有步驟成功
→ Success
只做 WhatIf
→ Preview
這對後面的 Report 非常重要。
建立 Offboarding CSV
未來 HR / IT 可以提供:
SamAccountName,OffboardingDate,Ticket
user01,2026-09-30,INC001001
user02,2026-10-01,INC001002
但我不建議 Script 一讀到 CSV 就自動執行。
第一階段先:
Import CSV
↓
Validate Account
↓
產生 Preview Report
例如:
$Requests =
Import-Csv "C:\Temp\Offboarding.csv"
然後:
foreach ($Request in $Requests) {
$User = Get-ADUser `
-Identity $Request.SamAccountName `
-Properties Enabled `
-ErrorAction SilentlyContinue
[PSCustomObject]@{
SamAccountName =
$Request.SamAccountName
UserExists =
($null -ne $User)
CurrentlyEnabled =
if ($null -ne $User) {
$User.Enabled
}
else {
$null
}
OffboardingDate =
$Request.OffboardingDate
Ticket =
$Request.Ticket
}
}
這份才是:
Change Preview Report。
今天完整範例:安全版 Offboarding Preview
這一版故意只做 Preview,不直接修改 Production AD。
Import-Module ActiveDirectory
$UserName =
"user01"
$DisabledOU =
"OU=DisabledUsers,DC=contoso,DC=com"
$SensitiveGroups = @(
"VPN-Users",
"Server-Admins",
"FileServer-Admins"
)
$ReportFolder =
"C:\Temp\ADReports"
$Date =
Get-Date -Format "yyyyMMdd"
if (-not (Test-Path $ReportFolder)) {
New-Item `
-Path $ReportFolder `
-ItemType Directory |
Out-Null
}
try {
$User = Get-ADUser `
-Identity $UserName `
-Properties `
Enabled,
Department,
Title,
Description,
LastLogonDate `
-ErrorAction Stop
}
catch {
Write-Error `
"User query failed: $($_.Exception.Message)"
return
}
$BeforeState = [PSCustomObject]@{
SamAccountName =
$User.SamAccountName
Name =
$User.Name
Enabled =
$User.Enabled
Department =
$User.Department
Title =
$User.Title
LastLogonDate =
$User.LastLogonDate
DistinguishedName =
$User.DistinguishedName
CheckTime =
Get-Date
}
$BeforeState |
Export-Csv -Path "$ReportFolder\$($UserName)_Before_$Date.csv"
-NoTypeInformation `
-Encoding UTF8
$Groups =
Get-ADPrincipalGroupMembership `
-Identity $UserName
$Groups |
Select-Object Name, GroupCategory, GroupScope, DistinguishedName | Export-Csv
-Path "$ReportFolder$($UserName)Groups_Before$Date.csv" -NoTypeInformation
-Encoding UTF8
$SensitiveGroupReview =
foreach ($GroupName in $SensitiveGroups) {
$IsMember =
$Groups.Name -contains $GroupName
[PSCustomObject]@{
SamAccountName =
$UserName
GroupName =
$GroupName
IsMember =
$IsMember
PlannedAction =
if ($IsMember) {
"Remove"
}
else {
"None"
}
}
}
$SensitiveGroupReview |
Export-Csv -Path "$ReportFolder\$($UserName)_GroupReview_$Date.csv"
-NoTypeInformation `
-Encoding UTF8
Write-Host ""
Write-Host "===== PREVIEW ONLY ====="
Write-Host ""
Disable-ADAccount -Identity $UserName
-WhatIf
foreach ($Item in $SensitiveGroupReview) {
if ($Item.IsMember) {
Remove-ADGroupMember `
-Identity $Item.GroupName `
-Members $UserName `
-Confirm:$false `
-WhatIf
}
}
Set-ADUser -Identity $UserName
-Description "Disabled $(Get-Date -Format 'yyyy-MM-dd') - Offboarding" `
-WhatIf
Move-ADObject -Identity $User.DistinguishedName
-TargetPath $DisabledOU `
-WhatIf
Write-Host ""
Write-Host "No Active Directory changes were made."
Write-Host "Review the preview and CSV reports before approval."
這支 Script 最大的重點
最後一句是:
No Active Directory changes were made.
因為:
-WhatIf
還在。
也就是:
Input
↓
Query
↓
Backup Current State
↓
Group Review
↓
Preview
↓
STOP
到這裡先停。
這比:
寫好 Script
↓
直接跑 Production
安全很多。
Production 版本應該再加 Approval Gate
未來可以做:
Offboarding.csv
↓
Validation
↓
Preview Report
↓
Ticket / Manager Approval
↓
Approved.csv
↓
Change Script
↓
Verification
↓
Change Report
也就是把:
Request
和:
Execution
分開。
這會比用:
按 Y 確認
更加適合企業流程。
Onboarding 其實也可以使用相同架構
例如:
NewUsers.csv
↓
Validation
↓
帳號是否重複?
OU 是否存在?
Group 是否存在?
UPN 是否重複?
↓
Preview
↓
Approval
↓
Create User
↓
Add Group
↓
Verify
↓
Report
所以今天學到的不只是:
New-ADUser
Disable-ADAccount
而是一個可以套用到很多 Automation 的模式:
Detect
Validate
Preview
Approve
Change
Verify
Audit
為什麼我一直強調 Preview?
因為 Automation 有一個特性:
它不只讓正確的事情變快,也會讓錯誤的事情變快。
人工操作:
弄錯一個帳號
可能影響一個人。
Automation:
foreach 500 Users
條件寫錯,可能一下影響很多帳號。
所以越是批次、自動化、權限高的 Script,就越需要:
Validation
WhatIf
Scope
Approval
Logging
Verification
Day 17 小結
今天是整個系列的一個重要分界點。
Day 1~16 大部分在做:
Read
Query
Audit
Report
Day 17 第一次正式開始:
Change
主要接觸:
New-ADUser
Set-ADUser
Add-ADGroupMember
Disable-ADAccount
Remove-ADGroupMember
Move-ADObject
但比這些 Cmdlet 更重要的是今天建立的 Change Workflow:
Input
↓
Validation
↓
Before-State Snapshot
↓
Preview / WhatIf
↓
Approval
↓
Action
↓
Verification
↓
Log / Report
尤其是離職帳號,我們不再把它想成:
Disable User
而是一整個 Lifecycle:
確認帳號
↓
保存原始狀態
↓
保存 Group Membership
↓
Disable
↓
清理必要的敏感權限
↓
更新標記
↓
Move Disabled OU
↓
Verification
↓
Retention
↓
依 Policy 決定未來是否 Delete
真正好的 Automation 不是:
「什麼都自動做。」
而是:
「把適合自動化的部分做快,同時保留必要的確認、核准與追蹤。」
這也是從「會寫 PowerShell」走向「能把 PowerShell 放進企業維運流程」非常重要的一步。
Day 18 預告
Day 18|AD 批次建立帳號:從 CSV Onboarding 到自動建立 User
Day 17 我們已經知道:
New-ADUser
可以建立一個帳號。
但真正的 Automation 不會只建立:
1 個 User
而是開始處理:
NewUsers.csv
姓名
帳號
Department
Title
OU
UPN
需要的 Group
Day 18 我們會建立:
HR / IT Input CSV
↓
Import-Csv
↓
欄位驗證
↓
SamAccountName 重複檢查
↓
UPN 重複檢查
↓
OU 是否存在
↓
Group 是否存在
↓
Preview
↓
建立 AD User
↓
加入 Group
↓
Result Report
並特別處理一個很重要的問題:
100 個新人裡面如果第 37 個建立失敗,前面成功的 36 個跟後面的 63 個該怎麼辦?
Day 18 會正式開始做第一支比較完整的 CSV-driven AD Onboarding Automation。