Infrastructure-as-code framework for Active Directory objects and Group Policy. Sanitized from production deployment for public sharing.
233 lines
7.9 KiB
PowerShell
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
|
|
}
|