Invoke-MetasysMethod.psm1

function Invoke-MetasysMethod {
    <#
    .SYNOPSIS
        Invokes methods of the Metasys REST API

    .DESCRIPTION
        This function allows you to invoke various methods of the Metasys REST API.
        Once a session is established (on the first invocation) the session state
        is maintained in the terminal session. This allows you to make additional
        calls with less boilerplate text for each call.

    .OUTPUTS
        The payloads from Metasys as Powershell objects.

    .EXAMPLE
        Invoke-MetasysMethod -Reference thesun:thesun

        This will prompt you for a hostname and your credentials and then attempt
        to look up the id of the object with the specified reference

    .EXAMPLE
        Invoke-MetasysMethod -Path /objects/$id

        After retrieving an id of an object (like in the previous example) and
        string it in variable $id, this example will read the default view of the
        object.

    .EXAMPLE
        Invoke-MetasysMethod -Path /alarms

        This will read the first page of alarms from the site.

    .EXAMPLE
        Invoke-MetasysMethod -Method Put -Path /objects/$id/commands/adjust -Body '[72.5]'

        This example will send the adjust command to the specified object (assuming
        a valid id is stored in $id).

    .LINK

        https://github.jci.com/cwelchmi/metasys-powershell-tutorial/blob/main/invoke-metasys-method.md

    #>


    [CmdletBinding(PositionalBinding = $false)]
    param(
        # The hostname or ip address of the site you wish to interact with
        [string]$SiteHost,
        # The username of the account you wish to use on this Site
        [string]$UserName,
        # A switch used to force Login. This isn't normally needed except
        # when you wish to switch accounts or switch sites. By using this
        # switch you will be prompted for the site or your credentials if
        # not supplied on the command line.
        [switch]$Login,
        # The relative path to an endpont. For example: /alarms
        # All of the relative paths are listed in the API Documentation
        # Path and Reference are mutally exclusive.
        [string]$Path,
        # Session information is stored in environment variables. To force a
        # cleanup use this switch to remove all environment variables. The next
        # time you invoke this function you'll need to provide credentials and
        # a SiteHost.
        [switch]$Clear,
        # The json payload to send with your request.
        [string]$Body,
        # The HTTP Method you are sending.
        [string]$Method = "Get",
        # The version of the API you intent to use
        [Int]$Version = 3,
        # NOTE: Insecure. DO NOT use in production. This switch will cause
        # all checks of the certificate to be skipped.
        [switch]$SkipCertificateCheck,
        # A short cut for looking up the id of an object.
        [string]$Reference,
        # Rather than just returning the content, return the full web response
        # object will include extra like the response headers.
        [switch]$FullWebResponse
    )

    class MetasysEnvVars {
        static [string] getSiteHost() {
            return $env:METASYS_SITE_HOST
        }

        static [void] setSiteHost([string]$siteHost) {
            $env:METASYS_SITE_HOST = $siteHost
        }

        static [int] getVersion() {
            return $env:METASYS_VERSION
        }

        static [void] setVersion([int]$version) {
            $env:METASYS_VERSION = $version
        }

        static [string] getExpires() {
            return $env:METASYS_EXPIRES
        }

        static [void] setExpires([string]$expires) {
            $env:METASYS_EXPIRES = $expires
        }

        static [string] getToken() {
            return $env:METASYS_SECURE_TOKEN
        }

        static [void] setToken([string]$token) {
            $env:METASYS_SECURE_TOKEN = $token
        }

        static [string] getLast() {
            return $env:METASYS_LAST_RESPONSE
        }

        static [void] setLast([string]$last) {
            $env:METASYS_LAST_RESPONSE = $last
        }

        static [void] clear() {
            $env:METASYS_SECURE_TOKEN = $null
            $env:METASYS_SITE_HOST = $null
            $env:METASYS_VERSION = $null
            $env:METASYS_LAST_RESPONSE = $null
            $env:METASYS_EXPIRES = $null
        }

        static [System.Boolean] getDefaultSkipCheck() {
            return $env:METASYS_SKIP_CHECK_NOT_SECURE
        }

    }

    Set-StrictMode -Version 2

    # In Windows Powershell the $IsMacOS variable doesn't exist so calls to check
    # it's value fail. This function wraps the call.

    function isMacOS {
        if ($PSVersionTable.PSEdition -eq "Core") {
            return $IsMacOS
        }

        return $False
    }

    function buildUri {
        param (
            [string]$siteHost = [MetasysEnvVars]::getSiteHost(),
            [int]$version = [MetasysEnvVars]::getVersion(),
            [string]$baseUri = "api",
            [string]$path
        )

        $uri = [System.Uri] ("https://" + $siteHost + "/" + ([System.IO.Path]::Join($baseUri, "v" + $version, $path)))
        return $uri
    }

    function buildRequest {
        param (
            [string]$method = "Get",
            [string]$uri,
            [string]$body = $null,
            [string]$token = [MetasysEnvVars]::getToken()
        )

        return @{
            Method               = $method
            Uri                  = buildUri -path $Path
            Body                 = $body
            Authentication       = "bearer"
            Token                = ConvertTo-SecureString $token
            SkipCertificateCheck = $SkipCertificateCheck
            ContentType          = "application/json"
        }
    }

    function executeRequest {
        param (
            [string]$method = "Get",
            [string]$uri,
            [string]$body = $null,
            [string]$token = [MetasysEnvVars]::getToken()
        )

        return Invoke-RestMethod @(buildRequest -uri (buildUri -path $Path) -method $Method -body $Body)
    }

    function find-internet-user {
        param (
            [string]$siteHost
        )

        if (!(isMacOS)) {
            return
        }

        $cred = Invoke-Expression "security find-internet-password -s $siteHost 2>/dev/null"
        if ($cred) {
            $userNameLine = $cred | Where-Object { $_.StartsWith(" ""acct") }
            if ($userNameLine) {
                $userName = $userNameLine.Split('=')[1].Trim('"')
                return $userName
            }
        }
    }

    function find-internet-password {
        param (
            [string]$siteHost,
            [string]$userName
        )

        if (!(isMacOS)) {
            return
        }

        $passwordEntry = Invoke-Expression "security find-internet-password -s $siteHost -a $userName -w 2>/dev/null"
        if ($passwordEntry) {
            return ConvertTo-SecureString $passwordEntry -AsPlainText
        }
    }

    function add-internet-password {
        param(
            [string]$siteHost,
            [string]$userName,
            [SecureString]$password
        )

        if (!(isMacOS)) {
            return
        }

        $plainText = ConvertFrom-SecureString -SecureString $password -AsPlainText

        Invoke-Expression "security add-internet-password -U -s $siteHost -a $userName -w $plainText -c mgw1 "

    }

    If (($Version -lt 2) -or ($Version -gt 3)) {
        Write-Error -Message "Version out of range. Should be 2 or 3"
        return
    }

    if ($Clear.IsPresent) {
        [MetasysEnvVars]::clear()
        return # end the program
    }

    if (!$SkipCertificateCheck.IsPresent) {
        $SkipCertificateCheck =[MetasysEnvVars]::getDefaultSkipCheck()
    }

    # Login Region

    $ForceLogin = $false

    if ([MetasysEnvVars]::getExpires()) {
        $expiration = [Datetime]::Parse([MetasysEnvVars]::getExpires())
        if ([Datetime]::now -gt $expiration) {
            # Token is expired, require login
            $ForceLogin = $true
        }
        else {
            # attempt to renew the token to keep it fresh
            $refreshRequest = @{
                Method               = "Get"
                Uri                  = buildUri -path "/refreshToken"
                Authentication       = "bearer"
                Token                = ConvertTo-SecureString -String ([MetasysEnvVars]::getToken())
                SkipCertificateCheck = $SkipCertificateCheck
            }
            try {
                $refreshResponse = Invoke-RestMethod @refreshRequest
                [MetasysEnvVars]::setExpires($refreshResponse.expires)
                [MetasysEnvVars]::setToken( (ConvertFrom-SecureString -SecureString $refreshResponse.accessToken) )

            }
            catch {
                # refreshing doesn't seem to work
            }


        }
    }

    if (($Login) -or (![MetasysEnvVars]::getToken()) -or ($ForceLogin) -or ($SiteHost -and ($SiteHost -ne [MetasysEnvVars]::getSiteHost()))) {
        if (!$SiteHost) {
            $SiteHost = Read-Host -Prompt "Site host"
        }

        if (!$UserName) {
            # attempt to find a user name in keychain
            $UserName = find-internet-user($SiteHost)

            if (!$UserName) {
                $UserName = Read-Host -Prompt "UserName"
            }
        }

        $password = find-internet-password $SiteHost $UserName

        if (!$password) {
            $password = Read-Host -Prompt "Password" -AsSecureString

            ## Attempt to store credentials
            add-internet-password $SiteHost  $UserName  $password
        }

        $jsonObject = @{
            username = $UserName
            password = ConvertFrom-SecureString -SecureString $password -AsPlainText
        }
        $json = (ConvertTo-Json $jsonObject)

        $loginRequest = @{
            Method               = "Post"
            Uri                  = buildUri -siteHost $SiteHost -version $Version -path "login"
            Body                 = $json
            ContentType          = "application/json"
            SkipCertificateCheck = $SkipCertificateCheck
        }

        try {
            $loginResponse = Invoke-RestMethod @loginRequest
            $secureToken = ConvertTo-SecureString -String $loginResponse.accessToken -AsPlainText
            [MetasysEnvVars]::setToken((ConvertFrom-SecureString -SecureString $secureToken))
            [MetasysEnvVars]::setSiteHost($SiteHost)
            [MetasysEnvVars]::setExpires($loginResponse.expires)
            [MetasysEnvVars]::setVersion($Version)
        }
        catch {
            Write-Host "An error occurred:"
            Write-Host $_
            return
        }
    }

    if ($Path -and $Reference) {
        Write-Warning "-Path and -Reference are mutually exclusive"
        return
    }

    if (!$Path -and !$Reference) {
        return
    }

    if ($Reference) {
        $Path = "/objectIdentifiers?fqr=" + $Reference
        $request = buildRequest -uri (buildUri -path $Path)
    }

    if ($Path) {
        $request = buildRequest -uri (buildUri -path $Path) -method $Method -body $Body
    }

    $responseObject = Invoke-WebRequest @request
    $response = $null
    if ($FullWebResponse.IsPresent) {
        $response = $responseObject
    }
    else {
        if ($responseObject -and $responseObject.Content) {
            $response = ConvertFrom-Json ([String]::new($responseObject.Content))
        }
    }
    Write-Verbose ("Http Status: " + $responseObject.StatusCode)
    [MetasysEnvVars]::setLast((ConvertTo-Json $response -Depth 15))
    return $response

}

function Invoke-MetasysGet {
    param([Parameter(Position = 0)][string]$Path)
    Invoke-MetasysMethod -Path $Path
}
function Invoke-MetasysPatch {
    param([Parameter(Position = 0)][string]$Path, [Parameter(Position = 1)][string]$Body)
    Invoke-MetasysMethod -Method Patch -Body $Body -Path $Path
}

function Invoke-MetasysPut {
    param([Parameter(Position = 0)][string]$Path, [Parameter(Position = 1)][string]$Body)
    Invoke-MetasysMethod -Method Put -Body $Body -Path $Path
}

function Invoke-MetasysGetObject {
    <#
    .SYNOPSIS
        Reads the default view of a Metasys object

    .DESCRIPTION
        This function allows you to read a Metasys object given the specified
        Reference. You will be prompted for a site host and your credentials unless a
        session has already been established.

    .OUTPUTS
        The object payload returned by the server.

    .EXAMPLE
        Invoke-MetasysGet-Object -Reference thesun:thesun

        This will prompt you for the Site host and your credentials and read the default view of this object.

    .LINK

        https://github.jci.com/cwelchmi/metasys-powershell-tutorial/blob/main/invoke-metasys-method.md

    #>

    param(
        # The fully qualified references of a Metasys object
        [Parameter(Position = 0)][string]$Reference
    )
    $id = [System.Environment]::GetEnvironmentVariable("idFor:" + $Reference)
    if (!$id) {
        $id = Invoke-MetasysGet -Path ("/objectIdentifiers?fqr=" + $Reference)
        [System.Environment]::SetEnvironmentVariable("idFor:" + $Reference, $id)
    }
    Invoke-MetasysGet ("/objects/" + $id)
}

New-Alias -Name mget -Value Invoke-MetasysGet
New-Alias -Name mpatch -Value Invoke-MetasysPatch
New-Alias -Name mput -Value Invoke-MetasysPut
New-Alias -Name mget-object -Value Invoke-MetasysGetObject

Export-ModuleMember -Function 'Invoke-MetasysMethod', 'Invoke-MetasysGet', 'Invoke-MetasysPut', 'Invoke-MetasysGetObject'
Export-ModuleMember -Alias 'mget', 'mpatch', 'mput', 'mget-object'