Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/ACL/Test-ACLListforChanges.ps1


# The Azure Devops ACL API is different to other ACL APIs where it only provides a means to get, remove and set ACLs.
# This means that if there is a change to the ACL's then the entire ACL must be set again.
# This function captures the differences between two ACLs and if there is a different the properties changed will contain the new reference ACL.
#

Function Test-ACLListforChanges
{
    [CmdletBinding()]
    param (
        # The Reference ACL to compare against.
        [Parameter()]
        [Object[]]
        $ReferenceACLs,

        # The Difference ACL to compare against.
        [Parameter()]
        [Object[]]
        $DifferenceACLs
    )

    Write-Verbose "[Test-ACLListforChanges] Started."

    $result = @{
        status = "Unchanged"
        reason = @(
            @{
                Value = "No changes detected."
                Reason = "No changes detected."
            }
        )
        propertiesChanged = @()
    }

    #
    # Test if the Reference and Difference ACLs are null.

    if (($ReferenceACLs.aces -eq $null) -and ($DifferenceACLs -eq $null))
    {
        Write-Verbose "[Test-ACLListforChanges] ACLs are null."
        return $result
    }

    # Get the Token
    #$Token = Get-ACLToken $ReferenceACLs $DifferenceACLs

    # If the Reference ACL is null/empty (no ACEs desired), check whether current state also has no ACEs.
    if (($null -eq $ReferenceACLs) -or ($null -eq $ReferenceACLs.aces) -or ($ReferenceACLs[0].aces.Count -eq 0))
    {
        # DifferenceACLs is empty when the live state has no explicit ACEs (only inherited).
        # An empty list (Count=0) or null both mean "no explicit ACEs present".
        if (($null -eq $DifferenceACLs) -or ($DifferenceACLs.Count -eq 0) -or ($DifferenceACLs[0].aces.Count -eq 0))
        {
            Write-Verbose "[Test-ACLListforChanges] No ACEs desired and none present - state is Unchanged."
            return $result
        }
        # Desired state is 0 ACEs but current state has ACEs - need to clear them.
        Write-Verbose "[Test-ACLListforChanges] No ACEs desired but Difference has ACEs - needs clear."
        $result.status = "Changed"
        $result.propertiesChanged = $ReferenceACLs
        $result.reason += @{
            Value = $ReferenceACLs
            Reason = "ACEs exist but none are desired - clearing required."
        }
        return $result
    }

    # If the Difference ACL is null but no ACEs are desired, the cleared state is correct.
    if (($null -eq $DifferenceACLs) -and ($ReferenceACLs[0].aces.Count -eq 0))
    {
        Write-Verbose "[Test-ACLListforChanges] No ACEs desired and no ACL found - state is Unchanged."
        return $result
    }

    # If the Difference ACL is null, the desired ACE(s) don't exist live yet - that's drift to
    # reconcile (Changed), not an absent resource (NotFound would tell the base class Ensure is
    # Absent, so Set never actually applies the permission and Test keeps reporting the same gap).
    if ($null -eq $DifferenceACLs)
    {
        Write-Verbose "[Test-ACLListforChanges] Difference ACL is null."
        $result.status = "Changed"
        $result.propertiesChanged = $ReferenceACLs
        $result.reason += @{
            Value = $ReferenceACLs
            Reason = "Difference ACL is null."
        }
        return $result
    }

    # Set the flag to be false
    $isChanged = $false

    #
    # Test if the Reference and Difference ACLs count is not equal.

    # Use indexed access to avoid member-enumeration flattening (which returns key-count instead of item-count).
    if ($ReferenceACLs[0].aces.Count -ne $DifferenceACLs[0].aces.Count)
    {
        Write-Verbose "[Test-ACLListforChanges] ACEs count is not equal."
        $result.status = "Changed"
        $result.reason += @{
            Value = $ReferenceACLs
            Reason = "ACLs count is not equal."
        }
        $result.propertiesChanged = $ReferenceACLs
        return $result
    }

    #
    # Test if the inherited flag is not equal.

    if ($ReferenceACLs.inherited -ne $DifferenceACLs.inherited)
    {
        Write-Verbose "[Test-ACLListforChanges] Inherited flag is not equal."
        $result.status = "Changed"
        $result.reason += @{
            Value = $ReferenceACLs
            Reason = "Inherited flag is not equal."
        }
        $result.propertiesChanged = $ReferenceACLs
        return $result
    }

    #
    # Test each of the Reference ACLs
    ForEach ($ReferenceACL in $ReferenceACLs)
    {

        $acl = $DifferenceACLs | Where-Object { $_.Identity.value.originId -eq $ReferenceACL.Identity.value.originId }

        # Test if the ACL is not found in the Difference ACL.
        if ($null -eq $acl)
        {
            $result.status = "Changed"
            $result.propertiesChanged = $ReferenceACLs
            $result.reason += @{
                Value = $ReferenceACL
                Reason = "ACL not found in Difference ACL."
            }
            return $result
        }

        # Test the inherited flag.
        if ($ReferenceACL.isInherited -ne $acl.isInherited)
        {
            $result.status = "Changed"
            $result.propertiesChanged = $ReferenceACLs
            $result.reason += @{
                Value = $ReferenceACL
                Reason = "Inherited flag is not equal."
            }
            return $result
        }

        # Iterate through the ACEs and compare them.

        ForEach ($ReferenceACE in $ReferenceACL.ACEs)
        {

            # Check if the ACE is found in the Difference ACL.
            $ace = $DifferenceACLs.ACEs | Where-Object { $_.Identity.value.originId -eq $ReferenceACE.Identity.value.originId }

            # Check if the ACE is not found in the Difference ACL.
            if ($null -eq $ace)
            {
                $result.status = "Changed"
                $result.propertiesChanged = $ReferenceACLs
                $result.reason += @{
                    Value = $ReferenceACE
                    Reason = "ACE not found in Difference ACL."
                }
                return $result
            }

            #
            # From this point on, we know that the ACE is found in both ACLs.

            #
            # Compare the Allow ACEs

            $ReferenceAllow = Get-BitwiseOrResult $ReferenceACE.Permissions.Allow.Bit
            $DifferenceAllow = Get-BitwiseOrResult $ace.Permissions.Allow.Bit

            # Test if the integers are not equal.
            if ($ReferenceAllow -ne $DifferenceAllow)
            {
                Write-Verbose "[Test-ACLListforChanges] Allow ACEs are not equal."
                $result.propertiesChanged = $ReferenceACLs
                $result.reason += @{
                    Value = @{
                        ReferenceAllow = $ReferenceAllow
                        DifferenceAllow = $DifferenceAllow
                    }
                    Reason = "Allow ACEs are not equal."
                }
                $result.status = "Changed"
            }

            #
            # Compare the Deny ACEs

            $ReferenceDeny = Get-BitwiseOrResult $ReferenceACE.Permissions.Deny.Bit
            $DifferenceDeny = Get-BitwiseOrResult $ace.Permissions.Deny.Bit

            # Test if the integers are not equal.
            if ($ReferenceDeny -ne $DifferenceDeny)
            {
                Write-Verbose "[Test-ACLListforChanges] Deny ACEs are not equal."
                $result.propertiesChanged = $ReferenceACLs
                $result.reason += @{
                    Value = @{
                        ReferenceDeny = $ReferenceDeny
                        DifferenceDeny = $DifferenceDeny
                    }
                    Reason = "Deny ACEs are not equal."
                }
                $result.status = "Changed"
            }

        }

    }

    #
    # Test each of the Difference ACLs

    foreach ($DifferenceACL in $DifferenceACLs)
    {

        $acl = $ReferenceACLs | Where-Object { $_.Identity.value.originId -eq $DifferenceACL.Identity.value.originId }

        # Test if the ACL is not found in the Reference ACL.
        if ($null -eq $acl)
        {
            $result.status = "Changed"
            $result.reason += @{
                Value = $DifferenceACL
                Reason = "ACL not found in Reference ACL."
            }
            $result.propertiesChanged = $ReferenceACLs
            return $result
        }

        # No other tests are required as the Reference ACL has already been tested.

    }

    # Result the result hash table.
    return $result

}