Private/Get-GuacEntityResponse.ps1
|
#Requires -Version 5.1 function Get-GuacMapEntries { <# .SYNOPSIS Enumerates the entries of a decoded Guacamole JSON map. .DESCRIPTION Guacamole directory listings are JSON objects keyed by identifier (DirectoryResource.getObjects returns Map<String, ExternalType>). Depending on the PowerShell version and the HTTP response decoder, such a body arrives either as a [System.Collections.IDictionary] (Windows PowerShell 5.1 ConvertFrom-Json) or as a [PSCustomObject] whose properties are the entries (PowerShell 7.x Invoke-WebRequest JSON decoding, or a single-element array unwrap). This helper accepts either shape and yields one [PSCustomObject] per entry, with the entry key in the "Key" property and the entry value in the "Value" property. A $null body yields nothing. This function is private to the N2C.GuacAdmin module. #> [CmdletBinding()] [OutputType([PSCustomObject])] param ( [Parameter(Position = 0)] [AllowNull()] [object] $Map ) if ($null -eq $Map) { return } if ($Map -is [System.Collections.IDictionary]) { foreach ($key in $Map.Keys) { $keyName = [string]$key Write-Output ([PSCustomObject]@{ Key = $keyName; Value = $Map[$key] }) } return } foreach ($prop in $Map.PSObject.Properties) { Write-Output ([PSCustomObject]@{ Key = $prop.Name; Value = $prop.Value }) } } function ConvertTo-GuacMapValue { <# .SYNOPSIS Normalizes a decoded Guacamole JSON value into a PS 5.1/7.x-compatible shape. .DESCRIPTION Depending on the PowerShell version and the HTTP response decoder, JSON objects arrive either as IDictionary (Windows PowerShell 5.1 ConvertFrom-Json) or as PSCustomObject (PowerShell 7.x Invoke-WebRequest). The PSCustomObject shape does not support string indexing ($map['key'] returns $null), which breaks callers such as $connection.Parameters['hostname'] and $permissionSet.ConnectionPermissions['conn-1']. This helper converts JSON objects into hashtables (string-indexable on both versions) and JSON arrays into object arrays whose elements are recursively normalized, up to $Depth. Scalars, $null, and values deeper than $Depth pass through unchanged. This function is private to the N2C.GuacAdmin module. #> [CmdletBinding()] [OutputType([object])] param ( [Parameter(Position = 0)] [AllowNull()] [object] $Value, [Parameter(Position = 1)] [int] $Depth = 8 ) if ($null -eq $Value) { return $null } if ($Depth -le 0) { return $Value } if ($Value -is [System.Collections.IDictionary]) { $map = @{} foreach ($key in $Value.Keys) { $map[[string]$key] = (ConvertTo-GuacMapValue -Value $Value[$key] -Depth ($Depth - 1)) } return $map } if ($Value -is [System.Management.Automation.PSCustomObject]) { $map = @{} foreach ($prop in $Value.PSObject.Properties) { $map[$prop.Name] = (ConvertTo-GuacMapValue -Value $prop.Value -Depth ($Depth - 1)) } return $map } if ($Value -is [System.Collections.IEnumerable] -and $Value -isnot [string]) { $items = @() foreach ($item in $Value) { $items += (ConvertTo-GuacMapValue -Value $item -Depth ($Depth - 1)) } return $items } return $Value } function Convert-GuacEpochToDateTime { <# .SYNOPSIS Converts a Unix epoch millisecond timestamp to a [DateTime] UTC. .DESCRIPTION The Guacamole REST API returns time values as Unix epoch milliseconds (e.g., 1790203199488). This helper converts such values to [DateTime] objects with Kind = Utc, which is PowerShell-idiomatic. Null values remain null; non-numeric values are returned unchanged. Reference: Guacamole uses java.sql.Timestamp which is inherently UTC. This function is private to the N2C.GuacAdmin module. #> [CmdletBinding()] [OutputType([DateTime])] param ( [Parameter(Position = 0)] [AllowNull()] [object] $EpochMillis ) if ($null -eq $EpochMillis) { return $null } # Already a DateTime? Return as-is if ($EpochMillis -is [DateTime]) { return $EpochMillis.ToUniversalTime() } # Try to parse as numeric (epoch milliseconds) $numeric = 0 if ([double]::TryParse($EpochMillis, [ref]$numeric)) { # Unix epoch: 1970-01-01 00:00:00 UTC $epoch = [DateTime]::new(1970, 1, 1, 0, 0, 0, [DateTimeKind]::Utc) return $epoch.AddMilliseconds($numeric) } # Return unchanged if not numeric return $EpochMillis } function Convert-GuacDurationToTimeSpan { <# .SYNOPSIS Converts a duration in seconds to a [TimeSpan]. .DESCRIPTION Guacamole history records include duration in seconds. This helper converts such values to [TimeSpan] for better readability. Null values remain null. This function is private to the N2C.GuacAdmin module. #> [CmdletBinding()] [OutputType([TimeSpan])] param ( [Parameter(Position = 0)] [AllowNull()] [object] $Seconds ) if ($null -eq $Seconds) { return $null } if ($Seconds -is [TimeSpan]) { return $Seconds } $numeric = 0.0 if ([double]::TryParse($Seconds, [ref]$numeric)) { return [TimeSpan]::FromSeconds($numeric) } return $Seconds } function Convert-GuacHistoryRecord { <# .SYNOPSIS Converts epoch timestamps in a Guacamole history record and computes duration. .DESCRIPTION Converts startDate and endDate (epoch ms) to [DateTime] UTC and computes a duration [TimeSpan] client-side from endDate - startDate (or now - startDate for active sessions). The Guacamole REST API does not return a duration field; it is derived here. This function is private to the N2C.GuacAdmin module. #> [CmdletBinding()] [OutputType([PSCustomObject])] param ( [Parameter(Position = 0, Mandatory = $true)] [object] $Record ) if ($null -eq $Record) { return $null } # Handle both PSCustomObject and hashtable inputs if ($Record -is [System.Collections.IDictionary]) { if ($Record.ContainsKey('startDate') -and $null -ne $Record['startDate']) { $Record['startDate'] = Convert-GuacEpochToDateTime -EpochMillis $Record['startDate'] } if ($Record.ContainsKey('endDate') -and $null -ne $Record['endDate']) { $Record['endDate'] = Convert-GuacEpochToDateTime -EpochMillis $Record['endDate'] } # Compute duration client-side (API does not return it) $start = $null $end = $null $active = $false if ($Record.ContainsKey('startDate')) { $start = $Record['startDate'] } if ($Record.ContainsKey('endDate')) { $end = $Record['endDate'] } if ($Record.ContainsKey('active')) { $active = [bool]$Record['active'] } if ($null -ne $start) { if ($null -ne $end) { $Record['duration'] = [TimeSpan]($end - $start) } elseif ($active) { $Record['duration'] = [TimeSpan]([DateTime]::UtcNow - $start) } else { $Record['duration'] = $null } } else { $Record['duration'] = $null } return $Record } else { foreach ($prop in $Record.PSObject.Properties) { switch ($prop.Name) { 'startDate' { if ($null -ne $prop.Value) { $prop.Value = Convert-GuacEpochToDateTime -EpochMillis $prop.Value } } 'endDate' { if ($null -ne $prop.Value) { $prop.Value = Convert-GuacEpochToDateTime -EpochMillis $prop.Value } } } } # Compute duration client-side (API does not return it) $start = $null $end = $null $active = $false foreach ($prop in $Record.PSObject.Properties) { switch ($prop.Name) { 'startDate' { $start = $prop.Value } 'endDate' { $end = $prop.Value } 'active' { $active = [bool]$prop.Value } } } if ($null -ne $start) { if ($null -ne $end) { $Record | Add-Member -NotePropertyName 'duration' -NotePropertyValue ([TimeSpan]($end - $start)) -Force } elseif ($active) { $Record | Add-Member -NotePropertyName 'duration' -NotePropertyValue ([TimeSpan]([DateTime]::UtcNow - $start)) -Force } else { $Record | Add-Member -NotePropertyName 'duration' -NotePropertyValue $null -Force } } else { $Record | Add-Member -NotePropertyName 'duration' -NotePropertyValue $null -Force } return $Record } } function Get-GuacEntityResponse { <# .SYNOPSIS Wraps a decoded Guacamole entity JSON body in a PSCustomObject with an Identifier. .DESCRIPTION The Guacamole REST API returns directory listings as a JSON object keyed by identifier (DirectoryResource.getObjects returns Map<String, ExternalType>) and single objects as a plain JSON object without their identifier (DirectoryObjectResource.getObject returns the ExternalType directly). This helper normalizes both into [PSCustomObject] instances that carry the original properties plus an "Identifier" property, so that entity objects can be piped to Update-/Remove- cmdlets via ValueFromPipelineByPropertyName. The "Identifier" value comes from the -Identifier parameter (the map key for listings, the path parameter for single objects) and takes precedence over any "identifier" property present in the body. A $null body (for example a 204-style empty response) returns $null. Reference (Apache Guacamole 1.6.0): - guacamole/src/main/java/org/apache/guacamole/rest/directory/DirectoryResource.java (getObjects: Map<String, ExternalType>) - guacamole/src/main/java/org/apache/guacamole/rest/directory/DirectoryObjectResource.java (getObject: ExternalType) This function is private to the N2C.GuacAdmin module. #> [CmdletBinding()] [OutputType([PSCustomObject])] param ( [Parameter(Position = 0)] [AllowNull()] [object] $Response, [Parameter(Position = 1)] [AllowEmptyString()] [string] $Identifier = [string]::Empty, [Parameter(Position = 2)] [AllowEmptyString()] [string] $DataSource = [string]::Empty, [Parameter(Position = 3)] [AllowEmptyString()] [string] $EntityType = [string]::Empty ) if ($null -eq $Response) { return $null } # Normalize the response so that nested JSON objects become string-indexable # hashtables on every PowerShell version (PSCustomObject, as produced by the # PowerShell 7.x JSON decoder, does not support $map['key'] indexing). $normalized = ConvertTo-GuacMapValue -Value $Response $props = [ordered]@{} if ($normalized -is [System.Collections.IDictionary]) { foreach ($key in $normalized.Keys) { $props[[string]$key] = $normalized[$key] } } else { foreach ($prop in $normalized.PSObject.Properties) { $props[$prop.Name] = $prop.Value } } if (-not [string]::IsNullOrWhiteSpace($Identifier)) { $props['Identifier'] = $Identifier } if (-not [string]::IsNullOrWhiteSpace($DataSource)) { $props['DataSource'] = $DataSource } # Convert epoch timestamps to [DateTime] UTC based on entity type if (-not [string]::IsNullOrWhiteSpace($EntityType)) { switch ($EntityType) { 'activeConnections' { # APIActiveConnection: startDate (epoch ms) if ($null -ne $props['startDate']) { $props['startDate'] = Convert-GuacEpochToDateTime -EpochMillis $props['startDate'] } } 'connections' { # APIConnection: lastActive (epoch ms). Always present; null if never used. if ($null -ne $props['lastActive']) { $props['lastActive'] = Convert-GuacEpochToDateTime -EpochMillis $props['lastActive'] } else { $props['lastActive'] = $null } } 'users' { # APIUser: lastActive (epoch ms). Always present; null if never used. if ($null -ne $props['lastActive']) { $props['lastActive'] = Convert-GuacEpochToDateTime -EpochMillis $props['lastActive'] } else { $props['lastActive'] = $null } } } } return [PSCustomObject]$props } function ConvertTo-GuacEntityBody { <# .SYNOPSIS Converts a user-supplied entity object into a JSON-serializable body. .DESCRIPTION Copies the properties of a PSCustomObject or hashtable into an ordered hashtable suitable for ConvertTo-GuacJson, optionally excluding named properties (for example "Identifier" when creating an object, where the server assigns the identifier). Nested PSCustomObject/hashtable values (for example a "parameters" map) are preserved and serialize correctly on both Windows PowerShell 5.1 and PowerShell 7.x. This function is private to the N2C.GuacAdmin module. #> [CmdletBinding()] [OutputType([hashtable])] param ( [Parameter(Position = 0)] [AllowNull()] [object] $Object, [Parameter(Position = 1)] [string[]] $ExcludeProperty = @() ) if ($null -eq $Object) { return [ordered]@{} } $props = [ordered]@{} if ($Object -is [System.Collections.IDictionary]) { foreach ($key in $Object.Keys) { $keyName = [string]$key if ($ExcludeProperty -contains $keyName) { continue } $props[$keyName] = $Object[$key] } } else { foreach ($prop in $Object.PSObject.Properties) { if ($ExcludeProperty -contains $prop.Name) { continue } $props[$prop.Name] = $prop.Value } } return $props } function Get-GuacIdentifier { <# .SYNOPSIS Extracts the identifier of a Guacamole entity object. .DESCRIPTION Returns the identifier carried by an entity object returned by the module's Get-* entity cmdlets, which is the "Identifier" property added by Get-GuacEntityResponse. Also falls back to the lowercase "identifier" property that some raw API objects carry, and to the "username" property for users (the APIUser external shape has no "identifier" field; the username is the user's identifier). Returns an empty string when the object carries no identifier. This function is private to the N2C.GuacAdmin module. #> [CmdletBinding()] [OutputType([string])] param ( [Parameter(Position = 0)] [AllowNull()] [object] $Object ) if ($null -eq $Object) { return [string]::Empty } # NOTE: property lookup is done by iterating and matching the name # (case-insensitive). Integer indexing into $Object.PSObject.Properties is # NOT used: it is a PSMemberInfoIntegratingCollection, and indexing it with # an int performs a name-match query that returns an empty PSPropertyInfo # (verified on pwsh 7.6), which silently yields $null values. foreach ($candidate in @('Identifier', 'identifier', 'username')) { $value = [string]::Empty if ($Object -is [System.Collections.IDictionary]) { foreach ($key in $Object.Keys) { if ([string]$key -ieq $candidate) { $value = [string]$Object[$key] break } } } else { foreach ($prop in $Object.PSObject.Properties) { if ($prop.Name -ieq $candidate) { $value = [string]$prop.Value break } } } if (-not [string]::IsNullOrWhiteSpace($value)) { return $value } } return [string]::Empty } |