Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Helper/ACL/Parse-ACLToken.ps1

Function Parse-ACLToken
{
    param(
        [Parameter(Mandatory = $true)]
        [String]$Token,

        [Parameter(Mandatory = $true)]
        [String]$SecurityNamespace
    )

    $result = @{}
    $useRegexVariable = $true

    Write-Verbose "[Parse-ACLToken] Started."
    Write-Verbose "[Parse-ACLToken] Token: $Token"
    Write-Verbose "[Parse-ACLToken] Security Namespace: $SecurityNamespace"

    # Helper: extract named capture group 'identifiers' from all regex matches for complex patterns.
    $extractIdentifiers = {
        param([string]$tok, [string]$pattern)
        $m = [regex]::Matches($tok, $pattern)
        if ($m.Count -eq 0) { throw "Token '$tok' is not recognized." }
        @($m | ForEach-Object { @{ identifier = $_.Groups['identifiers'].Value } })
    }

    switch ($SecurityNamespace)
    {
        'Git Repositories' {
            switch -regex ($Token.Trim())
            {
                $LocalizedDataAzACLTokenPatten.OrganizationGit { $result.type = 'OrganizationGit'; break }
                $LocalizedDataAzACLTokenPatten.GitProject      { $result.type = 'GitProject';      break }
                $LocalizedDataAzACLTokenPatten.GitRepository   { $result.type = 'GitRepository';   break }
                $LocalizedDataAzACLTokenPatten.GitBranch       { $result.type = 'GitBranch';       break }
                default { throw "Token '$Token' is not recognized." }
            }
        }

        'Identity' {
            switch -regex ($Token.Trim())
            {
                $LocalizedDataAzACLTokenPatten.ResourcePermission { $result.type = 'ResourcePermission'; break }
                $LocalizedDataAzACLTokenPatten.GroupPermission    { $result.type = 'GroupPermission';    break }
                default { throw "Token '$Token' is not recognized." }
            }
        }

        'CSS' {
            switch -regex ($Token.Trim())
            {
                $LocalizedDataAzACLTokenPatten.AreaPathPermission {
                    $result.type        = 'AreaPathPermission'
                    $result.Identifiers = & $extractIdentifiers $Token $LocalizedDataAzACLTokenPatten.AreaPathPermission
                    $useRegexVariable   = $false
                    break
                }
                default { throw "Token '$Token' is not recognized." }
            }
        }

        'Iteration' {
            switch -regex ($Token.Trim())
            {
                $LocalizedDataAzACLTokenPatten.IterationPathPermission {
                    $result.type        = 'IterationPathPermission'
                    $result.Identifiers = & $extractIdentifiers $Token $LocalizedDataAzACLTokenPatten.IterationPathPermission
                    $useRegexVariable   = $false
                    break
                }
                default { throw "Token '$Token' is not recognized." }
            }
        }

        'WorkItemQueryFolders' {
            switch -regex ($Token.Trim())
            {
                # The namespace root is a bare '$'. AzDoQueryPermission scans every ACL in
                # the namespace, so it meets this token on any organization - and throwing
                # here aborted the whole scan, which is what made every AzDoQueryPermission
                # integration test fail with "Token '$' is not recognized."
                $LocalizedDataAzACLTokenPatten.QueryRootPermission {
                    $result.type      = 'QueryRoot'
                    $useRegexVariable = $false
                    break
                }

                $LocalizedDataAzACLTokenPatten.QueryPermission {
                    $result.type      = 'QueryPermission'
                    $result.ProjectId = $matches.ProjectId

                    # As in New-ACLToken: the folder ids come from the remainder, because the
                    # project id is a GUID too and would otherwise be read as the first folder.
                    $remainder = $matches.Remainder
                    $result.Identifiers = @()

                    if (-not [String]::IsNullOrEmpty($remainder))
                    {
                        $folderMatches = [regex]::Matches($remainder, $LocalizedDataAzACLTokenPatten.QueryFolderIdentifier)
                        $result.Identifiers = @($folderMatches | ForEach-Object { @{ identifier = $_.Groups['identifiers'].Value } })
                    }

                    $useRegexVariable = $false
                    break
                }

                # Non-throwing, matching Project, Process, Build and Library: every
                # namespace whose resource enumerates org-wide ACLs has to tolerate a token
                # shape it does not model, or one unexpected entry fails the whole resource.
                default
                {
                    $result.type      = 'QueryUnknown'
                    $useRegexVariable = $false
                }
            }
        }

        'Project' {
            switch -regex ($Token.Trim())
            {
                $LocalizedDataAzACLTokenPatten.ProjectPermission { $result.type = 'Project';        break }
                default                                          { $result.type = 'ProjectUnknown'        }
            }
        }

        'Process' {
            switch -regex ($Token.Trim())
            {
                $LocalizedDataAzACLTokenPatten.ProcessRootPermission { $result.type = 'ProcessRoot'; break }
                $LocalizedDataAzACLTokenPatten.ProcessPermission     { $result.type = 'Process';     break }
                default                                              { $result.type = 'ProcessUnknown'     }
            }
        }

        'Build' {
            switch -regex ($Token.Trim())
            {
                $LocalizedDataAzACLTokenPatten.BuildPermission       { $result.type = 'Build';       break }
                $LocalizedDataAzACLTokenPatten.BuildFolderPermission { $result.type = 'BuildFolder'; break }
                default                                              { $result.type = 'BuildUnknown'       }
            }
        }

        'Library' {
            switch -regex ($Token.Trim())
            {
                $LocalizedDataAzACLTokenPatten.LibraryPermission { $result.type = 'Library';        break }
                default                                          { $result.type = 'LibraryUnknown'        }
            }
        }

        'ServiceEndpoints' {
            switch -regex ($Token.Trim())
            {
                $LocalizedDataAzACLTokenPatten.ServiceEndpointPermission { $result.type = 'ServiceEndpoints';        break }
                default                                                   { $result.type = 'ServiceEndpointsUnknown'       }
            }
        }

        'AgentPool' {
            switch -regex ($Token.Trim())
            {
                $LocalizedDataAzACLTokenPatten.AgentPoolPermission {
                    $result.type   = 'AgentPool'
                    $result.PoolId = $Token.Trim()
                    break
                }
                default { $result.type = 'AgentPoolUnknown' }
            }
        }

        'DistributedTask' {
            switch -regex ($Token.Trim())
            {
                $LocalizedDataAzACLTokenPatten.EnvironmentPermission { $result.type = 'Environment'; break }
                $LocalizedDataAzACLTokenPatten.AgentPoolPermission   { $result.type = 'AgentPool';   break }
                default                                              { $result.type = 'DistributedTaskUnknown' }
            }
        }

        default {
            # Generic / pass-through for any namespace not explicitly handled above.
            Write-Warning "[Parse-ACLToken] Security Namespace '$SecurityNamespace' is not natively recognised — using generic token."
            $result.type       = 'Generic'
            $result.TokenValue = $Token
        }
    }

    # Populate result with named capture groups from the automatic $Matches variable.
    if ($useRegexVariable) {
        $Matches.Keys | Where-Object { $_.Length -gt 1 } | ForEach-Object {
            $result."$_" = $Matches."$_"
        }
    }

    $result._token = $Token

    return $result
}