declarative-ad-framework/gpo/lib/GPOPermissions.ps1
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

352 lines
11 KiB
PowerShell

# GPOPermissions.ps1
# GPO links, management permissions, and security filtering.
# No dependencies on GPOCore.ps1 (uses only AD/GP cmdlets directly).
function Normalize-GPOLinkTo {
<#
.SYNOPSIS
Normalizes the LinkTo value from settings.ps1 into a consistent
array of hashtables with Target, Order, and Enforced keys.
.DESCRIPTION
Supports three input formats:
- String: 'OU=...' (backward compatible, no order/enforcement management)
- Hashtable: @{ Target = 'OU=...'; Order = 1; Enforced = $true }
- Array: @( @{ Target = ... }, @{ Target = ... } )
#>
param(
[Parameter(Mandatory)]
$LinkTo
)
# Single string
if ($LinkTo -is [string]) {
return @(@{ Target = $LinkTo; Order = $null; Enforced = $false })
}
# Single hashtable with Target key
if ($LinkTo -is [hashtable] -and $LinkTo.Target) {
return @(@{
Target = $LinkTo.Target
Order = if ($LinkTo.ContainsKey('Order')) { $LinkTo.Order } else { $null }
Enforced = if ($LinkTo.ContainsKey('Enforced')) { $LinkTo.Enforced } else { $false }
})
}
# Array of hashtables or strings
if ($LinkTo -is [array]) {
$result = @()
foreach ($item in $LinkTo) {
if ($item -is [string]) {
$result += @{ Target = $item; Order = $null; Enforced = $false }
} elseif ($item -is [hashtable] -and $item.Target) {
$result += @{
Target = $item.Target
Order = if ($item.ContainsKey('Order')) { $item.Order } else { $null }
Enforced = if ($item.ContainsKey('Enforced')) { $item.Enforced } else { $false }
}
}
}
return $result
}
return @()
}
function Ensure-GPOLink {
<#
.SYNOPSIS
Idempotently links a GPO to an OU. Optionally manages link order
and enforcement state.
#>
param(
[Parameter(Mandatory)]
[string]$GPOName,
[Parameter(Mandatory)]
[string]$TargetOU,
$Order = $null,
[bool]$Enforced = $false,
[string]$Domain = (Get-ADDomain).DNSRoot
)
$inheritance = Get-GPInheritance -Target $TargetOU
$linked = $inheritance.GpoLinks | Where-Object { $_.DisplayName -eq $GPOName }
if ($linked) {
$changed = $false
# Check enforcement
$currentEnforced = $linked.Enforced -eq 'Yes'
if ($currentEnforced -ne $Enforced) {
$enforcedStr = if ($Enforced) { 'Yes' } else { 'No' }
Set-GPLink -Name $GPOName -Target $TargetOU -Domain $Domain -Enforced $enforcedStr | Out-Null
Write-Host " [UPDATED] Enforced: $currentEnforced -> $Enforced on $TargetOU" -ForegroundColor Yellow
$changed = $true
}
# Check order (only if specified)
if ($null -ne $Order -and $linked.Order -ne $Order) {
Set-GPLink -Name $GPOName -Target $TargetOU -Domain $Domain -Order $Order | Out-Null
Write-Host " [UPDATED] Link order: $($linked.Order) -> $Order on $TargetOU" -ForegroundColor Yellow
$changed = $true
}
if (-not $changed) {
Write-Host " Already linked: $GPOName -> $TargetOU" -ForegroundColor Green
}
} else {
$params = @{
Name = $GPOName
Target = $TargetOU
Domain = $Domain
}
if ($null -ne $Order) { $params.Order = $Order }
if ($Enforced) { $params.Enforced = 'Yes' }
New-GPLink @params | Out-Null
Write-Host " Linked: $GPOName -> $TargetOU" -ForegroundColor Yellow
}
}
function Compare-GPOLink {
<#
.SYNOPSIS
Checks whether a GPO is linked to the specified OU with correct
order and enforcement. Returns diff objects for any discrepancies.
#>
param(
[Parameter(Mandatory)]
[string]$GPOName,
[Parameter(Mandatory)]
[string]$TargetOU,
$Order = $null,
[bool]$Enforced = $false
)
$diffs = @()
$inheritance = Get-GPInheritance -Target $TargetOU
$linked = $inheritance.GpoLinks | Where-Object { $_.DisplayName -eq $GPOName }
if (-not $linked) {
Write-Host " [DRIFT] Not linked: $GPOName -> $TargetOU" -ForegroundColor Red
$diffs += [PSCustomObject]@{
Type = 'GPOLink'
GPO = $GPOName
TargetOU = $TargetOU
Status = 'Not linked'
}
return $diffs
}
Write-Host " [OK] Linked: $GPOName -> $TargetOU" -ForegroundColor Green
# Check enforcement
$currentEnforced = $linked.Enforced -eq 'Yes'
if ($currentEnforced -ne $Enforced) {
Write-Host " [DRIFT] Enforced: $currentEnforced -> $Enforced on $TargetOU" -ForegroundColor Red
$diffs += [PSCustomObject]@{
Type = 'GPOLinkEnforced'
GPO = $GPOName
TargetOU = $TargetOU
Current = $currentEnforced
Desired = $Enforced
}
}
# Check order (only if specified)
if ($null -ne $Order -and $linked.Order -ne $Order) {
Write-Host " [DRIFT] Link order: $($linked.Order) -> $Order on $TargetOU" -ForegroundColor Red
$diffs += [PSCustomObject]@{
Type = 'GPOLinkOrder'
GPO = $GPOName
TargetOU = $TargetOU
Current = $linked.Order
Desired = $Order
}
}
return $diffs
}
# -------------------------------------------------------------------
# Security Filtering (Deny Apply Group Policy)
# -------------------------------------------------------------------
function Ensure-GPOSecurityFiltering {
<#
.SYNOPSIS
Adds Deny Apply Group Policy ACEs to a GPO for specified groups.
Uses the AD ACL on the GPO object -- Set-GPPermission doesn't support Deny.
#>
param(
[Parameter(Mandatory)]
[string]$GPOName,
[string[]]$DenyApply = @()
)
if ($DenyApply.Count -eq 0) { return }
$gpo = Get-GPO -Name $GPOName -ErrorAction Stop
$domainDN = (Get-ADDomain).DistinguishedName
$gpoADPath = "AD:CN={$($gpo.Id)},CN=Policies,CN=System,$domainDN"
$acl = Get-Acl $gpoADPath
# Apply Group Policy extended right GUID
$applyGpoGuid = [Guid]'edacfd8f-ffb3-11d1-b41d-00a0c968f939'
foreach ($groupName in $DenyApply) {
$group = Get-ADGroup -Identity $groupName -ErrorAction Stop
$sid = New-Object System.Security.Principal.SecurityIdentifier($group.SID)
# Check if a Deny rule already exists for this SID + extended right
$alreadyDenied = $acl.Access | Where-Object {
$_.AccessControlType -eq 'Deny' -and
$_.ObjectType -eq $applyGpoGuid -and
$_.IdentityReference.Translate([System.Security.Principal.SecurityIdentifier]).Value -eq $sid.Value
}
if ($alreadyDenied) {
Write-Host " [OK] $groupName already denied Apply on GPO: $GPOName" -ForegroundColor Green
} else {
$denyRule = New-Object System.DirectoryServices.ActiveDirectoryAccessRule(
$sid,
[System.DirectoryServices.ActiveDirectoryRights]::ExtendedRight,
[System.Security.AccessControl.AccessControlType]::Deny,
$applyGpoGuid
)
$acl.AddAccessRule($denyRule)
Set-Acl $gpoADPath $acl
Write-Host " [DENY] $groupName denied Apply on GPO: $GPOName" -ForegroundColor Yellow
}
}
}
function Compare-GPOSecurityFiltering {
<#
.SYNOPSIS
Checks whether Deny Apply Group Policy ACEs exist for the specified groups.
Returns diffs for any missing deny rules.
#>
param(
[Parameter(Mandatory)]
[string]$GPOName,
[string[]]$DenyApply = @()
)
$diffs = @()
if ($DenyApply.Count -eq 0) { return $diffs }
$gpo = Get-GPO -Name $GPOName -ErrorAction Stop
$domainDN = (Get-ADDomain).DistinguishedName
$gpoADPath = "AD:CN={$($gpo.Id)},CN=Policies,CN=System,$domainDN"
$acl = Get-Acl $gpoADPath
$applyGpoGuid = [Guid]'edacfd8f-ffb3-11d1-b41d-00a0c968f939'
foreach ($groupName in $DenyApply) {
$group = Get-ADGroup -Identity $groupName -ErrorAction Stop
$sid = New-Object System.Security.Principal.SecurityIdentifier($group.SID)
$found = $acl.Access | Where-Object {
$_.AccessControlType -eq 'Deny' -and
$_.ObjectType -eq $applyGpoGuid -and
$_.IdentityReference.Translate([System.Security.Principal.SecurityIdentifier]).Value -eq $sid.Value
}
if ($found) {
Write-Host " [OK] $groupName denied Apply on GPO: $GPOName" -ForegroundColor Green
} else {
Write-Host " [DRIFT] $groupName not denied Apply on GPO: $GPOName" -ForegroundColor Red
$diffs += [PSCustomObject]@{
Type = 'SecurityFiltering'
GPO = $GPOName
Group = $groupName
Status = 'Missing Deny'
}
}
}
return $diffs
}
# -------------------------------------------------------------------
# GPO Management Permissions
# -------------------------------------------------------------------
function Ensure-GPOManagementPermission {
<#
.SYNOPSIS
Ensures a security group has GpoEditDeleteModifySecurity on a GPO.
Idempotent: no-op if the permission already exists.
#>
param(
[Parameter(Mandatory)]
[string]$GPOName,
[Parameter(Mandatory)]
[string]$GroupName
)
$hasPermission = $false
try {
$perm = Get-GPPermission -Name $GPOName -TargetName $GroupName -TargetType Group -ErrorAction Stop
if ($perm.Permission -eq 'GpoEditDeleteModifySecurity') {
$hasPermission = $true
}
} catch {
# Group has no permissions on this GPO
}
if ($hasPermission) {
Write-Host " [OK] $GroupName has edit rights on GPO: $GPOName" -ForegroundColor Green
} else {
Set-GPPermission -Name $GPOName -PermissionLevel GpoEditDeleteModifySecurity `
-TargetName $GroupName -TargetType Group -Replace | Out-Null
Write-Host " [GRANTED] $GroupName edit rights on GPO: $GPOName" -ForegroundColor Yellow
}
}
function Compare-GPOManagementPermission {
<#
.SYNOPSIS
Checks whether a security group has GpoEditDeleteModifySecurity on a GPO.
Returns a diff object if the permission is missing.
#>
param(
[Parameter(Mandatory)]
[string]$GPOName,
[Parameter(Mandatory)]
[string]$GroupName
)
$hasPermission = $false
try {
$perm = Get-GPPermission -Name $GPOName -TargetName $GroupName -TargetType Group -ErrorAction Stop
if ($perm.Permission -eq 'GpoEditDeleteModifySecurity') {
$hasPermission = $true
}
} catch {
# Group has no permissions on this GPO
}
if ($hasPermission) {
Write-Host " [OK] $GroupName has edit rights on GPO: $GPOName" -ForegroundColor Green
} else {
Write-Host " [DRIFT] $GroupName missing edit rights on GPO: $GPOName" -ForegroundColor Red
return [PSCustomObject]@{
Type = 'ManagementPermission'
GPO = $GPOName
Group = $GroupName
Status = 'Missing GpoEditDeleteModifySecurity'
}
}
}