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

<#
.SYNOPSIS
Retrieves the current ACL state of an Azure DevOps classic Release folder.
 
.DESCRIPTION
Builds the 'ReleaseManagement' ACL token for the folder and compares its ACL against the desired
permissions.
 
A Release folder's token addresses the folder by path - '{projectId}/{folderPath}' - whereas a
release definition's token uses its numeric id. Omitting FolderPath targets the project release
root, whose token is the project id alone.
 
.PARAMETER ProjectName
The name of the Azure DevOps project.
 
.PARAMETER FolderPath
The Release folder path. Omit to target the project release root.
 
.PARAMETER isInherited
Whether the ACL inherits permissions from its parent.
 
.PARAMETER Permissions
The desired access control entries.
 
.PARAMETER LookupResult
The lookup result supplied by the DSC base class.
 
.PARAMETER Ensure
The desired state, supplied by the DSC base class.
 
.PARAMETER Force
Forces the operation, supplied by the DSC base class.
 
.EXAMPLE
Get-AzDoReleaseFolderPermission -ProjectName 'Contoso' -FolderPath '\Platform' -isInherited $true
#>

Function Get-AzDoReleaseFolderPermission
{
    [CmdletBinding()]
    [OutputType([System.Management.Automation.PSObject[]])]
    param
    (
        [Parameter(Mandatory = $true)]
        [Alias('Name')]
        [System.String]$ProjectName,

        [Parameter()]
        [Alias('Path')]
        [System.String]$FolderPath,

        [Parameter(Mandatory = $true)]
        [System.Boolean]$isInherited,

        [Parameter()]
        [HashTable[]]$Permissions,

        [Parameter()]
        [HashTable]$LookupResult,

        [Parameter()]
        [Ensure]$Ensure,

        [Parameter()]
        [System.Management.Automation.SwitchParameter]$Force
    )

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

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

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

    $projectCache = Resolve-AzDoProject -ProjectName $ProjectName

    if (-not $projectCache)
    {
        $getResult.status = [DSCGetSummaryState]::Error
        $getResult.reason = "Project not found: $ProjectName"
        return $getResult
    }

    $normalizedPath = Format-AzDoPipelineFolderPath -Path $FolderPath
    $isRoot         = ([String]::IsNullOrWhiteSpace($FolderPath) -or $normalizedPath -eq '\')

    if (-not $isRoot)
    {
        $folders = List-DevOpsReleaseFolders -Organization $OrganizationName -ProjectName $ProjectName -Path $normalizedPath
        $folder  = $folders | Where-Object { (Format-AzDoPipelineFolderPath -Path $_.path) -eq $normalizedPath } | Select-Object -First 1

        if ($null -eq $folder)
        {
            Write-Warning "[Get-AzDoReleaseFolderPermission] Release folder '$normalizedPath' not found in project '$ProjectName'."
            $getResult.status = [DSCGetSummaryState]::NotFound
            $getResult.reason = "Release folder not found: $normalizedPath"
            return $getResult
        }
    }

    $namespace = Get-CacheItem -Key $SecurityNamespace -Type 'SecurityNamespaces'

    if (-not $namespace)
    {
        Write-Error "[Get-AzDoReleaseFolderPermission] Security namespace '$SecurityNamespace' not found."
        $getResult.status = [DSCGetSummaryState]::Error
        return $getResult
    }

    $getResult.namespace = $namespace

    # The API token carries the folder path without its leading separator.
    $tokenFolderPath = $normalizedPath.TrimStart('\')

    $aclToken = if ($isRoot) { '{0}' -f $projectCache.id }
                else         { '{0}/{1}' -f $projectCache.id, $tokenFolderPath }

    $DevOpsACLs = Get-DevOpsACL -OrganizationName $OrganizationName -SecurityDescriptorId $namespace.namespaceId -Token $aclToken
    if (-not $DevOpsACLs) { $DevOpsACLs = Get-DevOpsACL -OrganizationName $OrganizationName -SecurityDescriptorId $namespace.namespaceId }

    # Drop the ACLs this lookup cannot be interested in BEFORE formatting them, exactly as
    # Get-AzDoPipelineFolderPermission does. Formatting resolves every ACE through Find-Identity,
    # which costs an API round trip for each descriptor that is not already cached, so formatting
    # a whole namespace only to keep one token is where the time goes. The fallback above fires
    # whenever the folder has no explicit ACL - the normal state once permissions revert to
    # inherited - so the full namespace is the common path, not a rare one.
    $DevOpsACLs = @($DevOpsACLs | Where-Object {
        $rawToken = $_.token
        if ([String]::IsNullOrEmpty($rawToken)) { return $false }
        if ($isRoot) { return ($rawToken -eq $aclToken) }
        $prefix = '{0}/' -f $projectCache.id
        if (-not $rawToken.StartsWith($prefix, [System.StringComparison]::OrdinalIgnoreCase)) { return $false }
        (Format-AzDoPipelineFolderPath -Path $rawToken.Substring($prefix.Length)) -eq $normalizedPath
    })

    $DifferenceACLs = $DevOpsACLs | ConvertTo-FormattedACL -SecurityNamespace $SecurityNamespace -OrganizationName $OrganizationName

    if ($isRoot)
    {
        # The release root is the token with no folder path and no definition id.
        $DifferenceACLs = $DifferenceACLs | Where-Object {
            ($_.Token.Type -eq 'ReleaseRoot') -and ($_.Token.ProjectId -eq $projectCache.id)
        }
    }
    else
    {
        $DifferenceACLs = $DifferenceACLs | Where-Object {
            ($_.Token.Type -eq 'ReleaseFolder') -and
            ($_.Token.ProjectId -eq $projectCache.id) -and
            ((Format-AzDoPipelineFolderPath -Path $_.Token.FolderPath) -eq $normalizedPath)
        }
    }

    # The resource-side token keeps the leading separator on the folder path: New-ACLToken uses it
    # to tell a folder from a root/definition token of the same name.
    $tokenName = if ($isRoot) { $ProjectName } else { '{0}/{1}' -f $ProjectName, $normalizedPath }

    $params = @{
        Permissions       = $Permissions
        SecurityNamespace = $SecurityNamespace
        isInherited       = $isInherited
        OrganizationName  = $OrganizationName
        TokenName         = $tokenName
    }

    $ReferenceACLs = ConvertTo-ACL @params

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

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

    return $getResult
}