Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoOrganizationPermission/Get-AzDoOrganizationPermission.ps1

<#
.SYNOPSIS
Retrieves the current state of organization-level permissions.
 
.DESCRIPTION
Compares the desired ACEs against the organization root ACL in the 'Collection' security namespace.
Only the identities named in Permissions are compared; the root carries protected and system ACEs
that are left alone.
 
The root token is taken from the live ACL rather than assumed. If no root ACL is found the status is
Error, and Set and Remove refuse to write.
 
.PARAMETER GroupName
The group this configuration block is for (the resource key).
 
.PARAMETER Permissions
The desired ACEs (Identity + Permission map).
 
.PARAMETER LookupResult
Supplied by the DSC base class.
 
.PARAMETER Ensure
Supplied by the DSC base class.
 
.PARAMETER Force
Supplied by the DSC base class.
 
.OUTPUTS
System.Collections.Hashtable
#>

Function Get-AzDoOrganizationPermission
{
    [CmdletBinding()]
    [OutputType([System.Collections.Hashtable])]
    param (
        [Parameter(Mandatory = $true)][string]$GroupName,
        [Parameter()][HashTable[]]$Permissions,
        [Parameter()][HashTable]$LookupResult,
        [Parameter()][Ensure]$Ensure,
        [Parameter()][System.Management.Automation.SwitchParameter]$Force
    )

    Write-Verbose "[Get-AzDoOrganizationPermission] Started."

    $SecurityNamespace = 'Collection'
    $OrganizationName  = Get-AzDoOrganizationName

    $getResult = @{
        Ensure            = [Ensure]::Absent
        propertiesChanged = @()
        groupName         = $GroupName
        status            = $null
        reason            = $null
    }

    $namespace = Get-CacheItem -Key $SecurityNamespace -Type 'SecurityNamespaces'
    if (-not $namespace)
    {
        Write-Error "[Get-AzDoOrganizationPermission] Security namespace '$SecurityNamespace' not found." -ErrorAction Continue
        $getResult.status = [DSCGetSummaryState]::Error
        $getResult.reason = "Security namespace '$SecurityNamespace' not found."
        return $getResult
    }

    $getResult.namespace = $namespace

    # The namespace holds one ACL per organization, so reading it whole is cheap.
    $DevOpsACLs = @(Get-DevOpsACL -OrganizationName $OrganizationName -SecurityDescriptorId $namespace.namespaceId)
    $rootACL    = $DevOpsACLs | Where-Object { $_.token -match $LocalizedDataAzACLTokenPatten.OrganizationPermission } | Select-Object -First 1

    if (-not $rootACL)
    {
        # Never guess the token: an ACL written to the wrong one would silently do nothing.
        $seen = (@($DevOpsACLs | ForEach-Object { $_.token }) | Where-Object { $_ }) -join "', '"
        $getResult.status = [DSCGetSummaryState]::Error
        $getResult.reason = "No organization root ACL was found in the '$SecurityNamespace' namespace. Tokens seen: '$seen'."
        return $getResult
    }

    $rootToken = $rootACL.token

    # Wrap in @() so a single ACL is not unrolled to a bare hashtable ([0] indexing downstream).
    $DifferenceACLs = @($rootACL | ConvertTo-FormattedACL -SecurityNamespace $SecurityNamespace -OrganizationName $OrganizationName)

    # The root has no parent to inherit from, so its inheritance flag is not configurable: the
    # desired ACL takes the live value, which keeps it out of the comparison and the write.
    $params = @{
        Permissions       = $Permissions
        SecurityNamespace = $SecurityNamespace
        isInherited       = [bool]$rootACL.inheritPermissions
        OrganizationName  = $OrganizationName
        TokenName         = $rootToken
    }

    $ReferenceACLs = @(ConvertTo-ACL @params | Where-Object { $_.token.Type -ne 'OrganizationUnknown' })

    # Compare only the identities this resource manages; the root holds many other ACEs.
    if ($ReferenceACLs.Count -gt 0 -and $DifferenceACLs.Count -gt 0)
    {
        $desiredOriginIds = @($ReferenceACLs[0].aces | ForEach-Object { $_.Identity.value.originId } | Where-Object { $_ })
        if ($desiredOriginIds.Count -gt 0)
        {
            $DifferenceACLs[0]['aces'] = @($DifferenceACLs[0].aces | Where-Object { $_.Identity.value.originId -in $desiredOriginIds })
        }
        else
        {
            $DifferenceACLs[0]['aces'] = @()
        }
    }

    $compareResult = Test-ACLListforChanges -ReferenceACLs $ReferenceACLs -DifferenceACLs $DifferenceACLs

    $getResult.propertiesChanged = $compareResult.propertiesChanged
    $getResult.status            = [DSCGetSummaryState]::"$($compareResult.status)"
    $getResult.reason            = $compareResult.reason

    # Absent means none of the managed identities holds an entry on the token. The base class only
    # treats NotFound as absent, so report that once they are all gone; otherwise Test stays False
    # and Remove runs on every pass.
    # With no identity resolved there is nothing managed to look for, so the live ACEs do not count.
    $managedAceCount = if ($ReferenceACLs.Count -eq 0) { 0 }
                       else { @($DifferenceACLs | ForEach-Object { $_.aces } | Where-Object { $_ }).Count }
    if ($Ensure -eq [Ensure]::Absent -and $managedAceCount -eq 0)
    {
        $getResult.status = [DSCGetSummaryState]::NotFound
        $getResult.reason = 'None of the managed identities has an entry on the organization root.'
    }
    $getResult.ReferenceACLs     = $ReferenceACLs
    $getResult.DifferenceACLs    = $DifferenceACLs
    $getResult.aclToken          = $rootToken
    # The raw ACL, so Set can carry every ACE it does not manage through the replacing POST.
    $getResult.LiveACLs          = @($rootACL)

    return $getResult
}