functions/AccessRules/Test-LdsAccessRule.ps1

function Test-LdsAccessRule {
    <#
    .SYNOPSIS
        Tests, whether the current access rules match the configured state.
     
    .DESCRIPTION
        Tests, whether the current access rules match the configured state.
     
    .PARAMETER Server
        The LDS Server to target.
     
    .PARAMETER Partition
        The Partition on the LDS Server to target.
     
    .PARAMETER Credential
        Credentials to use for the operation.
     
    .PARAMETER Delete
        Undo everything defined in configuration.
        Allows rolling back after deployment.
     
    .EXAMPLE
        PS C:\> Test-LdsAccessRule -Server lds1.contoso.com -Partition 'DC=fabrikam,DC=org'
 
        Tests, whether the current access rules on lds1.contoso.com match the configured state.
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)]
        [string]
        $Server,

        [Parameter(Mandatory = $true)]
        [string]
        $Partition,

        [PSCredential]
        $Credential,

        [switch]
        $Delete
    )
    
    begin {
        #region Functions
        function Resolve-AccessRule {
            [OutputType([System.DirectoryServices.ActiveDirectoryAccessRule])]
            [CmdletBinding()]
            param (
                [Parameter(Mandatory = $true)]
                $RuleCfg,

                [Parameter(Mandatory = $true)]
                [string]
                $Server,

                [Parameter(Mandatory = $true)]
                [string]
                $Partition,

                [PSCredential]
                $Credential,

                [hashtable]
                $SchemaCache = @{ },

                [hashtable]
                $PrincipalCache = @{ },

                [string]
                $DomainSID
            )

            $ldsParam = $PSBoundParameters | ConvertTo-PSFHashtable -Include Server, Credential

            $rights = $script:adrights[$RuleCfg.Rights]
            
            # resolve AccessRule settings
            $inheritanceType = 'None'
            if ($RuleCfg.Inheritance) {
                $inheritanceType = $RuleCfg.Inheritance
            }
            $objectType = [guid]::Empty
            $inheritedObjectType = [guid]::Empty
            if ($RuleCfg.ObjectType) { $objectType = $RuleCfg.ObjectType | Resolve-SchemaGuid @ldsParam -Cache $SchemaCache }
            if ($RuleCfg.InheritedObjectType) { $inheritedObjectType = $RuleCfg.InheritedObjectType | Resolve-SchemaGuid @ldsParam -Cache $SchemaCache }
            $type = 'Allow'
            if ($RuleCfg.Type) { $type = $RuleCfg.Type }

            $principal = $PrincipalCache["$($RuleCfg.IdentityType):$($RuleCfg.Identity)"]
            if (-not $principal -and 'SID' -eq $RuleCfg.IdentityType){
                $ruleIdentity = $RuleCfg.Identity -replace '%DomainSID%', $DomainSID
                $sid = $ruleIdentity -as [System.Security.Principal.SecurityIdentifier]
                if (-not $sid) {
                    throw "Principal is not a legal SID: $($RuleCfg.Identity)!"
                }
                $PrincipalCache["$($RuleCfg.IdentityType):$($RuleCfg.Identity)"] = $sid
                $principal = $PrincipalCache["$($RuleCfg.IdentityType):$($RuleCfg.Identity)"]
            }
            elseif (-not $principal) {
                $principalObject = Get-ADObject @ldsParam -SearchBase $Partition -LDAPFilter "(&(objectClass=$($RuleCfg.IdentityType))(name=$($RuleCfg.Identity)))" -Properties ObjectSID -ErrorAction Stop
                if (-not $principalObject) {
                    throw "Principal not found: $($RuleCfg.IdentityType) - $($RuleCfg.Identity)"
                }

                $PrincipalCache["$($RuleCfg.IdentityType):$($RuleCfg.Identity)"] = $principalObject.ObjectSID
                $principal = $PrincipalCache["$($RuleCfg.IdentityType):$($RuleCfg.Identity)"]
            }

            [System.DirectoryServices.ActiveDirectoryAccessRule]::new(
                $principal,
                $rights,
                $type,
                $objectType,
                $inheritanceType,
                $inheritedObjectType
            )
        }
        
        function Compare-AccessRule {
            [CmdletBinding()]
            param (
                [Parameter(Mandatory = $true)]
                [System.DirectoryServices.ActiveDirectoryAccessRule[]]
                $Reference,

                [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
                $InputObject,

                [switch]
                $NoMatch
            )

            process {
                if (-not $InputObject) { return }


                $isMatched = $false

                foreach ($referenceObject in $Reference) {
                    if ($referenceObject.ActiveDirectoryRights -bxor $InputObject.ActiveDirectoryRights) { continue }
                    if ($referenceObject.InheritanceType -ne $InputObject.InheritanceType) { continue }
                    if ($referenceObject.ObjectType -ne $InputObject.ObjectType) { continue }
                    if ($referenceObject.InheritedObjectType -ne $InputObject.InheritedObjectType) { continue }
                    if ($referenceObject.AccessControlType -ne $InputObject.AccessControlType) { continue }
                    if ("$($referenceObject.IdentityReference)" -ne "$($InputObject.IdentityReference)") { continue }

                    $isMatched = $true
                    break
                }

                if ($isMatched -eq -not $NoMatch) {
                    $InputObject
                }
            }
        }

        function Get-ObjectDefaultRule {
            [CmdletBinding()]
            param (
                [string]
                $Path,

                [hashtable]
                $LdsParam,

                [hashtable]
                $LdsParamLight,

                $RootDSE,

                [hashtable]
                $DefaultPermissions
            )

            $adObject = Get-ADObject @LdsParam -Identity $Path -Properties ObjectClass
            if ($DefaultPermissions.ContainsKey($adObject.ObjectClass)) {
                return $DefaultPermissions[$adObject.ObjectClass]
            }

            $class = Get-ADObject @ldsParamLight -SearchBase $RootDSE.schemaNamingContext -LDAPFilter "(&(objectClass=classSchema)(ldapDisplayName=$($adObject.ObjectClass)))" -Properties defaultSecurityDescriptor
            $acl = [System.DirectoryServices.ActiveDirectorySecurity]::new()
            $acl.SetSecurityDescriptorSddlForm($class.defaultSecurityDescriptor)
            $DefaultPermissions[$adObject.ObjectClass] = $acl.GetAccessRules($true, $false, [System.Security.Principal.SecurityIdentifier])
            $DefaultPermissions[$adObject.ObjectClass]
        }
        #endregion Functions

        Update-ADSec
        Update-LdsConfiguration -LdsServer $Server -LdsPartition $Partition
        $ldsParam = $PSBoundParameters | ConvertTo-PSFHashtable -Include Server, Partition, Credential
        $ldsParamLight = $ldsParam | ConvertTo-PSFHashtable -Exclude Partition
        $rootDSE = Get-ADRootDSE @ldsParamLight
        $domainSID = (Get-ADObject @ldsParamLight -LDAPFilter '(&(objectCategory=group)(name=Administrators))' -SearchBase $ldsParam.Partition -Properties objectSID).ObjectSID.Value -replace '-512$'

        $principals = @{ }
        $schemaCache = @{ }
        $pathCache = @{ }
    }
    process {
        #region Adding
        foreach ($ruleCfg in $script:content.accessrule.Values) {
            $resolvedPath = $ruleCfg.Path -replace '%DomainDN%', $Partition
            try { $rule = Resolve-AccessRule @ldsParam -RuleCfg $ruleCfg -SchemaCache $schemaCache -PrincipalCache $principals -DomainSID $domainSID }
            catch {
                Write-PSFMessage -Level Warning -Message "Failed to process rule for $resolvedPath, granting $($ruleCfg.Rights) to $($ruleCfg.Identity)" -ErrorRecord $_
                continue
            }
            if (-not $pathCache[$resolvedPath]) { $pathCache[$resolvedPath] = @($rule) }
            else { $pathCache[$resolvedPath] = @($pathCache[$resolvedPath]) + @($rule) }

            $acl = Get-AdsAcl @ldsParamLight -Path $resolvedPath
            $currentRules = $acl.GetAccessRules($true, $false, [System.Security.Principal.SecurityIdentifier])
            $matching = $currentRules | Compare-AccessRule -Reference $rule

            $change = [PSCustomObject]@{
                Path  = $resolvedPath
                Name  = $ruleCfg.Identity
                Right = $ruleCfg.Rights
                Type  = $rule.AccessControlType
                Rule  = $rule
            }
            Add-Member -InputObject $change -MemberType ScriptMethod -Name ToString -Force -Value {
                if ('Allow' -eq $this.Type) { '{0} -> {1}' -f $this.Name, $this.Right }
                else { '{0} != {1}' -f $this.Name, $this.Right }
            }

            if ($matching) {
                if ($Delete) {
                    $change.Rule = $matching
                    New-TestResult -Type AccessRule -Action Remove -Identity $resolvedPath -Configuration $ruleCfg -ADObject $acl -Change $change
                }
                continue
            }
            if ($Delete) { continue }

            New-TestResult -Type AccessRule -Action Add -Identity $resolvedPath -Configuration $ruleCfg -ADObject $acl -Change $change
        }
        #endregion Adding

        #region Removing
        $schemaDefaultPermissions = @{ }
        $sidToName = @{ }

        foreach ($adPath in $pathCache.Keys) {
            $defaultRules = Get-ObjectDefaultRule -Path $adPath -LdsParam $ldsParam -LdsParamLight $ldsParamLight -RootDSE $rootDSE -DefaultPermissions $schemaDefaultPermissions
            $intendedRules = @($defaultRules) + @($pathCache[$adPath]) | Remove-PSFNull

            $acl = Get-AdsAcl @ldsParamLight -Path $adPath
            $currentRules = $acl.GetAccessRules($true, $false, [System.Security.Principal.SecurityIdentifier])
            $surplusRules = $currentRules | Compare-AccessRule -Reference $intendedRules -NoMatch

            foreach ($surplusRule in $surplusRules) {
                # Skip OU deletion protection
                if ('S-1-1-0' -eq $surplusRule.IdentityReference -and 'Deny' -eq $surplusRule.AccessControlType) { continue }
                
                if (-not $sidToName[$surplusRule.IdentityReference]) {
                    try { $sidToName[$surplusRule.IdentityReference] = Get-ADObject @ldsParamLight -SearchBase $Partition -LDAPFilter "(objectSID=$($surplusRule.IdentityReference))" -Properties Name }
                    catch { $sidToName[$surplusRule.IdentityReference] = @{ Name = $surplusRule.IdentityReference }}
                    if (-not $sidToName[$surplusRule.IdentityReference]) { $sidToName[$surplusRule.IdentityReference] = @{ Name = $surplusRule.IdentityReference } }
                }
                $change = [PSCustomObject]@{
                    Path  = $adPath
                    Name  = $sidToName[$surplusRule.IdentityReference].Name
                    Right = $surplusRule.ActiveDirectoryRights
                    Type  = $surplusRule.AccessControlType
                    Rule  = $surplusRule
                }

                Add-Member -InputObject $change -MemberType ScriptMethod -Name ToString -Force -Value {
                    if ('Allow' -eq $this.Type) { '{0} -> {1}' -f $this.Name, $this.Right }
                    else { '{0} != {1}' -f $this.Name, $this.Right }
                }
    
                New-TestResult -Type AccessRule -Action Remove -Identity $adPath -ADObject $acl -Change $change
            }
        }
        #endregion Removing
    }
}