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

201 lines
7.5 KiB
PowerShell

# ADUser.ps1
# User account management.
# Depends on: ADCore.ps1 (New-RandomPassword)
function Ensure-ADUserAccount {
<#
.SYNOPSIS
Idempotently creates a user account, ensures correct OU placement and group membership.
#>
param(
[Parameter(Mandatory)]
[string]$SamAccountName,
[Parameter(Mandatory)]
[string]$Name,
[string]$GivenName = '',
[string]$Surname = '',
[Parameter(Mandatory)]
[string]$Path,
[bool]$Enabled = $true,
[string[]]$MemberOf = @(),
[hashtable]$Properties = @{}
)
$created = $false
try {
$user = Get-ADUser -Identity $SamAccountName -Properties MemberOf -ErrorAction Stop
Write-Host " [OK] User exists: $SamAccountName" -ForegroundColor Green
# Check OU placement
$currentOU = ($user.DistinguishedName -split ',', 2)[1]
if ($currentOU -ne $Path) {
Move-ADObject -Identity $user.DistinguishedName -TargetPath $Path
Write-Host " [MOVED] $SamAccountName -> $Path" -ForegroundColor Yellow
}
# Check enabled state
if ($user.Enabled -ne $Enabled) {
Set-ADUser -Identity $SamAccountName -Enabled $Enabled
Write-Host " [UPDATED] $SamAccountName Enabled=$Enabled" -ForegroundColor Yellow
}
# Check optional properties
if ($Properties.Count -gt 0) {
$propNames = @($Properties.Keys)
$current = Get-ADUser -Identity $SamAccountName -Properties $propNames
foreach ($prop in $propNames) {
$desired = $Properties[$prop]
$actual = $current.$prop
if ("$actual" -ne "$desired") {
Set-ADUser -Identity $SamAccountName -Replace @{ $prop = $desired }
Write-Host " [UPDATED] $SamAccountName $prop='$desired'" -ForegroundColor Yellow
}
}
}
} catch [Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException] {
$password = New-RandomPassword
$securePass = ConvertTo-SecureString $password -AsPlainText -Force
$params = @{
SamAccountName = $SamAccountName
Name = $Name
UserPrincipalName = "$SamAccountName@$((Get-ADDomain).DNSRoot)"
Path = $Path
AccountPassword = $securePass
Enabled = $Enabled
ChangePasswordAtLogon = $true
}
if ($GivenName) { $params.GivenName = $GivenName }
if ($Surname) { $params.Surname = $Surname }
# Merge optional properties into creation parameters
foreach ($key in $Properties.Keys) {
$params[$key] = $Properties[$key]
}
New-ADUser @params
# Write password to a secured file -- never to console (avoids transcript leaks)
$credDir = Join-Path (Split-Path $PSScriptRoot -Parent) '.credentials'
$currentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
if (-not (Test-Path $credDir)) {
New-Item -ItemType Directory -Path $credDir -Force | Out-Null
# Lock directory permissions to current user only
$dirAcl = Get-Acl $credDir
$dirAcl.SetAccessRuleProtection($true, $false)
$dirRule = New-Object System.Security.AccessControl.FileSystemAccessRule(
$currentUser, 'FullControl', 'ContainerInherit,ObjectInherit', 'None', 'Allow')
$dirAcl.AddAccessRule($dirRule)
Set-Acl -Path $credDir -AclObject $dirAcl
}
$credFile = Join-Path $credDir "$SamAccountName.txt"
Set-Content -Path $credFile -Value "User: $SamAccountName`nPassword: $password`nCreated: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')`nNote: Must change on first login`nDelete this file after handing off the password."
# Lock file permissions to current user only
$acl = Get-Acl $credFile
$acl.SetAccessRuleProtection($true, $false)
$rule = New-Object System.Security.AccessControl.FileSystemAccessRule(
$currentUser, 'FullControl', 'Allow')
$acl.AddAccessRule($rule)
Set-Acl -Path $credFile -AclObject $acl
Write-Host " [CREATED] User: $SamAccountName (password saved to $credFile)" -ForegroundColor Yellow
$created = $true
}
# Sync group membership
foreach ($group in $MemberOf) {
$currentGroups = @(Get-ADPrincipalGroupMembership -Identity $SamAccountName | Select-Object -ExpandProperty Name)
if ($group -notin $currentGroups) {
Add-ADGroupMember -Identity $group -Members $SamAccountName
Write-Host " [ADDED] $SamAccountName -> $group" -ForegroundColor Yellow
}
}
return $created
}
function Compare-ADUserAccount {
<#
.SYNOPSIS
Compares desired user state against AD. Returns diffs.
#>
param(
[Parameter(Mandatory)]
[string]$SamAccountName,
[Parameter(Mandatory)]
[string]$Path,
[bool]$Enabled = $true,
[string[]]$MemberOf = @(),
[hashtable]$Properties = @{}
)
$diffs = @()
try {
$user = Get-ADUser -Identity $SamAccountName -Properties MemberOf -ErrorAction Stop
Write-Host " [OK] User exists: $SamAccountName" -ForegroundColor Green
# Check OU placement
$currentOU = ($user.DistinguishedName -split ',', 2)[1]
if ($currentOU -ne $Path) {
Write-Host " [DRIFT] $SamAccountName in $currentOU, expected $Path" -ForegroundColor Red
$diffs += [PSCustomObject]@{ Type = 'UserOU'; User = $SamAccountName; Current = $currentOU; Desired = $Path }
}
# Check enabled state
if ($user.Enabled -ne $Enabled) {
Write-Host " [DRIFT] $SamAccountName Enabled=$($user.Enabled), expected $Enabled" -ForegroundColor Red
$diffs += [PSCustomObject]@{ Type = 'UserEnabled'; User = $SamAccountName; Current = $user.Enabled; Desired = $Enabled }
}
# Check optional properties
if ($Properties.Count -gt 0) {
$propNames = @($Properties.Keys)
$current = Get-ADUser -Identity $SamAccountName -Properties $propNames
foreach ($prop in $propNames) {
$desired = $Properties[$prop]
$actual = $current.$prop
if ("$actual" -ne "$desired") {
Write-Host " [DRIFT] $SamAccountName $prop='$actual', expected '$desired'" -ForegroundColor Red
$diffs += [PSCustomObject]@{
Type = 'UserProperty'; User = $SamAccountName
Property = $prop; Current = $actual; Desired = $desired
}
}
}
}
# Check group membership
$currentGroups = @(Get-ADPrincipalGroupMembership -Identity $SamAccountName | Select-Object -ExpandProperty Name)
foreach ($group in $MemberOf) {
if ($group -notin $currentGroups) {
Write-Host " [DRIFT] $SamAccountName not in group $group" -ForegroundColor Red
$diffs += [PSCustomObject]@{ Type = 'UserGroup'; User = $SamAccountName; Group = $group; Status = 'Missing' }
}
}
} catch [Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException] {
Write-Host " [MISSING] User: $SamAccountName" -ForegroundColor Red
$diffs += [PSCustomObject]@{ Type = 'User'; Name = $SamAccountName; Status = 'Missing' }
}
return $diffs
}