Private/ConvertFrom-AzAuthorizationError.ps1

function ConvertFrom-AzAuthorizationError {
    <#
    .SYNOPSIS
        Parses an Azure Resource Manager AuthorizationFailed error into actions + scope.
    .DESCRIPTION
        Internal. Azure (ARM) is the only one of the four supported platforms
        whose authorization error reliably names the missing action and scope,
        e.g.:
 
          "The client 'x' with object id 'y' does not have authorization to
           perform action 'Microsoft.Storage/storageAccounts/write' over scope
           '/subscriptions/.../resourceGroups/rg' or the scope is invalid."
 
        or the LinkedAuthorizationFailed / structured-error variants that carry
        an explicit 'action'/'scope' field. This helper extracts every action and
        scope it can find from either an ErrorRecord, an Exception, or a raw
        string. Graph / Fabric / Purview errors do NOT carry this detail, which is
        why only the Azure provider calls this.
    .PARAMETER InputObject
        An ErrorRecord, Exception, or string containing the ARM error text.
    .OUTPUTS
        PSCustomObject: Actions [string[]], Scopes [string[]], IsAuthorizationError [bool],
        Message [string].
    #>

    [CmdletBinding()]
    [OutputType([psobject])]
    param(
        [Parameter(Mandatory, ValueFromPipeline)]
        [AllowNull()]
        [object]$InputObject
    )

    process {
        $text = switch ($InputObject) {
            { $_ -is [System.Management.Automation.ErrorRecord] } {
                $parts = @()
                if ($_.Exception) { $parts += $_.Exception.Message }
                if ($_.ErrorDetails -and $_.ErrorDetails.Message) { $parts += $_.ErrorDetails.Message }
                $parts -join "`n"
            }
            { $_ -is [System.Exception] } { $_.Message }
            default { [string]$_ }
        }

        if ([string]::IsNullOrWhiteSpace($text)) {
            return [pscustomobject]@{
                PSTypeName           = 'PSAutoRBAC.ParsedError'
                Actions              = @()
                Scopes               = @()
                IsAuthorizationError = $false
                Message              = ''
            }
        }

        $isAuth = $text -match 'AuthorizationFailed|does not have authorization to perform action|LinkedAuthorizationFailed|RoleAssignmentDoesNotExist'

        # 1) Prose form: ...perform action 'Provider/op/...'...
        $actions = @()
        $proseActions = [regex]::Matches($text, "perform action '([^']+)'")
        foreach ($m in $proseActions) { $actions += $m.Groups[1].Value }

        # 2) Structured form: "action": "Provider/op" (ARM error JSON) and DataAction variants.
        $jsonActions = [regex]::Matches($text, '"(?:action|dataAction)"\s*:\s*"([^"]+)"')
        foreach ($m in $jsonActions) { $actions += $m.Groups[1].Value }

        # Scopes: prose "over scope '...'" and structured "scope": "...".
        $scopes = @()
        foreach ($m in [regex]::Matches($text, "over scope '([^']+)'")) { $scopes += $m.Groups[1].Value }
        foreach ($m in [regex]::Matches($text, '"scope"\s*:\s*"([^"]+)"'))  { $scopes += $m.Groups[1].Value }

        $uniqueActions = @($actions | Where-Object { $_ } | Select-Object -Unique)
        $uniqueScopes  = @($scopes  | Where-Object { $_ } | Select-Object -Unique)
        Write-PSFMessage -Level Debug -Message "Parsed authorization error (IsAuthError=$isAuth, actions=[$($uniqueActions -join ', ')], scopes=[$($uniqueScopes -join ', ')])." -Tag 'PSAutoRBAC', 'ErrorParse'

        [pscustomobject]@{
            PSTypeName           = 'PSAutoRBAC.ParsedError'
            Actions              = $uniqueActions
            Scopes               = $uniqueScopes
            IsAuthorizationError = [bool]$isAuth
            Message              = $text.Trim()
        }
    }
}