Private/Resolve-GuacContextUrl.ps1

#Requires -Version 5.1
function Resolve-GuacSessionContext {
    <#
    .SYNOPSIS
        Resolves the session, server, and data source for a UserContext REST call.

    .DESCRIPTION
        Implements the shared -Session/-Server resolution used by every
        entity cmdlet (the same pattern as Remove-GuacSession and
        Test-GuacSession): an explicit [N2C_GuacAdmin_GuacSession] (also
        accepted from the pipeline) wins; otherwise the module default
        session for -Server is used. When neither -Session nor -Server is
        bound (for example when only the entity object was piped) and exactly
        one default session is registered in this process, that session is
        used.

        Returns an ordered hashtable with the resolved values:
        - Session : the [N2C_GuacAdmin_GuacSession] to use
        - Server : the normalized server base URL
        - Token : the authentication token (masked nowhere; carried
                         only for the transport)
        - DataSource : the data source (AuthenticationProvider identifier)
                         to scope the UserContext path to; defaults to
                         $session.DataSource

        Throws a terminating [N2C_GuacAdmin_GuacRestException] when no
        session can be resolved, when the session carries no token, or when
        an explicit -DataSource override is not among the session's
        available data sources.

        Reference (Apache Guacamole 1.6.0):
        - guacamole/src/main/java/org/apache/guacamole/rest/session/SessionResource.java
          (getUserContextResource: /session/data/{dataSource})
        - guacamole/src/main/java/org/apache/guacamole/rest/auth/APIAuthenticationResult.java
          (dataSource, availableDataSources from POST /api/tokens)

        This function is private to the N2C.GuacAdmin module.
    #>

    [CmdletBinding()]
    [OutputType([hashtable])]
    param (
        [Parameter(Mandatory = $false)]
        [AllowNull()]
        [object] $Session,

        [Parameter(Mandatory = $false)]
        [AllowEmptyString()]
        [string] $Server = [string]::Empty,

        [Parameter(Mandatory = $false)]
        [AllowEmptyString()]
        [string] $DataSource = [string]::Empty,

        [Parameter(Mandatory = $false)]
        [string] $CmdletName = [string]::Empty
    )

    $label = $CmdletName
    if ([string]::IsNullOrWhiteSpace($label)) { $label = 'this cmdlet' }

    $token = [string]::Empty
    if ($null -ne $Session) {
        $server = [string]$Session.Server
        $token = [string]$Session.Token
    }
    elseif (-not [string]::IsNullOrWhiteSpace($Server)) {
        $server = ConvertTo-GuacServerUrl -Server $Server
        $stateSession = Get-GuacSessionState -Server $server
        if ($null -eq $stateSession) {
            throw ($script:GuacRestExceptionType::new(
                ("No default GuacSession exists for server '{0}'. Create one with New-GuacSession or pass -Session." -f $server)
            ))
        }
        $Session = $stateSession
        $token = [string]$Session.Token
    }
    elseif ($script:GuacSessionState.Count -eq 1) {
        # Neither -Session nor -Server was bound (for example when only the
        # entity object was piped). Fall back to the single default session
        # registered in this process, which makes
        # Get-GuacUser -Id 'jdoe'
        # work after New-GuacSession without repeating -Session on every call
        # (the -CmsSession default-session pattern).
        $server = [string]$script:GuacSessionState.Keys[0]
        $Session = $script:GuacSessionState[$server]
        $token = [string]$Session.Token
    }
    else {
        throw ($script:GuacRestExceptionType::new(
            ('{0} requires a session: supply -Session or -Server.' -f $label)
        ))
    }

    if ([string]::IsNullOrWhiteSpace($token)) {
        throw ($script:GuacRestExceptionType::new(
            ("The session for server '{0}' carries no authentication token." -f $server)
        ))
    }

    if ([string]::IsNullOrWhiteSpace($DataSource)) {
            $DataSource = [string]$Session.DataSource
        }
        # Special case: -DataSource All means "iterate over all data sources"
        if ($DataSource -ieq 'All') {
            $available = @($Session.AvailableDataSources)
            if (-not $available -or $available.Count -eq 0) {
                throw ($script:GuacRestExceptionType::new(
                    ("The session for server '{0}' has no available data sources." -f $server)
                ))
            }
            # Return a special context that indicates "all" mode with the list of sources
            return [ordered]@{
                Session      = $Session
                Server       = $server
                Token        = $token
                DataSource   = 'All'
                DataSources  = $available
            }
        }
        $available = @($Session.AvailableDataSources)
        if ($available -and $available.Count -gt 0 -and $available -notcontains $DataSource) {
        throw ($script:GuacRestExceptionType::new(
            ("Data source '{0}' is not available for this user. Available: {1}" -f ($DataSource, ($available -join ', ')))
        ))
    }
    if ([string]::IsNullOrWhiteSpace($DataSource)) {
        throw ($script:GuacRestExceptionType::new(
            'The session carries no data source and no -DataSource was supplied.'
        ))
    }

    return [ordered]@{
        Session      = $Session
        Server       = $server
        Token        = $token
        DataSource   = $DataSource
    }
}

function Resolve-GuacContextUrl {
    <#
    .SYNOPSIS
        Builds a per-DataSource UserContext REST API path.

    .DESCRIPTION
        Produces the request path for the UserContext REST surface:
        "/api/session/data/{dataSource}/{collection}[/{id}][/{subpath}][?query]".

        Each segment is URL-escaped individually with [Uri]::EscapeDataString,
        which is required because Guacamole identifiers (connection UUIDs,
        usernames, group names) may legally contain characters such as "/"
        or "#". The collection name, the object identifier, and the literal
        subpath are therefore passed as separate parameters. A query string
        (without the leading "?") may be appended last; it is not escaped.

        Reference (Apache Guacamole 1.6.0):
        - guacamole/src/main/java/org/apache/guacamole/rest/session/SessionResource.java
          (getUserContextResource: @Path("data/{dataSource}"))
        - guacamole/src/main/java/org/apache/guacamole/rest/session/UserContextResource.java
          (@Path subresources: connections, connectionGroups, users, userGroups,
           sharingProfiles, history, schema)
        - guacamole/src/main/java/org/apache/guacamole/rest/directory/DirectoryResource.java
          (@Path("{identifier}") object resource)

        This function is private to the N2C.GuacAdmin module.
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param (
        [Parameter(Mandatory = $true)]
        [string] $DataSource,

        [Parameter(Mandatory = $false)]
        [AllowEmptyString()]
        [string] $Collection = [string]::Empty,

        [Parameter(Mandatory = $false)]
        [AllowEmptyString()]
        [string] $Id = [string]::Empty,

        [Parameter(Mandatory = $false)]
        [AllowEmptyString()]
        [string] $SubPath = [string]::Empty,

        [Parameter(Mandatory = $false)]
        [AllowEmptyString()]
        [string] $Query = [string]::Empty
    )

    if ([string]::IsNullOrWhiteSpace($DataSource)) {
        throw (New-Object System.ArgumentException(
            'The data source must be a non-empty AuthenticationProvider identifier.', 'DataSource'))
    }

    $url = '/api/session/data/' + [Uri]::EscapeDataString($DataSource)
    if (-not [string]::IsNullOrWhiteSpace($Collection)) {
        $url += '/' + [Uri]::EscapeDataString($Collection)
    }
    if (-not [string]::IsNullOrWhiteSpace($Id)) {
        $url += '/' + [Uri]::EscapeDataString($Id)
    }
    if (-not [string]::IsNullOrWhiteSpace($SubPath)) {
        $sub = $SubPath
        if ($sub.StartsWith('/')) { $sub = $sub.Substring(1) }
        if ($sub.EndsWith('/')) { $sub = $sub.TrimEnd('/') }
        $url += '/' + $sub
    }
    if (-not [string]::IsNullOrWhiteSpace($Query)) {
        $url += '?' + $Query
    }
    return $url
}