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