Private/Resolve-ServiceUrl.ps1

function Resolve-ServiceUrl {
    <#
    .SYNOPSIS
    Resolves a service URL and the CyberArk Identity URL from platform discovery.

    .DESCRIPTION
    Given an ISPSS shared services subdomain, or a URL for one of the shared services, queries
    CyberArk platform discovery (via Find-SharedServicesURL) once and returns the URLs a companion
    module's Connect- command needs:

      - ServiceUrl - the api URL of the requested service, with any trailing /api and trailing
                      slash removed, so a caller can append its own /<path> unconditionally.
      - IdentityUrl - the 'identity_user_portal' service api URL (the CyberArk Identity tenant URL
                      that New-IDSession / New-IDPlatformToken authenticate against).
      - ServicePath - the path discarded from the service URL by BaseUrlOnly, otherwise empty.

    When a URL is supplied, Find-SharedServicesURL derives the shared services subdomain from the
    first label of the host name - correct for the standard https://<subdomain>.<service>.cyberark.cloud
    URL form.

    Throws if the requested service is absent from the discovery response - a service which is not
    enabled on the tenant, or a mistyped key, would otherwise yield a null ServiceUrl and leave the
    calling Connect- command holding a session bound to no URL at all.

    .PARAMETER Service
    The platform discovery key of the service to resolve, for example 'sca' or 'jit'.

    .PARAMETER Subdomain
    The ISPSS shared services subdomain.

    .PARAMETER Url
    A service URL to derive the shared services subdomain from.

    .PARAMETER BaseUrlOnly
    Return only the scheme and host of the service URL, with any path reported separately as
    ServicePath. Needed by services which publish a path along with the host - 'alerong' (Remote
    Access) publishes https://api.alero.io/internal/ra/v1/<tenantId>, where the API base URL is the
    host and the trailing segment is the tenant id.

    .EXAMPLE
    Resolve-ServiceUrl -Service sca -Subdomain sometenant

    .EXAMPLE
    Resolve-ServiceUrl -Service jit -Url https://sometenant.dpa.cyberark.cloud
    #>

    [OutputType([pscustomobject])]
    [CmdletBinding(DefaultParameterSetName = 'Subdomain')]
    param(
        [parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$Service,

        [parameter(Mandatory = $true, ParameterSetName = 'Subdomain')]
        [ValidateNotNullOrEmpty()]
        [string]$Subdomain,

        [parameter(Mandatory = $true, ParameterSetName = 'URL')]
        [ValidateNotNullOrEmpty()]
        [string]$Url,

        [parameter(Mandatory = $false)]
        [switch]$BaseUrlOnly
    )

    $Reference = if ($PSCmdlet.ParameterSetName -eq 'Subdomain') { $Subdomain } else { $Url }

    try {

        $Discovery = if ($PSCmdlet.ParameterSetName -eq 'Subdomain') {
            Find-SharedServicesURL -subdomain $Subdomain
        } else {
            Find-SharedServicesURL -url $Url
        }

    } catch {

        throw "Unable to resolve CyberArk shared services URLs from '$Reference': $($PSItem.Exception.Message)"

    }

    $IdentityUrl = $Discovery.identity_user_portal.api -replace '/$', ''

    if ([string]::IsNullOrEmpty($IdentityUrl)) {
        throw "CyberArk Identity URL (identity_user_portal) not found in platform discovery response for '$Reference'"
    }

    $ServiceApi = $Discovery.$Service.api
    $ServicePath = ''

    if ($BaseUrlOnly -and (-not [string]::IsNullOrEmpty($ServiceApi))) {

        $Builder = [System.UriBuilder]::new($ServiceApi)
        $ServiceHost = $Builder.Host
        $ServicePath = $Builder.Path -replace '/$', ''

        #alerong has been observed publishing the host and path concatenated without their separating
        #slash - https://api.alero.iointernal/ra/v1/<tenantId> - which a plain parse reads as the host
        #'api.alero.iointernal'. Recover the authority at the TLD boundary and hand the leaked text
        #back to the path. The path charset excludes a dot, so a legitimate host cannot match.
        if ($ServiceHost -match '^(?<hostname>.+\.(?:io|com|cloud|net))(?<leaked>[a-z0-9-]+)$') {

            Write-Verbose "Discovery returned a malformed URL for '$Service': recovered host '$($Matches['hostname'])' from '$ServiceHost'"
            $ServicePath = "/$($Matches['leaked'])$ServicePath"
            $ServiceHost = $Matches['hostname']

        }

        $Builder.Host = $ServiceHost
        $Builder.Path = ''
        $ServiceUrl = $Builder.Uri.AbsoluteUri -replace '/$', ''

    } else {

        #Strip a trailing /api, then any trailing slash: services vary in whether they publish either,
        #and callers always append their own /<path>. cem and cds publish a bare host with a trailing slash.
        $ServiceUrl = $ServiceApi -replace '/api/?$', '' -replace '/$', ''

    }

    if ([string]::IsNullOrEmpty($ServiceUrl)) {
        throw "URL for the '$Service' service not found in platform discovery response for '$Reference'. The service may not be enabled on this tenant"
    }

    [pscustomobject]@{
        ServiceUrl  = $ServiceUrl
        IdentityUrl = $IdentityUrl
        ServicePath = $ServicePath
    }

}