Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/OrganizationSettings/Get-DevOpsOrganizationPolicy.ps1

<#
.SYNOPSIS
Reads the Azure DevOps organization policies.
 
.DESCRIPTION
The *Organization settings -> Policies* page reads the policies from the
`ms.vss-org-web.collection-admin-policy-data-provider` data provider, and so does this function.
 
The data provider is queried through `_apis/Contribution/HierarchyQuery`. If that returns no policy
data, the function falls back to the page's own data route
(`_settings/organizationPolicy?__rt=fps&__ver=2`). Both return the policies grouped by page section
(`applicationConnection`, `security`, `user`, ...), each entry carrying a `policy` object with a
`name` and a `value` (and usually an `effectiveValue`). The groups are flattened, so the caller
gets one policy object per policy, exactly as the service returned it.
 
Both of those are the web page's routes, and can answer a service principal or managed identity
with no policy data at all. When they do and `PolicyName` is given, each named policy is read from
the policy API (`_apis/OrganizationPolicy/Policies/{name}`) as a last resort, on the organization
host and then on its SPS host (`vssps.dev.azure.com`). That GET needs a `defaultValue` query
parameter - the value to report for a policy that was never set - and answers 405 without one. If
every route comes back empty, the error says what each one returned.
 
.PARAMETER ApiUri
The base organization URI, e.g. 'https://dev.azure.com/myorg/'.
 
.PARAMETER PolicyName
Optional. Returns only the policies with these names, e.g. 'Policy.LogAuditEvents'. Required for the
per-policy fallback route.
 
.PARAMETER DefaultValue
Optional. Policy name to the value the per-policy route reports for a policy that was never set.
A policy not listed is read with 'false'.
 
.PARAMETER ApiVersion
The REST API version used for the HierarchyQuery call. Defaults to '5.0-preview.1'.
 
.EXAMPLE
Get-DevOpsOrganizationPolicy -ApiUri 'https://dev.azure.com/myorg/'
 
.EXAMPLE
Get-DevOpsOrganizationPolicy -ApiUri 'https://dev.azure.com/myorg/' -PolicyName 'Policy.LogAuditEvents'
#>

function Get-DevOpsOrganizationPolicy
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string]$ApiUri,
        [Parameter()][string[]]$PolicyName,
        [Parameter()][hashtable]$DefaultValue = @{},
        [Parameter()][string]$ApiVersion = '5.0-preview.1'
    )

    $dataProviderId = 'ms.vss-org-web.collection-admin-policy-data-provider'
    $baseUri        = $ApiUri.TrimEnd('/')
    $failures       = @()
    $groups         = $null
    $policies       = $null

    # First choice: the contribution data provider API.
    $hierarchyQuery = @{
        contributionIds     = @($dataProviderId)
        dataProviderContext = @{
            properties = @{
                sourcePage = @{
                    url         = '{0}/_settings/organizationPolicy' -f $baseUri
                    routeId     = 'ms.vss-admin-web.collection-admin-hub-route'
                    routeValues = @{
                        adminPivot = 'organizationPolicy'
                        controller = 'ContributedPage'
                        action     = 'Execute'
                    }
                }
            }
        }
    }

    try
    {
        $params = @{
            Uri    = '{0}/_apis/Contribution/HierarchyQuery?api-version={1}' -f $baseUri, $ApiVersion
            Method = 'POST'
            Body   = $hierarchyQuery | ConvertTo-Json -Depth 10
        }
        $response = Invoke-AzDevOpsApiRestMethod @params
        $groups   = $response.dataProviders.$dataProviderId.policies

        if ($null -eq $groups)
        {
            # Say why, so a failure names the cause rather than just "no data".
            $exception = $response.dataProviderExceptions.$dataProviderId
            $failures += if ($null -ne $exception) {
                "HierarchyQuery: the data provider failed: $($exception.message)"
            } else {
                "HierarchyQuery: no policy data (data providers returned: $(@($response.dataProviders.PSObject.Properties.Name) -join ', '))"
            }
        }
    }
    catch
    {
        $failures += "HierarchyQuery: $_"
    }

    # Fallback: the data route the settings page itself loads.
    if ($null -eq $groups)
    {
        try
        {
            $params = @{
                Uri    = '{0}/_settings/organizationPolicy?__rt=fps&__ver=2' -f $baseUri
                Method = 'GET'
            }
            $response = Invoke-AzDevOpsApiRestMethod @params
            $groups   = $response.fps.dataProviders.data.$dataProviderId.policies

            if ($null -eq $groups)
            {
                $failures += if ($response -is [string]) {
                    'settings page data: a non-JSON response (a sign-in page answers an identity the page does not accept)'
                } else {
                    "settings page data: no policy data (response carried: $(@($response.PSObject.Properties.Name) -join ', '))"
                }
            }
        }
        catch
        {
            $failures += "settings page data: $_"
        }
    }

    if ($null -ne $groups)
    {
        $policies = foreach ($group in $groups.PSObject.Properties)
        {
            foreach ($entry in @($group.Value))
            {
                if ($null -ne $entry.policy) { $entry.policy }
            }
        }
    }
    elseif ($PolicyName.Count -gt 0)
    {
        # Last resort: read each policy from the policy API, on the organization host and then on
        # the SPS host. The GET needs defaultValue; without it the service answers 405. All or
        # nothing, so a failure keeps every route's reason.
        $spsUri    = $baseUri -replace '^https://dev\.azure\.com/', 'https://vssps.dev.azure.com/'
        $readFails = @()
        $policies  = foreach ($name in $PolicyName)
        {
            $default = if ($DefaultValue.ContainsKey($name)) { [string]$DefaultValue[$name] } else { 'false' }
            $policy  = $null
            $reasons = @()

            foreach ($hostUri in @($baseUri, $spsUri | Select-Object -Unique))
            {
                try
                {
                    $params = @{
                        Uri    = '{0}/_apis/OrganizationPolicy/Policies/{1}?defaultValue={2}&api-version={3}' -f $hostUri, $name, [uri]::EscapeDataString($default), $ApiVersion
                        Method = 'GET'
                    }
                    $policy = Invoke-AzDevOpsApiRestMethod @params
                    break
                }
                catch
                {
                    $reasons += "$($hostUri): $_"
                }
            }

            if ($null -eq $policy)
            {
                $readFails += "policy API read ($name): $($reasons -join ' | ')"
                continue
            }

            if ($null -eq $policy.PSObject.Properties['name'])
            {
                $policy | Add-Member -NotePropertyName name -NotePropertyValue $name
            }
            $policy
        }

        if ($readFails.Count -gt 0)
        {
            $failures += $readFails
            $policies  = $null
        }
    }

    if ($null -eq $policies)
    {
        $reason = if ($failures.Count -gt 0) { $failures -join '; ' } else { 'the response carried no policy data' }
        Throw "[Get-DevOpsOrganizationPolicy] Failed to retrieve organization policies: $reason"
    }

    if ($PSBoundParameters.ContainsKey('PolicyName'))
    {
        $policies = $policies | Where-Object { $_.name -in $PolicyName }
    }

    return $policies
}