lib/TMD.TaskAction.ps1


Function Invoke-TMActionRequest {
    <#
    .SYNOPSIS
        This function accepts a TransitionManager Action Request and queues it for execution


    .NOTES
        Name: Invoke-TMActionRequest
        Author: TransitionManager
        Version: 1.0
        DateCreated: 2021-04-05


    .EXAMPLE
        Invoke-TMActionRequest -ActionRequestB64 $Base64EncodedActionRequest


    .LINK
        https://support.transitionmanager.net
    #>


    [CmdletBinding()]
    param(
        [Parameter(
            Mandatory = $false,
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true,
            Position = 0
        )]
        [string]  $ActionRequestB64,
        [Parameter(
            Mandatory = $false,
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true,
            Position = 1
        )]
        [string]  $ScriptBlockString,
        [Parameter(
            Mandatory = $false,
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true,
            Position = 2
        )]
        [pscredential]  $Credential,
        [Parameter(
            Mandatory = $false,
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true,
            Position = 3
        )]
        [pscustomobject]$Params

    )

    BEGIN {}

    PROCESS {
        try {

            ## Import the ActionRequest from the B64 encoded argument
            $ActionRequest = Import-TMDActionRequest -FromB64 $ActionRequestB64 -SkipProviderLoad -PassThru

            ## Create a timestamp for the temp file name
            $Timestamp = [Math]::Round((Get-Date).ToFileTime() / 10000)

            ## Write the ActionRequest to the Queue folder
            $TempFileName = Join-Path $userPaths.queue ($Timestamp.ToString() + '.tmdar')
            Export-Clixml -InputObject $ActionRequest -Path $TempFileName

        } catch {
            throw $_
        }
    }


    END {}
}
Function Invoke-TMDebugActionRequest {
    <#
    .SYNOPSIS
        This function accepts a TransitionManager Action Request and queues it for execution


    .NOTES
        Name: Invoke-TMActionRequest
        Author: TransitionManager
        Version: 1.0
        DateCreated: 2021-04-05


    .EXAMPLE
        Invoke-TMActionRequest -ActionRequestB64 $Base64EncodedActionRequest


    .LINK
        https://support.transitionmanager.net
    #>


    [CmdletBinding()]
    param(
        [Parameter(
            Mandatory = $false,
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true,
            Position = 0
        )]
        [string]  $ActionRequestFilePath
    )

    BEGIN {}

    PROCESS {

        ## Setup an apropriate quiet environment
        $global:InformationPreference = 'Continue'
        $global:ProgressPreference = 'SilentlyContinue'
        $global:VerbosePreference = 'SilentlyContinue'
        $global:DebugPreference = 'SilentlyContinue'
        $global:WarningPreference = 'SilentlyContinue'
        $global:ErrorActionPreference = 'Continue'         ## Using 'Stop' will break execution and not throw errors to the next handler. Don't release it that way.
        $global:ConfirmPreference = 'None'              ## Allows functions that would typically require confirmation to be overridden
        $global:PSModuleAutoloadingPreference = 'All'      ## Enable Auto Module Imports if commands match

        ## Test to see if we already have an ActionRequest object. If so, this session has already been run and the setup is no longer needed.
        if (-not $actionRequest) {

            ##
            ## Setup the Shell and Session
            ##

            ## The remote session had previously imported some importent detail, this will do it here in this session.
            . Import-TMDActionRequest -FromFile $ActionRequestFilePath

            ## Get the Task File Name
            if ($IsWindows) {
                $TaskDebugFolderPath = Join-Path -Path 'C:\Users' $env:USERNAME 'TMD_Files' 'debug'
                $TaskDebugFilePath = (Join-Path $TaskDebugFolderPath ('Task_' + $actionRequest.task.taskNumber + '.ps1'))
            }

            # Print Debug Header
            Write-Host "Beginning Debug Session for:"
            Write-Host " Task #: "$actionRequest.task.taskNumber -ForegroundColor Green
            Write-Host " Action: "$actionRequest.options.apiAction.name -ForegroundColor Green
            Write-Host ""
            Write-Host 'Variable [$Params]: '
            $Params.PSObject.Properties | Where-Object {
                $_.Name -notlike 'Dt*'
            } | Select-Object Name, Value | Out-String

            New-Variable -Name 'ActionProvider' -Value $actionRequest.task -Force -Scope Global

            ## Set a breakpoint to give the user control over how the file is run. (Only the first time.)
            if ((Get-PSBreakpoint).Count -lt 1) {

                Set-PSBreakpoint -Script $TaskDebugFilePath -Line 1 | Out-Null
            }
        }

        ## Now that the Session has been configured and the ActionRequest imported, Run the User's script
        . $TaskDebugFilePath

        ## Mark the task as complete in TM
        Complete-TMTask -ActionRequest $ActionRequest @DataOption

    }

    END {}
}
Function Import-TMDActionRequest {
    param(
        [Parameter(Mandatory = $false)][String]$FromB64,
        [Parameter(Mandatory = $false)][String]$SerializedActionRequestObject,
        [Parameter(Mandatory = $false)][PSObject]$PSObjectActionRequest,
        [Parameter(Mandatory = $false)][String]$FromFile,
        [Parameter(Mandatory = $false)][Switch]$PassThru,
        [Parameter(Mandatory = $false)][Switch]$SkipProviderLoad
    )
    if ($FromFile) {
        $actionRequest = (Get-Content -Path $FromFile | ConvertFrom-Json)
    } elseif ($SerializedActionRequestObject) {
        $actionRequest = [System.Management.Automation.PSSerializer]::Deserialize($SerializedActionRequestObject)
    } elseif ($PSObjectActionRequest) {
        $actionRequest = $PSObjectActionRequest
    } elseif ($FromB64) {
        $actionRequest = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($FromB64)) | ConvertFrom-Json
    } else {
        throw 'Unable to create Action Request Object.'
    }


    ## Make sure we created a new actionRequest,
    ## use it as a container to populate the runspace with some variables
    if ($actionRequest) {

        ## Create the TM Session Callback options for the Task Update functions
        $TMDCallbackOptions = @{
            SiteURL     = $actionRequest.options.callback.siteUrl;
            BearerToken = $actionRequest.options.callback.token;
            ProjectID   = $actionRequest.options.projectId;
            TaskID      = $actionRequest.task.id;
        }

        ## Handle Credential Creation for items passed from Credentials
        if ($null -ne $actionRequest.options.credentials) {

            ## Extract the Username and and Encrypt the password
            $Username = $actionRequest.options.credentials.username
            $EncryptedPassword = ConvertTo-SecureString $actionRequest.options.credentials.password -AsPlainText -Force

            ## Remove the credentials object from the $actionRequest
            $actionRequest.options.credentials = $null

            ## Create a new Credential and assign it to the $Credential variable in the current scope (session)
            $PSCredential = New-Object System.Management.Automation.PSCredential ($Username, $EncryptedPassword)
            $ActionRequest | Add-Member -NotePropertyName 'PSCredential' -NotePropertyValue $PSCredential | Out-Null
            New-Variable -Name 'Credential' -Value $PSCredential -Force -Scope Global
        }



        ## Handle Credential Creation within any Parameters that defined a credential
        $suppliedCredentials = $actionRequest.params.PSObject.Properties | Where-Object { $_.Name -like 'supplied_credential_*' }

        ## Convert any 'supplied_credential_' objects into their appropriate PSObject format.
        if ($suppliedCredentials.count -gt 0) {

            $suppliedCredentials | ForEach-Object {
                $suppliedCredential = $_

                ## Remove the credentials object from the parameter
                $actionRequest.params.PSObject.Properties.Remove($suppliedCredential.Name)

                ## Extract the Username and and Encrypt the password
                $Username = $suppliedCredential.value.username
                $EncryptedPassword = ConvertTo-SecureString $suppliedCredential.value.password -AsPlainText -Force
                $ConvertedCredential = New-Object System.Management.Automation.PSCredential ($Username, $EncryptedPassword)

                ## Add the converted credential to the Params
                $actionRequest.params | Add-Member -NotePropertyName ($suppliedCredential.Name -replace 'supplied_credential_', '' ) -NotePropertyValue $ConvertedCredential -Force
            }
        }

        ## Handle Credential Creation within any Parameters that defined a credential
        $storedCredentials = $actionRequest.params.PSObject.Properties | Where-Object { $_.Name -like 'use_storedcredential_*' }

        ## Retrieve any 'use_stored_credential_' objects from local storage into their appropriate PSObject format.
        if ($storedCredentials.count -gt 0) {

            $storedCredentials | ForEach-Object {
                $storedCredential = $_

                ## Remove the credentials object from the parameter
                $actionRequest.params.PSObject.Properties.Remove($storedCredential.Name)

                $RetrievedCredential = Get-StoredCredential -CredentialName $storedCredential.Value

                ## Add the converted credential to the Params
                $actionRequest.params | Add-Member -NotePropertyName ($storedCredential.Name -replace 'use_storedcredential_', '' ) -NotePropertyValue $RetrievedCredential -Force
            }
        }

        ## Allow Required Modules to be imported automatically
        $PSModuleAutoloadingPreference = 'All'
        $Global:PSModuleAutoloadingPreference = 'All'

        ## Add the ProviderSetup to the ActionRequest
        if ($ProviderSetup) {

            ## Always add the ProviderSetup to the Action Request
            $actionRequest | Add-Member -NotePropertyName ProviderSetup -NotePropertyValue $ProviderSetup -Force

            ## Only import the Modules if it's not supposed to skip (This only occurs when a script is being bootstrapped to connect to the Session)
            if (-not $SkipProviderLoad) {

                ## Display the Startup/Import Message
                if ($ProviderSetup.StartupMessage) {
                    Write-Host $ProviderSetup.StartupMessage
                }

                ## Import Each Declared Module
                foreach ($Module in $ProviderSetup.ModulesToImport) {
                    # . Import-PublicGalleryModule $Module
                    . Import-Module $Module -ErrorAction 'SilentlyContinue' -SkipEditionCheck -Scope Global
                }
                foreach ($Module in $ProviderSetup.WinPSModulesToImport) {
                    # . Import-PublicGalleryModule $Module
                    # . Import-Module $Module -ErrorAction 'SilentlyContinue' -SkipEditionCheck -Scope Global
                    . Import-Module $Module -UseWindowsPowerShell
                }

                ## If the ProviderSetup has a StartupScript Run the ScriptBlock
                if ($ProviderSetup.StartupScript) {
                    . Invoke-Command -ScriptBlock $ProviderSetup.StartupScript -NoNewScope -ErrorAction 'SilentlyContinue'
                }
            }
        }

        ## Import the remaining Variables
        New-Variable -Name 'ActionRequest' -Value $actionRequest -Force -Scope Global
        New-Variable -Name 'Params' -Value $actionRequest.params -Force -Scope Global
        New-Variable -Name 'TMDCallbackOptions' -Value $TMDCallbackOptions -Force -Scope Global

    } else {
        ## The result of the script didn't produce an actionRequest object
        throw 'Unable to create an $actionRequest object'
    }
    if ($PassThru) { $ActionRequest }
}
Function Set-TMTaskAction {
    param(

        ## TMD Callback Options
        [Parameter(Mandatory = $false)][PSObject]$TMDCallbackOptions,
        [Parameter(Mandatory = $false)][PSObject]$ActionRequest,

        ## The selected Task State drives the APU URI generation
        [Parameter(Mandatory = $true)]
        [ValidateSet('started', 'progress', 'success', 'error')]
        [String]$State,

        ## Extra Features / Data Values
        [Parameter(Mandatory = $false)][String]$Message,

        [Parameter(Mandatory = $false)]
        [ValidateRange(0, 100)]
        [int]$Progress,

        [Parameter(Mandatory = $false)][String]$Stdout,
        [Parameter(Mandatory = $false)][String]$Stderr,
        [Parameter(Mandatory = $false)][PSObject]$Data,
        [Parameter(Mandatory = $false)][String]$DataFile,
        [Parameter(Mandatory = $false)][Boolean]$AllowInsecureSSL = $false
    )

    ## Create a Callback Options object to know what Server to use
    if (-not $TMDCallbackOptions -and $ActionRequest) {
        ## Create the TM Session Callback options for the Task Update functions
        $TMDCallbackOptions = @{
            SiteURL     = $ActionRequest.options.callback.siteUrl;
            BearerToken = $ActionRequest.options.callback.token;
            ProjectID   = $ActionRequest.options.projectId;
            TaskID      = $ActionRequest.task.id;
        }
    }

    ## Don't make a call if Callback options were not present
    if (-not $TMDCallbackOptions) {
        return
    }

    #Honor SSL Settings
    if ($TMSessionConfig.AllowInsecureSSL -or $AllowInsecureSSL) {
        $TMCertSettings = @{SkipCertificateCheck = $true }
    } else {
        $TMCertSettings = @{SkipCertificateCheck = $false }
    }
    $uriPaths = @{
        started  = 'actionStarted'
        progress = 'actionProgress'
        success  = 'actionDone'
        error    = 'actionError'
    }

    $MPBoundary = '--------------------------314159265358979323846264'

    ## Headers
    $RequestHeaders = @{
        "Accept-Version" = "1.0";
        "Content-Type"   = "multipart/form-data; boundary=" + $MPBoundary;
        "Accept"         = "application/json";
        "Authorization"  = 'Bearer ' + $TMDCallbackOptions.BearerToken;
        "Cache-Control"  = "no-cache";
    }

    # Body Creation
    $Body = [System.Text.StringBuilder]::new()

    ## Add Project
    [void]$Body.Append( '--' + $MPBoundary )
    [void]$Body.Append($CRLF)
    [void]$Body.Append( 'Content-Disposition: form-data; name="project.id"' )
    [void]$Body.Append($CRLF)
    [void]$Body.Append( '' )
    [void]$Body.Append($CRLF)
    [void]$Body.Append( $TMDCallbackOptions.ProjectID )
    [void]$Body.Append($CRLF)

    ## Add Progress
    if ($Progress) {
        [void]$Body.Append( '--' + $MPBoundary )
        [void]$Body.Append($CRLF)
        [void]$Body.Append( 'Content-Disposition: form-data; name="progress"' )
        [void]$Body.Append($CRLF)
        [void]$Body.Append( 'Content-Type: application/x-www-form-urlencoded' )
        [void]$Body.Append($CRLF)
        [void]$Body.Append( '' )
        [void]$Body.Append($CRLF)
        [void]$Body.Append( $Progress )
        [void]$Body.Append($CRLF)
    }

    ## Add Message
    if ($Message) {
        [void]$Body.Append( '--' + $MPBoundary )
        [void]$Body.Append($CRLF)
        [void]$Body.Append( 'Content-Disposition: form-data; name="message"' )
        [void]$Body.Append($CRLF)
        [void]$Body.Append( '' )
        [void]$Body.Append($CRLF)
        [void]$Body.Append( $Message )
        [void]$Body.Append($CRLF)
    }

    #Standard Out
    if ($Stdout) {
        [void]$Body.Append( '--' + $MPBoundary )
        [void]$Body.Append($CRLF)
        [void]$Body.Append( 'Content-Disposition: form-data; name="stdout"' )
        [void]$Body.Append($CRLF)
        [void]$Body.Append( '' )
        [void]$Body.Append($CRLF)
        [void]$Body.Append( $Stdout )
        [void]$Body.Append($CRLF)
    }

    #Standard Err
    if ($Stderr) {
        [void]$Body.Append( '--' + $MPBoundary )
        [void]$Body.Append($CRLF)
        [void]$Body.Append( 'Content-Disposition: form-data; name="stderr"' )
        [void]$Body.Append($CRLF)
        [void]$Body.Append( '' )
        [void]$Body.Append($CRLF)
        [void]$Body.Append( $Stderr )
        [void]$Body.Append($CRLF)
    }

    #Data
    if ($Data) {
        [void]$Body.Append( '--' + $MPBoundary )
        [void]$Body.Append($CRLF)
        [void]$Body.Append( 'Content-Disposition: form-data; name="data"' )
        [void]$Body.Append($CRLF)
        [void]$Body.Append( 'Content-Type: application/json;' )
        [void]$Body.Append($CRLF)
        [void]$Body.Append( '' )
        [void]$Body.Append($CRLF)
        [void]$Body.Append( ($Data | ConvertTo-Json -Depth 100 -Compress ) )
        [void]$Body.Append($CRLF)
    }

    # #Data File
    # if($DataFile){
    # [void]$Body.Append( '--' + $MPBoundary )
    # [void]$Body.Append( 'Content-Disposition: form-data; name="datafile"' )
    # [void]$Body.Append( '' )
    # [void]$Body.Append( $DataFile )
    # }

    ## End the Body and save it to a string
    [void]$Body.Append( "--" + $MPBoundary + "--" )
    [void]$Body.Append($CRLF)
    $PostBody = $Body.ToString()

    # API
    $uri = $TMDCallbackOptions.SiteURL
    $uri += "/api/task/" + $TMDCallbackOptions.TaskID + "/"
    $uri += $uriPaths.$State

    ## Invoke the HTTP Request and handle the response
    try {
        # Perform actual web request
        $Response = Invoke-WebRequest -Method Post -Uri $uri -Headers $RequestHeaders -Body $PostBody @TMCertSettings
        if ($Response.StatusCode -eq 200) {
            $ResponseJson = $response.Content | ConvertFrom-Json -Depth 100

            if ($ResponseJson.status -eq 'error') {
                throw $ResponseJson.errors
            }
        }

        ## TM 5.0+ returns a 204 (OK, but no content)
        elseif ($Response.StatusCode -eq 204) {
            return
        } else {
            Throw ('Error while setting task to ' + $State)
        }

    } catch {
        throw $_
    }
}
Function Update-TMTaskProgress {
    param(
        [Parameter(Mandatory = $true)]
        [ValidateRange(0, 100)]
        [int]$Progress,

        ## Extra Features / Data Values
        [Parameter(Mandatory = $false)][String]$Message
    )
    ## Allows running scripts that call these functions, so long as there is no Action Request
    # if (-Not $global:actionRequest) { return }

    ## If the Script wants to provide a 'Message', Messages are logged as task notes.
    if ($Message) {
        $MessageOption = @{ Message = $Message }
    }

    ## Post Back Success with Data and Messages as provided by the Script
    Set-TMTaskAction -TMDCallbackOptions $global:TMDCallbackOptions -State 'progress' -Progress $Progress @MessageOption
}
Function Complete-TMTask {
    param(
        [Parameter(Mandatory = $false)][pscustomobject]$ActionRequest,
        [Parameter(Mandatory = $false)][pscustomobject]$Data,
        [Parameter(Mandatory = $false)][Boolean]$AllowInsecureSSL = $false
    )

    if ($Data) {
        $DataOption = @{ Data = $Data }
    }

    ## Post Back Success with Data and Messages as provided by the Script
    Set-TMTaskAction -ActionRequest $ActionRequest -Progress 100 -State 'success' @DataOption -AllowInsecureSSL $AllowInsecureSSL
}
Function Set-TMTaskOnHold {
    param(
        ## Extra Features / Data Values
        [Parameter(Mandatory = $false)][pscustomobject]$ActionRequest,
        [Parameter(Mandatory = $false)][String]$Message,
        [Parameter(Mandatory = $false)][String]$Data,
        [Parameter(Mandatory = $false)][Boolean]$AllowInsecureSSL = $false
    )

    ## If the Script wants to provide a 'Message', Messages are logged as task notes.
    if ($Message) {
        $MessageOption = @{ Message = $Message }
    }
    if ($Data) {
        $DataOption = @{ Data = $Data }
    }

    ## Post Back Success with Data and Messages as provided by the Script
    Set-TMTaskAction -ActionRequest $ActionRequest -State 'error' @MessageOption @DataOption -AllowInsecureSSL $AllowInsecureSSL
}

Function Update-TMTaskAsset {
    param(
        [Parameter(Mandatory = $false)][PSObject][AllowNull()]$AssetUpdates = $null,
        [Parameter(Mandatory = $false)][String][AllowNull()]$Field = $null,
        [Parameter(Mandatory = $false)][String][AllowNull()]$Value = $null
    )

    ## Ensure that there is a TM Asset Updates global Array use
    # if (-not $global:TMAssetUpdates) { $global:TMAssetUpdates = [System.Collections.ArrayList]@() }
    if (-not $global:TMAssetUpdates) {
        $global:TMAssetUpdates = [pscustomobject]@{}
    }

    ## Allow for providing in a full Hashtable of updates
    if ($AssetUpdates) {
        $AssetUpdates.PSObject.Properties | ForEach-Object {
            $global:TMAssetUpdates | Add-Member -NotePropertyName $_.Name -NotePropertyValue $_.value -Force
            Write-Host "Updating TM Asset Field: " -NoNewline
            Write-Host $_.Name -NoNewline -ForegroundColor Green
            Write-Host ' with value: ' -NoNewline
            Write-Host $_.Value -ForegroundColor Yellow
        }
    }

    ## Allow for adding in a single field and value.
    if ($Field) {
        $global:TMAssetUpdates | Add-Member -NotePropertyName $Field -NotePropertyValue $Value -Force
        Write-Host "Updating TM Asset Field: " -NoNewline
        Write-Host $Field -NoNewline -ForegroundColor Cyan
        Write-Host ' with value: ' -NoNewline
        Write-Host $Value -ForegroundColor Yellow
    }
}