Damien Coles f172d00514 Initial release: Declarative AD Framework v2.1.0
Infrastructure-as-code framework for Active Directory objects and Group Policy.
Sanitized from production deployment for public sharing.
2026-02-19 17:02:42 +00:00

232 lines
8.8 KiB
PowerShell

# Apply-ADBaseline.ps1
# Orchestration script for managing AD objects (OUs, groups, users).
#
# Reads declarative definitions from ous.ps1, groups.ps1, users.ps1,
# delegations.ps1, and password-policies.ps1, then compares against
# live AD and applies changes. Works from any machine with RSAT.
#
# Usage:
# .\Apply-ADBaseline.ps1 # Apply all AD objects
# .\Apply-ADBaseline.ps1 -TestOnly # Compare desired vs. current (no changes)
[CmdletBinding()]
param(
[switch]$TestOnly
)
$ErrorActionPreference = 'Stop'
$ScriptRoot = $PSScriptRoot
# -------------------------------------------------------------------
# Load helper functions
# -------------------------------------------------------------------
. (Join-Path $ScriptRoot 'lib\ADHelper.ps1')
# -------------------------------------------------------------------
# Load definitions
# -------------------------------------------------------------------
$ous = & (Join-Path $ScriptRoot 'ous.ps1')
$groups = & (Join-Path $ScriptRoot 'groups.ps1')
$users = & (Join-Path $ScriptRoot 'users.ps1')
$delegations = & (Join-Path $ScriptRoot 'delegations.ps1')
$passwordPolicies = & (Join-Path $ScriptRoot 'password-policies.ps1')
$totalDiffs = 0
$createdUsers = @()
# -------------------------------------------------------------------
# Pre-flight: Warn about stale credential files (older than 24 hours)
# -------------------------------------------------------------------
$credDir = Join-Path $ScriptRoot '.credentials'
if (Test-Path $credDir) {
$staleFiles = Get-ChildItem -Path $credDir -Filter '*.txt' |
Where-Object { $_.LastWriteTime -lt (Get-Date).AddHours(-24) }
if ($staleFiles.Count -gt 0) {
Write-Host '========================================================' -ForegroundColor Red
Write-Host ' WARNING: Stale credential files detected (>24 hours)' -ForegroundColor Red
Write-Host '========================================================' -ForegroundColor Red
foreach ($f in $staleFiles) {
$age = [math]::Round(((Get-Date) - $f.LastWriteTime).TotalHours, 1)
Write-Host " - $($f.Name) (${age}h old)" -ForegroundColor Red
}
Write-Host ' These should have been handed off and deleted.' -ForegroundColor Red
Write-Host '========================================================' -ForegroundColor Red
Write-Host ''
}
}
# -------------------------------------------------------------------
# Step 1: Organizational Units (must exist before groups/users)
# -------------------------------------------------------------------
Write-Host "=== Organizational Units ===" -ForegroundColor White
foreach ($ou in $ous) {
if ($TestOnly) {
$diff = Compare-ADOU -Name $ou.Name -Path $ou.Path
if ($diff) { $totalDiffs++ }
} else {
Ensure-ADOU -Name $ou.Name -Path $ou.Path -Description $ou.Description | Out-Null
}
}
Write-Host ''
# -------------------------------------------------------------------
# Step 2: Security Groups (must exist before users can join them)
# -------------------------------------------------------------------
Write-Host "=== Security Groups ===" -ForegroundColor White
foreach ($group in $groups) {
if ($TestOnly) {
$diffs = Compare-ADSecurityGroup -Name $group.Name -Members $group.Members
$totalDiffs += @($diffs).Count
} else {
$params = @{
Name = $group.Name
Path = $group.Path
Description = $group.Description
Scope = $group.Scope
}
# Don't sync members yet - users may not exist
Ensure-ADSecurityGroup @params | Out-Null
}
}
Write-Host ''
# -------------------------------------------------------------------
# Step 3: User Accounts
# -------------------------------------------------------------------
Write-Host "=== User Accounts ===" -ForegroundColor White
foreach ($user in $users) {
# Extract optional properties (anything not in the core schema)
$coreKeys = @('SamAccountName','Name','GivenName','Surname','Path','Enabled','MemberOf')
$optionalProps = @{}
foreach ($key in $user.Keys) {
if ($key -notin $coreKeys) {
$optionalProps[$key] = $user[$key]
}
}
if ($TestOnly) {
$diffs = Compare-ADUserAccount `
-SamAccountName $user.SamAccountName `
-Path $user.Path `
-Enabled $user.Enabled `
-MemberOf $user.MemberOf `
-Properties $optionalProps
$totalDiffs += @($diffs).Count
} else {
$wasCreated = Ensure-ADUserAccount `
-SamAccountName $user.SamAccountName `
-Name $user.Name `
-GivenName $user.GivenName `
-Surname $user.Surname `
-Path $user.Path `
-Enabled $user.Enabled `
-MemberOf $user.MemberOf `
-Properties $optionalProps
if ($wasCreated) {
$createdUsers += $user.SamAccountName
}
}
}
Write-Host ''
# -------------------------------------------------------------------
# Step 4: Sync group membership (now that users exist)
# -------------------------------------------------------------------
if (-not $TestOnly) {
Write-Host "=== Group Membership Sync ===" -ForegroundColor White
foreach ($group in $groups) {
if ($group.Members.Count -gt 0) {
Ensure-ADSecurityGroup -Name $group.Name -Path $group.Path -Scope $group.Scope -Members $group.Members | Out-Null
}
}
Write-Host ''
}
# -------------------------------------------------------------------
# Step 5: OU Delegation (ACLs)
# -------------------------------------------------------------------
Write-Host "=== OU Delegation ===" -ForegroundColor White
foreach ($delegation in $delegations) {
foreach ($ou in $delegation.TargetOUs) {
if ($TestOnly) {
$diffs = Compare-OUDelegation `
-GroupName $delegation.GroupName `
-TargetOU $ou `
-Rights $delegation.Rights
$totalDiffs += @($diffs).Count
} else {
Ensure-OUDelegation `
-GroupName $delegation.GroupName `
-TargetOU $ou `
-Rights $delegation.Rights | Out-Null
}
}
}
Write-Host ''
# -------------------------------------------------------------------
# Step 6: Fine-Grained Password Policies (PSOs)
# -------------------------------------------------------------------
Write-Host "=== Password Policies ===" -ForegroundColor White
foreach ($pso in $passwordPolicies) {
if ($TestOnly) {
$diffs = Compare-ADPasswordPolicy -Definition $pso
$totalDiffs += @($diffs).Count
} else {
Ensure-ADPasswordPolicy -Definition $pso
}
}
Write-Host ''
# -------------------------------------------------------------------
# Summary
# -------------------------------------------------------------------
if ($TestOnly) {
if ($totalDiffs -eq 0) {
Write-Host 'All AD objects match desired state.' -ForegroundColor Green
} else {
Write-Host "$totalDiffs difference(s) found." -ForegroundColor Red
}
Write-Host 'Compliance test complete. No changes were made.' -ForegroundColor Yellow
} else {
Write-Host 'All AD objects applied.' -ForegroundColor Green
# --- Credential handoff warning ---
if ($createdUsers.Count -gt 0) {
$credDir = Join-Path $ScriptRoot '.credentials'
Write-Host ''
Write-Host '========================================================' -ForegroundColor Magenta
Write-Host ' ACTION REQUIRED: New user credentials pending handoff' -ForegroundColor Magenta
Write-Host '========================================================' -ForegroundColor Magenta
Write-Host ''
Write-Host " $($createdUsers.Count) user(s) were created with temporary passwords." -ForegroundColor Yellow
Write-Host ' Passwords are stored in:' -ForegroundColor Yellow
Write-Host " $credDir" -ForegroundColor White
Write-Host ''
foreach ($u in $createdUsers) {
Write-Host " - $u.txt" -ForegroundColor White
}
Write-Host ''
Write-Host ' You must:' -ForegroundColor Yellow
Write-Host ' 1. Read each credential file' -ForegroundColor White
Write-Host ' 2. Securely share the password with the end user' -ForegroundColor White
Write-Host ' 3. Delete the credential file after handoff' -ForegroundColor White
Write-Host ''
Write-Host ' These files are ACL-locked to your account only.' -ForegroundColor DarkGray
Write-Host ' Users must change their password on first login.' -ForegroundColor DarkGray
Write-Host '========================================================' -ForegroundColor Magenta
}
}