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

233 lines
7.9 KiB
PowerShell

# ADDelegation.ps1
# OU delegation (ACL) management.
# Uses AD schema GUIDs to construct ActiveDirectoryAccessRule objects.
# No dependencies on other AD modules.
# AD Schema GUIDs -- well-known, universal across all AD forests.
# Verified against Microsoft AD Schema documentation.
# https://learn.microsoft.com/en-us/windows/win32/adschema/
$Script:ADClassGuid = @{
user = [Guid]'bf967aba-0de6-11d0-a285-00aa003049e2'
}
$Script:ADAttributeGuid = @{
userAccountControl = [Guid]'bf967a68-0de6-11d0-a285-00aa003049e2'
lockoutTime = [Guid]'28630ebf-41d5-11d1-a9c1-0000f80367c1'
displayName = [Guid]'bf967953-0de6-11d0-a285-00aa003049e2'
givenName = [Guid]'f0f8ff8e-1191-11d0-a060-00aa006c33ed'
sn = [Guid]'bf967a41-0de6-11d0-a285-00aa003049e2'
mail = [Guid]'bf967961-0de6-11d0-a285-00aa003049e2'
telephoneNumber = [Guid]'bf967a49-0de6-11d0-a285-00aa003049e2'
description = [Guid]'bf967950-0de6-11d0-a285-00aa003049e2'
}
$Script:ADExtendedRight = @{
ResetPassword = [Guid]'00299570-246d-11d0-a768-00aa006e0529'
}
function ConvertTo-DelegationACEs {
<#
.SYNOPSIS
Converts a rights specification into ActiveDirectoryAccessRule objects.
#>
param(
[Parameter(Mandatory)]
[System.Security.Principal.SecurityIdentifier]$SID,
[Parameter(Mandatory)]
$Rights
)
$aces = @()
$rightsArray = @($Rights)
foreach ($right in $rightsArray) {
if ($right -eq 'FullControl') {
$aces += New-Object System.DirectoryServices.ActiveDirectoryAccessRule(
$SID,
[System.DirectoryServices.ActiveDirectoryRights]::GenericAll,
[System.Security.AccessControl.AccessControlType]::Allow,
[System.DirectoryServices.ActiveDirectorySecurityInheritance]::All
)
}
elseif ($right -eq 'ListContents') {
$aces += New-Object System.DirectoryServices.ActiveDirectoryAccessRule(
$SID,
[System.DirectoryServices.ActiveDirectoryRights]::ListChildren,
[System.Security.AccessControl.AccessControlType]::Allow,
[System.DirectoryServices.ActiveDirectorySecurityInheritance]::None
)
}
elseif ($right -eq 'ReadAllProperties') {
$aces += New-Object System.DirectoryServices.ActiveDirectoryAccessRule(
$SID,
[System.DirectoryServices.ActiveDirectoryRights]::ReadProperty,
[System.Security.AccessControl.AccessControlType]::Allow,
[System.DirectoryServices.ActiveDirectorySecurityInheritance]::None
)
}
elseif ($right -eq 'ResetPassword') {
$aces += New-Object System.DirectoryServices.ActiveDirectoryAccessRule(
$SID,
[System.DirectoryServices.ActiveDirectoryRights]::ExtendedRight,
[System.Security.AccessControl.AccessControlType]::Allow,
$Script:ADExtendedRight['ResetPassword'],
[System.DirectoryServices.ActiveDirectorySecurityInheritance]::Descendents,
$Script:ADClassGuid['user']
)
}
elseif ($right -match '^ReadWriteProperty:(.+)$') {
$attrName = $Matches[1]
if (-not $Script:ADAttributeGuid.ContainsKey($attrName)) {
throw "Unknown AD attribute '$attrName' in delegation rule. Add it to `$Script:ADAttributeGuid in ADDelegation.ps1."
}
$aces += New-Object System.DirectoryServices.ActiveDirectoryAccessRule(
$SID,
([System.DirectoryServices.ActiveDirectoryRights]::ReadProperty -bor
[System.DirectoryServices.ActiveDirectoryRights]::WriteProperty),
[System.Security.AccessControl.AccessControlType]::Allow,
$Script:ADAttributeGuid[$attrName],
[System.DirectoryServices.ActiveDirectorySecurityInheritance]::Descendents,
$Script:ADClassGuid['user']
)
}
else {
throw "Unknown delegation right: '$right'. Valid: FullControl, ListContents, ReadAllProperties, ResetPassword, ReadWriteProperty:<attribute>"
}
}
return $aces
}
function Test-ACEExists {
<#
.SYNOPSIS
Returns $true if an equivalent ACE already exists in the given ACL.
#>
param(
[System.DirectoryServices.ActiveDirectorySecurity]$ACL,
[System.DirectoryServices.ActiveDirectoryAccessRule]$DesiredACE
)
$desiredSID = $DesiredACE.IdentityReference.Value
foreach ($existing in $ACL.Access) {
$existingSID = $null
try {
$existingSID = $existing.IdentityReference.Translate(
[System.Security.Principal.SecurityIdentifier]).Value
} catch {
continue
}
if ($existingSID -ne $desiredSID) { continue }
# Bitwise subset check: AD merges compatible ACEs, so the stored
# rights may be a superset of what we asked for (e.g. ListChildren
# + ReadProperty merged into one ACE).
if (($existing.ActiveDirectoryRights -band $DesiredACE.ActiveDirectoryRights) -ne $DesiredACE.ActiveDirectoryRights) { continue }
if ($existing.AccessControlType -ne $DesiredACE.AccessControlType) { continue }
if ($existing.ObjectType -ne $DesiredACE.ObjectType) { continue }
if ($existing.InheritanceType -ne $DesiredACE.InheritanceType) { continue }
if ($existing.InheritedObjectType -ne $DesiredACE.InheritedObjectType) { continue }
return $true
}
return $false
}
function Ensure-OUDelegation {
<#
.SYNOPSIS
Idempotently applies delegation ACEs to an OU for a given security group.
Returns the number of ACEs added (0 if already in desired state).
#>
param(
[Parameter(Mandatory)]
[string]$GroupName,
[Parameter(Mandatory)]
[string]$TargetOU,
[Parameter(Mandatory)]
$Rights
)
$group = Get-ADGroup -Identity $GroupName -ErrorAction Stop
$sid = New-Object System.Security.Principal.SecurityIdentifier($group.SID)
$desiredACEs = ConvertTo-DelegationACEs -SID $sid -Rights $Rights
$adPath = "AD:$TargetOU"
$acl = Get-Acl $adPath
$added = 0
foreach ($ace in $desiredACEs) {
if (-not (Test-ACEExists -ACL $acl -DesiredACE $ace)) {
$acl.AddAccessRule($ace)
$added++
}
}
if ($added -gt 0) {
Set-Acl $adPath $acl
Write-Host " [DELEGATED] $GroupName on $TargetOU ($added ACE(s) added)" -ForegroundColor Yellow
} else {
Write-Host " [OK] $GroupName delegation on $TargetOU" -ForegroundColor Green
}
return $added
}
function Compare-OUDelegation {
<#
.SYNOPSIS
Checks if delegation ACEs exist for a group on an OU.
Returns diff objects for any missing ACEs.
#>
param(
[Parameter(Mandatory)]
[string]$GroupName,
[Parameter(Mandatory)]
[string]$TargetOU,
[Parameter(Mandatory)]
$Rights
)
$diffs = @()
$group = Get-ADGroup -Identity $GroupName -ErrorAction Stop
$sid = New-Object System.Security.Principal.SecurityIdentifier($group.SID)
$desiredACEs = ConvertTo-DelegationACEs -SID $sid -Rights $Rights
$adPath = "AD:$TargetOU"
$acl = Get-Acl $adPath
$missing = 0
foreach ($ace in $desiredACEs) {
if (-not (Test-ACEExists -ACL $acl -DesiredACE $ace)) {
$missing++
}
}
if ($missing -eq 0) {
Write-Host " [OK] $GroupName delegation on $TargetOU" -ForegroundColor Green
} else {
Write-Host " [DRIFT] $GroupName missing $missing ACE(s) on $TargetOU" -ForegroundColor Red
$diffs += [PSCustomObject]@{
Type = 'Delegation'
Group = $GroupName
TargetOU = $TargetOU
Status = "Missing $missing ACE(s)"
}
}
return $diffs
}