functions/gplinks/Test-DMGPLink.ps1

function Test-DMGPLink {
    <#
    .SYNOPSIS
        Tests, whether the configured group policy linking matches the desired state.
     
    .DESCRIPTION
        Tests, whether the configured group policy linking matches the desired state.
        Define the desired state using the Register-DMGPLink command.
     
    .PARAMETER Server
        The server / domain to work with.
     
    .PARAMETER Credential
        The credentials to use for this operation.
     
    .EXAMPLE
        PS C:\> Test-DMGPLink -Server contoso.com
 
        Tests, whether the group policy links of contoso.com match the configured state
    #>

    [CmdletBinding()]
    param (
        [PSFComputer]
        $Server,
        
        [PSCredential]
        $Credential
    )
    
    begin {
        $parameters = $PSBoundParameters | ConvertTo-PSFHashtable -Include Server, Credential
        $parameters['Debug'] = $false
        Assert-ADConnection @parameters -Cmdlet $PSCmdlet
        Invoke-Callback @parameters -Cmdlet $PSCmdlet
        Assert-Configuration -Type GroupPolicyLinks, GroupPolicyLinksDynamic -Cmdlet $PSCmdlet
        Set-DMDomainContext @parameters

        #region Utility Functions
        function Get-OUData {
            [CmdletBinding()]
            param (
                $Parameters
            )

            $ous = @{ }
            #region Explicit OUs
            foreach ($organizationalUnit in $script:groupPolicyLinks.Keys) {
                $resolvedOU = Resolve-String -Text $organizationalUnit
                $ous[$resolvedOU] = [PSCustomObject]@{
                    OrganizationalUnit = $resolvedOU
                    ProcessingMode     = 'Additive'
                    Include            = @()
                    Exclude            = @()
                    ExtendedInclude    = @()
                }
                $ous[$resolvedOU].Include = $script:groupPolicyLinks[$organizationalUnit].Values | Where-Object Present
                $ous[$resolvedOU].Exclude = $script:groupPolicyLinks[$organizationalUnit].Values | Where-Object Present -EQ $false
                if ($ous[$resolvedOU].Include.ProcessingMode -contains 'Constrained') {
                    $ous[$resolvedOU].ProcessingMode = 'Constrained'
                }
            }
            #region Explicit OUs
            
            #region Filter-Based OUs
            foreach ($filter in $script:groupPolicyLinksDynamic.Keys) {
                $adObjects = Resolve-ADObject @Parameters -Filter (Resolve-String -Text $filter) -ObjectClass organizationalUnit
                $values = $script:groupPolicyLinksDynamic[$filter].Values
                
                foreach ($adObject in $adObjects) {
                    if (-not $ous[$adObject.DistinguishedName]) {
                        $ous[$adObject.DistinguishedName] = [PSCustomObject]@{
                            OrganizationalUnit = $adObject.DistinguishedName
                            ProcessingMode     = 'Additive'
                            Include            = @()
                            Exclude            = @()
                            ExtendedInclude    = @()
                        }
                    }
                    $container = $ous[$adObject.DistinguishedName]
                    $container.Include = $container.Include, $values | Remove-PSFNull -Enumerate | Where-Object Present
                    $container.Exclude = $container.Exclude, $values | Remove-PSFNull -Enumerate | Where-Object Present -EQ $false
                    if ($container.Include.ProcessingMode -contains 'Constrained') {
                        $container.ProcessingMode = 'Constrained'
                    }
                }
            }
            #endregion Filter-Based OUs
            
            $ous.Values
        }
        
        function New-Update {
            [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')]
            [CmdletBinding()]
            param (
                $PolicyName,
                $Status,
                $Action
            )

            [PSCustomObject]@{
                PSTypeName = 'DomainManagement.GPLink.Update'
                Action     = $Action
                Policy     = $PolicyName
                Status     = $Status
            }
        }
        
        function Get-LinkUpdate {
            [CmdletBinding()]
            param (
                $Configuration,
                $ADObject
            )

            $includeSorted = $Configuration.Include | Sort-Object @{ Expression = { $_.Tier }; Descending = $true }, Precedence | Where-Object PolicyName -NotIn $Configuration.Exclude.PolicyName
            $currentSorted = $ADObject.LinkedGroupPolicyObjects | Sort-Object Precedence

            if ($Configuration.ProcessingMode -eq 'Additive') {
                $currentAdditive = $ADObject.LinkedGroupPolicyObjects | Where-Object DisplayName -NotIn $includeSorted.PolicyName | Where-Object DisplayName -NotIn $Configuration.Exclude.PolicyName | Sort-Object Precedence | Add-Member -MemberType NoteProperty -Name Tier -Value 0 -PassThru -Force | Add-Member -MemberType AliasProperty -Name PolicyName -Value DisplayName -PassThru -Force
                $newDesiredState = @($currentAdditive) + @($includeSorted) | Write-Output | Remove-PSFNull | Sort-Object @{ Expression = { $_.Tier }; Descending = $true }, Precedence
            }
            else { $newDesiredState = $includeSorted }
            $Configuration.ExtendedInclude = $newDesiredState
                
            if (Compare-Array -ReferenceObject $newDesiredState.PolicyName -DifferenceObject $currentSorted.DisplayName -OrderSpecific -Quiet) {
                return
            }

            $index = 0
            foreach ($desired in $newDesiredState) {
                if ($currentSorted.DisplayName -notcontains $desired.PolicyName) {
                    New-Update -Action Add -PolicyName $desired.PolicyName -Status 'Enabled'
                    $index = $index + 1
                    continue
                }
                if ($index -gt @($currentSorted).Count -or $desired.PolicyName -ne $currentSorted[$index].DisplayName) {
                    New-Update -Action Reorder -PolicyName $desired.PolicyName -Status 'Enabled'
                    $index = $index + 1
                    continue
                }
                $index = $index + 1
            }
            foreach ($current in $currentSorted) {
                if ($current.DisplayName -notin $newDesiredState.PolicyName) {
                    New-Update -Action Delete -PolicyName $current.DisplayName -Status $current.Status
                }
            }
        }
        #endregion Utility Functions

        $gpoDisplayToDN = @{ }
        $gpoDNToDisplay = @{ }
        foreach ($adPolicyObject in (Get-ADObject @parameters -LDAPFilter '(objectCategory=groupPolicyContainer)' -Properties DisplayName, DistinguishedName)) {
            $gpoDisplayToDN[$adPolicyObject.DisplayName] = $adPolicyObject.DistinguishedName
            $gpoDNToDisplay[$adPolicyObject.DistinguishedName] = $adPolicyObject.DisplayName
        }
    }
    process {
        #region Process Configuration
        $ouData = Get-OUData -Parameters $parameters
        foreach ($ouDatum in $ouData) {
            $resultDefaults = @{
                Server        = $Server
                ObjectType    = 'GPLink'
                Identity      = $ouDatum.OrganizationalUnit
                Configuration = $ouDatum
            }

            #region Handle AD Object doesn't exist
            try {
                $adObject = Get-ADObject @parameters -Identity $ouDatum.OrganizationalUnit -ErrorAction Stop -Properties gPLink, Name, DistinguishedName
                $resultDefaults['ADObject'] = $adObject
            }
            catch {
                Write-PSFMessage -String 'Test-DMGPLink.OUNotFound' -StringValues $ouDatum.OrganizationalUnit -ErrorRecord $_ -Tag 'panic', 'failed'
                New-TestResult @resultDefaults -Type 'MissingParent'
                Continue
            }
            #endregion Handle AD Object doesn't exist

            #region Handle AD Object does not contain any links
            $currentState = $adObject | ConvertTo-GPLink -PolicyMapping $gpoDNToDisplay
            Add-Member -InputObject $adObject -MemberType NoteProperty -Name LinkedGroupPolicyObjects -Value $currentState -Force
            if (-not $currentState) {
                New-TestResult @resultDefaults -Type 'New'
                continue
            }
            #endregion Handle AD Object does not contain any links

            $updates = Get-LinkUpdate -Configuration $ouDatum -ADObject $adObject
            if ($updates) {
                New-TestResult @resultDefaults -Type 'Update' -Changed $updates
            }
        }

        #region Process Managed Estate
        # OneLevel needs to be converted to base, as searching for OUs with "OneLevel" would return unmanaged OUs.
        # This search however is targeted at GPOs linked to managed OUs only.
        $translateScope = @{
            'Subtree'  = 'Subtree'
            'OneLevel' = 'Base'
            'Base'     = 'Base'
        }
        $adObjects = foreach ($searchBase in (Resolve-ContentSearchBase @parameters)) {
            Get-ADObject @parameters -LDAPFilter '(gPLink=*)' -SearchBase $searchBase.SearchBase -SearchScope $translateScope[$searchBase.SearchScope] -Properties gPLink, Name, DistinguishedName
        }

        foreach ($adObject in $adObjects) {
            # If we have a configuration on it, it has already been processed
            if ($adObject.DistinguishedName -in $ouData.OrganizationalUnit) { continue }
            if ([string]::IsNullOrWhiteSpace($adObject.GPLink)) { continue }

            $linkObjects = $adObject | ConvertTo-GPLink -PolicyMapping $gpoDNToDisplay
            Add-Member -InputObject $adObject -MemberType NoteProperty -Name LinkedGroupPolicyObjects -Value $linkObjects -Force

            $changes = foreach ($linkedObject in $linkObjects) {
                New-Update -PolicyName $linkedObject.DisplayName -Status $linkedObject.Status -Action Delete
            }
            New-TestResult -ObjectType GPLink -Type 'Delete' -Identity $adObject.DistinguishedName -Server $Server -ADObject $adObject -Changed $changes
        }
        #endregion Process Managed Estate
    }
}