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

<#
.SYNOPSIS
Retrieves the permissions for a specified Azure DevOps group.
 
.DESCRIPTION
The Get-AzDoGroupPermission function retrieves the permissions for a specified Azure DevOps group.
It performs a lookup within the cache for the group and its associated project, retrieves the
security namespace, and constructs a hashtable detailing the group. It then performs a lookup
of the permissions for the group, formats the ACLs, and compares the reference ACLs to the
difference ACLs to determine any changes.
 
.PARAMETER GroupName
The name of the Azure DevOps group. This parameter is mandatory.
 
.PARAMETER isInherited
A boolean value indicating whether the permissions are inherited. This parameter is mandatory.
 
.PARAMETER Permissions
An array of hashtables representing the permissions to be checked. This parameter is optional.
 
.PARAMETER LookupResult
A hashtable representing the lookup result. This parameter is optional.
 
.PARAMETER Ensure
Specifies the desired state of the group permissions. This parameter is optional.
 
.PARAMETER Force
A switch parameter to force the operation. This parameter is optional.
 
.OUTPUTS
System.Management.Automation.PSObject[]
Returns a hashtable detailing the group permissions, including the reference ACLs, difference ACLs,
properties changed, status, and reason.
 
.EXAMPLE
PS C:\> Get-AzDoGroupPermission -GroupName "ProjectName\GroupName" -isInherited $true
 
Retrieves the permissions for the specified Azure DevOps group with inheritance.
 
#>


Function Get-AzDoGroupPermission
{
    [CmdletBinding()]
    [OutputType([System.Management.Automation.PSObject[]])]
    param (
        [Parameter(Mandatory = $true)]
        [string]$GroupName,

        [Parameter(Mandatory = $true)]
        [bool]$isInherited,

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

        [Parameter()]
        [HashTable]$LookupResult,

        [Parameter()]
        [Ensure]$Ensure,

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

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

    # Define the Descriptor Type and Organization Name
    $SecurityNamespace = 'Identity'
    $OrganizationName = (Get-AzDoOrganizationName)
    # Split the Group Name
    $split = $GroupName.Split('\').Split('/')

    # Test if the Group Name is valid
    if ($split.Count -ne 2)
    {
        Write-Warning "[Get-AzDoGroupPermission] Invalid Group Name: $GroupName"
        return
    }

    # Define the Project and Group Name
    $ProjectName = $split[0].Replace('[', '').Replace(']', '')
    $GroupName = $split[1]

    # If the Project Name contains 'organization'. Update the Project Name

    Write-Verbose "[Get-AzDoGroupPermission] Security Namespace: $SecurityNamespace"
    Write-Verbose "[Get-AzDoGroupPermission] Organization Name: $OrganizationName"

    #
    # Construct a hashtable detailing the group

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

    Write-Verbose "[Get-AzDoGroupPermission] Group result hashtable constructed."
    Write-Verbose "[Get-AzDoGroupPermission] Performing lookup of permissions for the group."

    # Define the ACL List
    $ACLList = [System.Collections.Generic.List[Hashtable]]::new()

    # Perform a Lookup within the Cache for the Group
    $group = Get-CacheItem -Key $('[{0}]\{1}' -f $ProjectName, $GroupName) -Type 'LiveGroups'
    $project = Get-CacheItem -Key $ProjectName -Type 'LiveProjects'

    # If not in cache (e.g. group created after last cache init), fall back to a live REST lookup
    if (-not $group)
    {
        Write-Verbose "[Get-AzDoGroupPermission] Group not found in cache — falling back to live API lookup."
        $allGroups = List-DevOpsGroups -Organization $OrganizationName
        $group = $allGroups | Where-Object { $_.principalName -eq $('[{0}]\{1}' -f $ProjectName, $GroupName) } | Select-Object -First 1
        if ($group)
        {
            # Enrich the group with its ACL identity before caching. The init-time cache guarantees every
            # LiveGroups entry carries a .value.ACLIdentity (used downstream by Find-Identity ->
            # ConvertTo-ACLHashtable to key ACEs by descriptor). Caching the raw group here would poison
            # the cache: a later Find-Identity principalName hit would return an identity whose
            # ACLIdentity.descriptor is null, crashing ConvertTo-ACLHashtable ("key cannot be null").
            try
            {
                $aclIdentitySource = Get-DevOpsDescriptorIdentity -OrganizationName $OrganizationName -SubjectDescriptor $group.descriptor

                $aclIdentity = [PSCustomObject]@{
                    id                  = $aclIdentitySource.id
                    descriptor          = $aclIdentitySource.descriptor
                    subjectDescriptor   = $aclIdentitySource.subjectDescriptor
                    providerDisplayName = $aclIdentitySource.providerDisplayName
                    isActive            = $aclIdentitySource.isActive
                    isContainer         = $aclIdentitySource.isContainer
                }
                $group | Add-Member -MemberType NoteProperty -Name 'ACLIdentity' -Force -Value $aclIdentity

                Add-CacheItem -Key $group.principalName -Value $group -Type 'LiveGroups' -SuppressWarning

                # Register in the descriptor index so a later descriptor search resolves from the index.
                $descriptorIndexParams = @{
                    AclDescriptor     = $aclIdentitySource.descriptor
                    PrincipalName     = $group.principalName
                    OriginId          = $group.originId
                    GraphDescriptor   = $group.descriptor
                    AclId             = $aclIdentitySource.id
                    SubjectDescriptor = $aclIdentitySource.subjectDescriptor
                    Persist           = $true
                }
                Add-IdentityDescriptorIndexItem @descriptorIndexParams
            }
            catch
            {
                Write-Warning "[Get-AzDoGroupPermission] Failed to enrich live group '$($group.principalName)' with its ACL identity: $_"
            }
        }
    }

    if (-not $group)
    {
        Throw "[Get-AzDoGroupPermission] Group not found: $('[{0}]\{1}' -f $ProjectName, $GroupName)"
        return
    }

    # If project not in cache, fall back to a live API lookup
    if (-not $project)
    {
        Write-Verbose "[Get-AzDoGroupPermission] Project not found in cache — falling back to live API lookup."
        $projectResponse = Invoke-AzDevOpsApiRestMethod -Uri "https://dev.azure.com/$OrganizationName/_apis/projects/${ProjectName}?api-version=7.1-preview.4" -Method Get
        if ($projectResponse)
        {
            $project = $projectResponse
            Add-CacheItem -Key $ProjectName -Value $project -Type 'LiveProjects'
        }
    }

    if (-not $project)
    {
        Throw "[Get-AzDoGroupPermission] Project not found: $ProjectName"
        return
    }

    #
    # Perform Lookup of the Permissions for the Group

    $namespace = Get-CacheItem -Key $SecurityNamespace -Type 'SecurityNamespaces'
    Write-Verbose "[Get-AzDoGroupPermission] Retrieved namespace: $($namespace.namespaceId)"

    # Add to the ACL Lookup Params
    $getGroupResult.namespace = $namespace

    # Build the token for this specific group so Get-DevOpsACL returns only the one ACL we care
    # about rather than every ACL in the Identity namespace (which can be thousands and very slow).
    # Azure DevOps Identity ACL tokens use a SINGLE backslash: {projectId}\{groupOriginId}.
    $groupToken = '{0}\{1}' -f $project.id, $group.originId

    $ACLLookupParams = @{
        OrganizationName        = $OrganizationName
        SecurityDescriptorId    = $namespace.namespaceId
        Token                   = $groupToken
    }

    # Get the ACL List and format the ACLS
    Write-Verbose "[Get-AzDoGroupPermission] ACL Lookup Params: $($ACLLookupParams | Out-String)"

    $DifferenceACLs = Get-DevOpsACL @ACLLookupParams | ConvertTo-FormattedACL -SecurityNamespace $SecurityNamespace -OrganizationName $OrganizationName
    $DifferenceACLs = $DifferenceACLs | Where-Object {
        ($_.Token.Type -eq 'GroupPermission') -and
        ($_.Token.GroupId -eq $group.originId) -and
        ($_.Token.ProjectId -eq $project.id)
    }

    #
    # Iterate through each of the Permissions and append the permission identity if it contains 'Self' or 'This'
    forEach ($Permission in $Permissions)
    {
        if ($Permission.Identity -in 'self', 'this')
        {
            $Permission.Identity = '[{0}]\{1}' -f $ProjectName, $GroupName
        }
    }

    Write-Verbose "[Get-AzDoGroupPermission] ACL List retrieved and formatted."

    #
    # Convert the Permissions into an ACL Token

    $params = @{
        Permissions         = $Permissions
        SecurityNamespace   = $SecurityNamespace
        isInherited         = $isInherited
        OrganizationName    = $OrganizationName
        TokenName           = '{0}\{1}' -f $project.id, $group.originId
    }

    # Convert the Permissions to an ACL Token
    $ReferenceACLs = ConvertTo-ACL @params | Where-Object { $_.token.Type -ne 'GroupUnknown' }

    # if the ACEs are empty, the desired permissions could not be resolved (unknown identities filtered out)
    if ($ReferenceACLs.aces.Count -eq 0)
    {
        Write-Verbose "[Get-AzDoGroupPermission] No resolvable ACEs for the group — treating as NotFound."
        $getGroupResult.status = [DSCGetSummaryState]::NotFound
        return $getGroupResult
    }

    # Compare the Reference ACLs to the Difference ACLs
    $compareResult = Test-ACLListforChanges -ReferenceACLs $ReferenceACLs -DifferenceACLs $DifferenceACLs
    $getGroupResult.propertiesChanged = $compareResult.propertiesChanged
    $getGroupResult.status = [DSCGetSummaryState]::"$($compareResult.status)"
    $getGroupResult.reason = $compareResult.reason

    # Export the ACL List to a file
    $getGroupResult.ReferenceACLs = $ReferenceACLs
    $getGroupResult.DifferenceACLs = $DifferenceACLs

    # Write
    Write-Verbose "[Get-AzDoGroupPermission] Result Status: $($getGroupResult.status)"
    Write-Verbose "[Get-AzDoGroupPermission] Returning Group Result."

    # Return the Group Result
    return $getGroupResult

}