Public/Authentication.ps1

# Define a TMSessions Variable to store connection details in
New-Variable -Scope global -Name 'TMSessions' -Value @{} -Force

function New-TMSession {
    <#
    .SYNOPSIS
    Creates a connection to a TransitionManager instance
 
    .DESCRIPTION
    This function creates a connection to the specified TransitionManager instance using
    the provided credentials. This session can be used when calling other functions within
    the TransitionManager module
 
    .PARAMETER SessionName
    The name that will be used when referring to the created TMSession
 
    .PARAMETER Server
    The URI of the TransitionManager instance to connect to
 
    .PARAMETER Credential
    Specifies a PSCredential object to be used when connecting to TransitionManager
 
    .PARAMETER StoredCredential
    Specifies a string that will be used to search for a StoredCredential.
    If a matching StoredCredential is found, it will be used when connecting to TransitionManager
 
    .PARAMETER Project
    The name of the project on the TransitionManager instance to enter into
 
    .PARAMETER TMVersion
    Used to override the API version used by other functions in the TransitionManager module
 
    .PARAMETER AllowInsecureSSL
    Boolean indicating whether or not an insecure SSL connection is allowed
 
    .PARAMETER Passthru
    Switch indicating that the newly created TMSession should be output
 
    .PARAMETER ProfileName
    The name of the stored TMProfile which contains the connection settings
 
    .EXAMPLE
    $Session = @{
        SessionName = 'TMDDEV'
        Server = 'tmddev.transitionmanager.net'
        Credential = (Get-StoredCredential -Name 'ME')
        Project = 'RD - VMware HCX'
    }
    New-TMSession @Session
 
    .EXAMPLE
    $Session = @{
        SessionName = 'TMDDEV'
        Server = 'tmddev.transitionmanager.net'
        StoredCredential = 'JohnSmith'
        Project = 'RD - VMware HCX'
    }
    New-TMSession @Session
 
    .EXAMPLE
    Get-TMProfile -Name 'TMDDEV' | New-TMSession
 
    .EXAMPLE
    New-TMSession -ProfileName 'TMQA09'
 
    .OUTPUTS
    None
    #>


    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "")]
    [CmdletBinding()]
    [Alias('Connect-TMServer')]
    param(
        [Parameter(Mandatory = $false,
            ValueFromPipelineByPropertyName = $true)]
        [Alias('Name')]
        [String]$SessionName = 'Default',

        [Parameter(Mandatory = $true,
            ParameterSetName = 'ByStoredCredential',
            ValueFromPipelineByPropertyName = $true)]
        [Parameter(Mandatory = $true,
            ParameterSetName = 'ByPSCredential',
            ValueFromPipelineByPropertyName = $true)]
        [Parameter(Mandatory = $false,
            ValueFromPipelineByPropertyName = $true)]
        [String]$Server,

        [Parameter(Mandatory = $true,
            ParameterSetName = 'ByPSCredential',
            ValueFromPipelineByPropertyName = $true)]
        [PSCredential]$Credential,

        [Parameter(Mandatory = $true,
        ParameterSetName = 'ByStoredCredential')]
        [String]$StoredCredential,

        [Parameter(Mandatory = $false,
            Position = 1,
            ValueFromPipelineByPropertyName = $true)]
        [Alias('Project')]
        [String]$ProjectName,

        [Parameter(Mandatory = $false,
            ValueFromPipelineByPropertyName = $true)]
        [Object]$TMVersion,

        [Parameter(Mandatory = $false,
            ValueFromPipelineByPropertyName = $true)]
        [Bool]$AllowInsecureSSL = $false,

        [Parameter(Mandatory = $false)]
        [Switch]$Passthru,

        [Parameter(Mandatory = $true,
            ParameterSetName = 'ByProfileObject')]
        [TMProfile]$TMProfile,

        [Parameter(Mandatory = $true,
            Position = 0,
            ParameterSetName = 'ByProfile')]
        [ArgumentCompleter( { Get-TMProfile -List })]
        [Alias('Profile')]
        [String]$ProfileName
    )

    begin {

        try {
            if ($PSCmdlet.ParameterSetName -eq 'ByStoredCredential') {
                $Credential = Get-StoredCredential -Name $StoredCredential

                #Get-StoredCredential does not throw terminating errors if a matching StoredCredential isn't found
                #so need to throw one here to end the script if no matches are found.
                if (-not $Credential) { throw }
            }

        } catch {
            Write-Error "No StoredCredential named $StoredCredential was found." -ErrorAction Stop
        }

        ## Create the TMServers Array that will be reachable at $Global:TMSessions
        if (-not $Global:TMSessions) {
            New-Variable -Name TMSessions -Scope Global -Value @{}
        }
    }

    process {
        if ($ProfileName) {
            $TMProfile = Get-TMProfile -Name $ProfileName
            if (!$TMProfile) {
                Write-Error "Could not load a TransitionManager profile named '$ProfileName'"
                return
            }
        }

        if ($TMProfile) {
            $SessionName = (($Global:TMSessions.Keys -contains 'Default') -and ($SessionName -eq 'Default')) ? $TMProfile.Name : 'Default'
            $Server = $TMProfile.Server
            $Credential = $TMProfile.Credential
            $AllowInsecureSSL = $TMProfile.AllowInsecureSSL
            if ([String]::IsNullOrWhiteSpace($Project)) {
                $Project = $TMProfile.Project
            }
        } else {
            if ($SessionName -eq 'Default') {
                $SessionName = (($Global:TMSessions.Keys -contains 'Default') -and ($SessionName -eq 'Default')) ? $Server : 'Default'
            }
        }

        # Make sure Credential is the correct type
        if (-not ($Credential -is [PSCredential])) {
            throw "The value passed for the Credential parameter must be either a String with the name of a stored credential or a PSCredential."
        }

        # Trim the server name
        $Instance = [TMSession]::FormatServerUrl($Server)

        # Check for Existing Session to this server
        if ($Global:TMSessions.Keys -contains $SessionName) {
            $NewTMSession = $Global:TMSessions[$SessionName]
        } else {
            $NewTMSession = [TMSession]::new($SessionName, $Instance, $AllowInsecureSSL)
        }

        # Get the TM Version (Allow a different api usage of an alternate version)
        $Version = [TMSession]::ParseVersion($TMVersion)
        if ($Version -le '0.0.0') {
            $Version = Get-TMVersion -Server $Server -AllowInsecureSSL $AllowInsecureSSL
        }
        $NewTMSession.TMVersion = $Version

        if ($NewTMSession.TMVersion -lt '6.1' -or $NewTMSession.TMVersion -ge '7.0.0') {
            Write-Error "Unable to Log into $Instance. Version $($NewTMSession.TMVersion) not supported"
            return
        }

        # Prepare Request Headers for use in the Session Header Cache
        $ContentType = 'application/json;charset=UTF-8'
        $RequestHeaders = @{
            "Accept-Version" = "1.0";
            "Content-Type"   = $ContentType;
            "Accept"         = "application/json";
            "Cache-Control"  = "no-cache";
        }

        $WebRequestSplat = @{
            Method                          = 'POST'
            Uri                             = "https://$Instance/tdstm/auth/signIn"
            Headers                         = $RequestHeaders
            Body                            = (
                @{
                    username = $Credential.UserName
                    password = $Credential.GetNetworkCredential().Password
                } | ConvertTo-Json
            )
            SessionVariable                 = 'TMWebSession'
            PreserveAuthorizationOnRedirect = $true
            ContentType                     = $ContentType
            SkipCertificateCheck            = $AllowInsecureSSL
        }

        # Attempt Login
        if ($VerbosePreference -eq 'Continue') {
            Write-Host "Logging into TransitionManager instance [ " -NoNewline
            Write-Host $Instance -ForegroundColor Cyan -NoNewline
            Write-Host " ]"
        }

        # Make the request
        try {
            Write-Verbose "Web Request Parameters:"
            Write-Verbose ($WebRequestSplat | ConvertTo-Json -Depth 10)
            Write-Verbose "Invoking web request"
            $Response = Invoke-WebRequest @WebRequestSplat
            Write-Verbose "Response status code: $($Response.StatusCode)"
            Write-Verbose "Response Content: $($Response.Content)"
        } catch {
            throw $_
        }

        # Check the Response code for 200
        if ($Response.StatusCode -eq 200) {
            $ResponseContent = $Response | ConvertFrom-Json -Depth 10

            # Ensure the login succeeded.
            if ($ResponseContent.PSObject.Properties.name -eq 'error' ) {

                # Report Error Condition
                Write-Error "Login Failed: $($ResponseContent[0].'error')"
                return
            }

            # Login Succeeded
            if ($ResponseContent.PSObject.Properties.name -notcontains 'userContext') {
                Write-Error "Login Failure! Unable to Log into $Server. Check the URL and Credentials and try again"
                return
            }
        } else {
            Write-Error "Unable to Log into $Server. Check the URL and Credentials and try again"
            return
        }

        $NewTMSession.TMWebSession = $TMWebSession
        $NewTMSession.ParseCallbackData($Response, [TMCallbackDataType]::WebService)

        # Login to the REST API
        $WebRequestSplat.Uri = "https://$Instance/tdstm/api/login"
        $WebRequestSplat.SessionVariable = 'TMRestSession'
        $WebRequestSplat.StatusCodeVariable = 'StatusCode'
        $WebRequestSplat.SkipHttpErrorCheck = $true

        # Make the request
        try {
            Write-Verbose "Web Request Parameters:"
            Write-Verbose ($WebRequestSplat | ConvertTo-Json -Depth 10)
            Write-Verbose "Invoking web request"
            $Response = Invoke-RestMethod @WebRequestSplat
            Write-Verbose "Response status code: $($Response.StatusCode)"
            Write-Verbose "Response Content: $($Response.Content)"
        } catch {
            throw $_
        }

        if ($StatusCode -notin 200, 204) {
            throw "Could not login to the TransitionManager API. Status Code: $StatusCode"
        }

        $NewTMSession.TMRestSession = $TMRestSession
        $NewTMSession.ParseCallbackData($Response, [TMCallbackDataType]::REST)

        # Create the TMSessionObject that will be checked/used by the rest of the TransitionManager Modules
        $NewTMSession.Authentication.PublicKey = Get-TMPublicKey -Server $Instance -AllowInsecureSSL $AllowInsecureSSL

        # Apply all of the gathered tokens to the appropriate headers/cookies
        $NewTMSession.ApplyHeaderTokens()

        # Add this Session to the TMSessions list
        $Global:TMSessions[$SessionName] = $NewTMSession

        # Get the User Info
        $Global:TMSessions[$SessionName].GetUserAccount()

        # Check that the correct projet is selected
        if ([String]::IsNullOrWhiteSpace($ProjectName)) {
            if ($VerbosePreference -eq 'Continue') {
                Write-Host "Login Successful! Entering Last Project used: [" -NoNewline
                Write-Host $NewTMSession.UserContext.Project.Name -ForegroundColor Cyan -NoNewline
                Write-Host "]"
            }
        } elseif ($NewTMSession.UserContext.Project.Name -ne $ProjectName) {
            Enter-TMProject -TMSession $SessionName -ProjectName $ProjectName
        }

        # Return the session if requested
        if ($Passthru) {
            $NewTMSession
        }
    }
}


function Get-TMVersion {
    <#
    .SYNOPSIS
    Gets the version of a given TransitionManager instance
 
    .DESCRIPTION
    This function queries the given TransitionManager server for it's version number and
    returns it in the specified format
 
    .PARAMETER Server
    The server to be checked
 
    .PARAMETER Format
    The format of the version string that is returned
 
    .PARAMETER AllowInsecureSSL
    Boolean indicating whether or not an insecure SSL connection is allowed
 
    .EXAMPLE
    Get-TMVersion -Server 'tmddev.transitionmanager.net'
 
    .OUTPUTS
    A string containing the server's version in the specified format
    #>


    [CmdletBinding()]
    [OutputType([String])]
    param(
        [Parameter(Mandatory = $true)]
        [String]$Server,

        [Parameter(Mandatory = $false)]
        [ValidateSet('VersionSemVer', 'SemVer', 'Minor')]
        [String]$Format = "SemVer",

        [Parameter(Mandatory = $false)]
        [Bool]$AllowInsecureSSL = $false
    )

    try {
        $Instance = [TMSession]::FormatServerUrl($Server)

        $Uri = "https://$Instance/tdstm/auth/loginInfo"
        $Response = Invoke-WebRequest -Method Get -Uri $Uri -SkipCertificateCheck:$AllowInsecureSSL
        if ($Response.StatusCode -eq 200) {
            $BuildVersion = ($Response.Content | ConvertFrom-Json).data.buildVersion
            $Version = [TMSession]::ParseVersion($BuildVersion)
            if ($Version -eq [Version]::new()) {
                throw "Unable to determine TM Server Version from string: $BuildVersion"
            }
            $Version
        } else {
            throw "Response status code $($Response.StatusCode) does not indicate success"
        }
    } catch {
        Write-Warning "Could not determine TM Server Version: $($_.Exception.Message)"
        [Version]::new()
    }
}


function Get-TMUserAccount {
    <#
    .SYNOPSIS
    Gets details about the TM User's account
 
    .DESCRIPTION
    Queries TransitionManager for a User's account details
 
    .PARAMETER TMSession
    A [TMSession] object or the name of a TMSession
 
    .PARAMETER UserId
    The ID of the user for whom the account details will be retrieved
 
    .EXAMPLE
    # Gets the User account details for the user logged into the 'ProjectInstance' TMSession
    Get-TMUserAccount -TMSession 'ProjectInstance'
 
    .EXAMPLE
    # Gets the User account details for the user with ID 632
    Get-TMUserAccount -UserId 632
 
    .OUTPUTS
    A [TMUserAccount] object representing the requested user's details
    #>


    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $false,
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true)]
        [Alias('Session', 'Name')]
        [Object]$TMSession = 'Default',

        [Parameter(Mandatory = $false)]
        [Alias('Id')]
        [Int32]$UserId
    )

    process {
        # Verify/Retrieve the TMSession
        $TMSession = Get-TMSession $TMSession

        # Use the TMSession's user if an Id wasn't provided
        if (-not $UserId) {
            $UserId = $TMSession.UserContext.User.Id
        }

        # Form the request
        Write-Verbose "Forming web request"
        $RestSplat = @{
            Method               = 'GET'
            Uri                  = "https://$($TMSession.TMServer)/tdstm/ws/user/$UserId"
            WebSession           = $TMSession.TMWebSession
            StatusCodeVariable   = 'StatusCode'
            SkipHttpErrorCheck   = $true
            SkipCertificateCheck = $TMSession.AllowInsecureSSL
        }

        try {
            Write-Debug "Web Request Parameters:"
            Write-Debug ($RestSplat | ConvertTo-Json -Depth 10)
            Write-Verbose "Invoking web request"
            $Response = Invoke-RestMethod @RestSplat
            Write-Debug "Response status code: $StatusCode"
            Write-Debug "Response Content: $($Response | ConvertTo-Json -Depth 10 -ErrorAction SilentlyContinue)"
        } catch {
            throw $_
        }

        if ($StatusCode -in 200, 204) {
            [TMUserAccount]::new($Response.data)
        } else {
            throw "The Status Code $StatusCode does not indicate success"
        }
    }
}


function Get-TMPublicKey {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [String]$Server,

        [Parameter(Mandatory = $false)]
        [Boolean]$AllowInsecureSSL = $false
    )


    $Server = [TMSession]::FormatServerUrl($Server)

    # Check for a version 4.7, 5.0, 5.1
    $Uri = "https://$Server/tdstm/auth/loginInfo?secure=true"
    try {
        $Response = Invoke-WebRequest -Method Get -Uri $Uri -SkipCertificateCheck:$AllowInsecureSSL
        if ($Response.StatusCode -in 200, 204) {
            ($Response.Content | ConvertFrom-Json).data.publicKey
        } else {
            throw "The response status code does not indicate success"
        }
    } catch {
        Write-Warning "Could not get public key information: $($_.Exception.Message)"
    }

}


function Get-TMSession {
    <#
    .SYNOPSIS
    Gets a TMSession by Name, Server or Version
 
    .PARAMETER Name
    One or more TMSession names to get
 
    .PARAMETER Server
    One or more TM servers for which a TMSession has been created
 
    .PARAMETER Version
    One or more TM server versions for which a TMSession has been created
 
    .EXAMPLE
    Get-TMSession -Name 'Default', 'TMDDEV'
 
    .EXAMPLE
    Get-TMSession -Version '6.1.*'
 
    .EXAMPLE
    Get-TMSession -Server '*.transitionmanager.net'
 
    .OUTPUTS
    One TMSession, or an array of TMSessions depending on the number of sessions to return
    #>


    [CmdletBinding(DefaultParameterSetName = 'ByName')]
    param(
        [Parameter(Mandatory = $false,
            Position = 0,
            ParameterSetName = 'ByName',
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true)]
        [Alias('SessionName')]
        [String[]]$Name = "*",

        [Parameter(Mandatory = $false,
            ParameterSetName = 'ByServer',
            ValueFromPipelineByPropertyName = $true)]
        [Alias('TMServer')]
        [String[]]$Server,

        [Parameter(Mandatory = $false,
            ParameterSetName = 'ByVersion',
            ValueFromPipelineByPropertyName = $true)]
        [Alias('TMVersion')]
        [String[]]$Version,

        [Parameter(Mandatory = $false,
            Position = 0,
            ParameterSetName = 'ByObject')]
        [TMSession] $TMSession

    )

    process {
        [string] $StarMatch = '.*\*.*'

        $SessionsToReturn = switch ($PSCmdlet.ParameterSetName) {
            'ByObject' {
                $Global:TMSessions.Values | Where-Object {
                    $_.Name -eq $TMSession.Name -and
                    $_.TMServer -eq $TMSession.TMServer -and
                    $_.TMVersion -eq $TMSession.TMVersion
                }
                break
            }

            'ByName' {
                if ( $Name -eq "*" ) {
                    $Global:TMSessions.Values
                }
                else {
                    foreach ($SingleName in $Name) {
                        $Global:TMSessions.Values | Where-Object Name -like $SingleName
                    }
                }
                break
            }

            'ByServer' {
                if ($Server.Count -eq 1 -and $Server -match $StarMatch) {
                    # Adding a star at the end so we can match just by name and not by FQDN
                    $Global:TMSessions.Values | Where-Object TMServer -like "$Server*"
                }
                else {
                    $Global:TMSessions.Values | Where-Object TMServer -in $Server
                }
                break
            }

            'ByVersion' {
                if ($Version.Count -eq 1 -and $Version -match $StarMatch) {
                    $Global:TMSessions.Values | Where-Object TMVersion -like $Version
                }
                else {
                    $Global:TMSessions.Values | Where-Object TMVersion -in $Version
                }
                break
            }
        }

        if ( $SessionsToReturn.Count ) {
            $SessionsToReturn = $SessionsToReturn.Count -eq 1 ?
                    [TMSession] $SessionsToReturn :
                    [TMSession[]] $SessionsToReturn
            return  ,$SessionsToReturn
        }

        switch ($PSCmdlet.ParameterSetName) {
            'ByObject' {
                throw ('Unexpected error returning a TMSession with Name: "{0}" on server "{1}"' -f ($TMSession)?.Name, ($TMSession).TMServer)
            }

            Default {
                $MatchingProperty = $PSCmdlet.ParameterSetName -replace '^By'
                $MatchingPropertyValue = (Get-Variable $MatchingProperty -ErrorAction SilentlyContinue)?.Value
                # $MatchingPropertyValue = $MatchingPropertyValue?.Value
                if ( $MatchingPropertyValue -match $StarMatch) {
                    return @{}
                }
                else {
                    throw "TMSession with provided $MatchingProperty '$MatchingPropertyValue' was not found"
                }
            }
        }
    }
}


function Remove-TMSession {
    <#
    .SYNOPSIS
    Disconnects one or more sessions with a TransitionManager instance
 
    .DESCRIPTION
    This function signs out of one or more TransitionManager instances and removes the session
    from the $Global:TMSessions variable
 
    .PARAMETER Name
    The name of one or more TMSessions to disconnect and remove
 
    .PARAMETER InputObject
    One or more TMSessions to disconnect and remove
 
    .EXAMPLE
    Remove-TMSession -Name 'Default', 'TMDDEV'
 
    .EXAMPLE
    Get-TMSession | Remove-TMSession
 
    .OUTPUTS
    None
    #>


    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true,
            Position = 0,
            ParameterSetName = 'ByName')]
        [Alias('SessionName')]
        [String[]]$Name,

        [Parameter(Mandatory = $true,
            ValueFromPipeline = $true,
            ParameterSetName = 'ByObject')]
        [Object[]]$InputObject
    )

    process {
        Write-Verbose "Parameter Set: $($PSCmdlet.ParameterSetName)"

        # Compile a list of the sessions that need to be disconnected
        $SessionsToRemove = @()
        switch ($PSCmdlet.ParameterSetName) {
            'ByName' {
                $Name | ForEach-Object { $SessionsToRemove += $Global:TMSessions[$_] }
            }

            'ByObject' {
                $SessionsToRemove = $InputObject
            }
        }

        # Iterate over each session that was passed and sign out of TM
        foreach ($Session in $SessionsToRemove) {
            Write-Host "Logging out of TransitionManager instance [ " -NoNewline
            Write-Host $Session.TMServer -ForegroundColor Cyan -NoNewline
            Write-Host " ]"

            # Format the parameters for the sign out request
            $WebRequestSplat = @{
                Method                          = 'POST'
                Uri                             = "https://$($Session.TMServer)/tdstm/auth/signOut"
                WebSession                      = $Session.TMWebSession
                ContentType                     = 'application/json;charset=UTF-8'
                PreserveAuthorizationOnRedirect = $true
                SkipCertificateCheck            = $Session.AllowInsecureSSL
            }

            try {
                # Make the request to sign out
                $Response = Invoke-WebRequest @WebRequestSplat

                if ($Response.StatusCode -eq 200) {

                    # Ensure the sign out was successful
                    $ResponseContent = $Response.Content | ConvertFrom-Json
                    if ($ResponseContent.status -ne 'success') {
                        Write-Error ($ResponseContent.errors -join '; ')
                    }
                    Write-Host "Log out successful!"

                    # Remove the session from the global sessions variable
                    $Global:TMSessions.Remove($Session.Name)
                } else {
                    Write-Error "Could not sign out of TransitionManager"
                }
            } catch {
                Write-Error $_
            }
        }
    }
}


function New-TMProfile {
    <#
    .SYNOPSIS
    Creates a TransitionManager connection profile on the local hard drive
 
    .DESCRIPTION
    This function creates a TransitionManager connection profile on the localk hard drive
    that can be used with the New-TMSession function to more easily create connections to
    TransitionManager instances
 
    .PARAMETER Name
    The name name of the profile that will be saved. This name will be used to reference the
    TMProfile in other functions.
 
    .PARAMETER Server
    The URI of the TransitionManager instance
 
    .PARAMETER Project
    The name of the project on the TransitionManager instance
 
    .PARAMETER Credential
    The credentials used to connect to TransitionManager
 
    .PARAMETER AllowInsecureSSL
    Boolean indicating whether or not an insecure SSL connection is allowed
 
    .PARAMETER Passthru
    Switch indicating that the newly created TMProfile should be output
 
    .EXAMPLE
    New-TMProfile -Name 'TMDDEV-RVTools' -Server 'tmddev.transitionmanager.net' -Project 'RD - RVTools' -Credential (Get-Credential)
 
    .EXAMPLE
    $Profile = @{
        Name = 'TMDDEV2'
        Server = 'tmddev2.transitionmanager.net'
        Credential = (Get-StoredCredential -Name 'ME')
    }
    New-TMProfile @Profile -Passthru | New-TMSession
 
    .OUTPUTS
    If Passthru switch is used, a TMProfile object. Otherwise, none
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [String]$Name,

        [Parameter(Mandatory = $true)]
        [String]$Server,

        [Parameter(Mandatory = $false)]
        [String]$Project,

        [Parameter(Mandatory = $true)]
        [PSCredential]$Credential,

        [Parameter(Mandatory = $false)]
        [Bool]$AllowInsecureSSL = $false,

        [Parameter(Mandatory = $false)]
        [Switch]$Passthru = $false
    )

    process {
        $ProfileDirectory = Join-Path -Path $HOME -ChildPath 'TMD_Files' -AdditionalChildPath 'Profiles'
        Test-FolderPath -FolderPath $ProfileDirectory

        $TMProfile = [TMProfile]::new($Name, $Server, $Project, $Credential, $AllowInsecureSSL)

        $TMProfile | Export-Clixml -Path (Join-Path -Path $ProfileDirectory -ChildPath "$Name.tmprofile")

        if ($Passthru) {
            $TMProfile
        }
    }
}


function Get-TMProfile {
    <#
    .SYNOPSIS
    Gets a TMProfile that is stored on the local hard drive
 
    .DESCRIPTION
    Gets one or more of the TransitionManager profiles that are saved on the local hard drive
 
    .PARAMETER Name
    The name of the TMProfile to get
 
    .PARAMETER List
    Switch indicating that a list of all TMProfile names should be returned
 
    .EXAMPLE
    $AllProfiles = Get-TMProfile
 
    .EXAMPLE
    Get-TMProfile -Name TMDDEV2
 
    .EXAMPLE
    Get-TMProfile -List
 
    .OUTPUTS
    One or more objects representing a saved TMProfile
    #>


    [CmdletBinding(DefaultParameterSetName = "Single")]
    [OutputType([TMProfile])]
    param(
        [Parameter(Mandatory = $false,
            Position = 0,
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true,
            ParameterSetName = "Single")]
        [ArgumentCompleter( { Get-TMProfile -List })]
        [String]$Name,

        [Parameter(Mandatory = $true, ParameterSetName = "List")]
        [Switch]$List
    )

    process {
        try {
            if (-not $Name -or $List) {
                $ProfileDirectory = Join-Path -Path $HOME -ChildPath 'TMD_Files' -AdditionalChildPath 'Profiles'
                Test-FolderPath -Path $ProfileDirectory
                $ProfileFiles = Get-ChildItem -Path $ProfileDirectory -Filter '*.tmprofile' -File
                $ProfileFiles | ForEach-Object {
                    if ($List) {
                        $_.Name -replace $_.Extension, ''
                    } else {
                        $TMProfile = Import-Clixml -Path $_.FullName
                        [TMProfile]::new(
                            $TMProfile.Name,
                            $TMProfile.Server,
                            $TMProfile.Project,
                            $TMProfile.Credential,
                            $TMProfile.AllowInsecureSSL
                        )
                    }
                }
            } else {
                $ProfileFilePath = Join-Path -Path $HOME -ChildPath 'TMD_Files' -AdditionalChildPath 'Profiles', ($Name + '.tmprofile')
                if (Test-Path -Path $ProfileFilePath) {
                    $TMProfile = Import-Clixml -Path $ProfileFilePath
                    [TMProfile]::new(
                        $TMProfile.Name,
                        $TMProfile.Server,
                        $TMProfile.Project,
                        $TMProfile.Credential,
                        $TMProfile.AllowInsecureSSL
                    )
                }
            }
        } catch {
            $PSCmdlet.WriteError($_)
        }
    }
}


function Remove-TMProfile {
    <#
    .SYNOPSIS
    Removes a previously stored TMProfile from the local hard drive
 
    .DESCRIPTION
    Overwrites a stored .tmprofile file multiple times with a random bytestream before deleting
    it from the local hard drive
 
    .PARAMETER Name
    The name of the TMProfile to be removed
 
    .PARAMETER OverwriteCount
    The number of times to overwrite the file before removing it
 
    .EXAMPLE
    Remove-TMProfile -Name 'TMDDEV2'
 
    .EXAMPLE
    Get-TMProfile | Remove-TMProfile
 
    .NOTES
    General notes
    #>


    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true,
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true)]
        [ArgumentCompleter( { Get-TMProfile -List })]
        [String]$Name,

        [Parameter(Mandatory = $false)]
        [Int]$OverwriteCount = 500
    )

    process {
        $ProfileFilePath = Join-Path -Path $HOME -ChildPath 'TMD_Files' -AdditionalChildPath 'Profiles', ($Name + '.tmprofile')
        Write-Verbose "Processing file '$ProfileFilePath'"
        if (Test-Path -PathType Leaf -Path $ProfileFilePath) {
            $BufferSize = $Name.Length
            $RandomDataBuffer = [System.Byte[]]::new($BufferSize)

            Write-Verbose "Overwriting the data within the file $OverwriteCount time(s)"
            for ($i = 0; $i -lt $OverwriteCount; $i++) {
                $Random = [System.Random]::new()
                $Random.NextBytes($RandomDataBuffer) | Set-Content -Path $ProfileFilePath -AsByteStream
            }
            Write-Verbose "Removing the file"
            Remove-Item -Path $ProfileFilePath -Force
        } else {
            $ErrorRecord = [System.Management.Automation.ErrorRecord]::new(
                [Exception]::new("A TransitionManager profile with the name '$Name' does not exist."),
                "TMD.AUTH02",
                [System.Management.Automation.ErrorCategory]::InvalidArgument,
                $ProfileFilePath
            )
            $PSCmdlet.WriteError($ErrorRecord)
        }
    }
}


function Update-TMSessionToken {
    <#
    .SYNOPSIS
    Updates the REST API access token
 
    .DESCRIPTION
    Refreshes the REST API access token in the TMRestSession of a TMSession
    object and updates the appropriate headers
 
    .PARAMETER TMSession
    A [TMSession] object or the name of a TMSession
 
    .EXAMPLE
    # Get an existing TMSession and pipe it to Update-TMSessionToken
    Get-TMSession -Name 'TMAD61' | Update-TMSessionToken
 
    .EXAMPLE
    $Session = New-TMSession -Server 'tmad61.transitionmanager.net' -Credential (Get-StoredCredential -Name ME) -Passthru
    Update-TMSessionToken -TMSession $Session
 
    .EXAMPLE
    # Updates the access token of the 'Default' TMSession
    Update-TMSessionToken
 
    .EXAMPLE
    # Updates the access token of the TMSession named 'ExampleSession'
    Update-TMSessionToken -TMSession 'ExampleSession'
 
    .OUTPUTS
    None
    #>


    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $false, ValueFromPipeline = $true)]
        [Alias('Session', 'Name')]
        [Object]$TMSession = 'Default'
    )

    process {
        $TMSession = Get-TMSession $TMSession

        # Make sure refresh token is present in the TMSession
        if ([String]::IsNullOrWhiteSpace($TMSession.Authentication.OAuth.RefreshToken)) {
            throw "A refresh token could not be found in the TMSession"
        }

        # Make sure a JSessionID is present in the TMSession
        if ([String]::IsNullOrWhiteSpace($TMSession.Authentication.JSessionId)) {
            # Extract the JSessionID from one of the cookies if needed
            $ExistingJsessionCookies = @()
            $ExistingJsessionCookies += ($this.TMWebSession.Cookies.GetAllCookies() | Where-Object Name -eq 'JSESSIONID')
            $ExistingJsessionCookies += ($this.TMRestSession.Cookies.GetAllCookies() | Where-Object Name -eq 'JSESSIONID')
            if ($ExistingJsessionCookies.Count -gt 0) {
                $TMSession.Authentication.JSessionId = $ExistingJsessionCookies[0].Value
                $TMSession.ApplyHeaderTokens()
            } else {
                throw "No JSESSIONID cookie could be found in the TMSession."
            }
        }

        # Make sure the CSRF token is present in the TMSession
        if ([String]::IsNullOrWhiteSpace($TMSession.Authentication.CsrfToken)) {
            if ($TMSession.TMWebSession.Headers.Keys -contains $TMSession.Authentication.CsrfHeaderName) {
                $TMSession.Authentication.CsrfToken = $TMSession.TMWebSession.Headers[$TMSession.Authentication.CsrfHeaderName]
                $TMSession.ApplyHeaderTokens()
            } else {
                throw "A CSRF token could not be found in the TMSession"
            }
        }

        # (Re)apply the tokens to the web sessions' headers

        # Cache the token info
        $OldToken = $TMSession.Authentication.OAuth.AccessToken

        # Form the request headers
        Write-Verbose "Forming web request"
        $TMSession.TMRestSession.Headers.'Content-Type' = "application/json"
        $TMSession.TMRestSession.Headers.'Accept' = "application/json"
        $RestSplat = @{
            Method               = 'POST'
            Uri                  = "https://$($TMSession.TMServer)/tdstm/oauth/access_token"
            WebSession           = $TMSession.TMRestSession
            StatusCodeVariable   = 'StatusCode'
            SkipHttpErrorCheck   = $true
            SkipCertificateCheck = $true
            Form                 = @{
                'grant_type'    = 'refresh_token'
                'refresh_token' = $TMSession.UserAccount.IsLocalAccount ? $TMSession.Authentication.OAuth.RefreshToken : $TMSession.Authentication.OAuth.AccessToken
            }
        }

        try {
            Write-Debug "Web Request Parameters:"
            Write-Debug ($RestSplat | ConvertTo-Json -Depth 10)
            Write-Verbose "Invoking web request"
            $Response = Invoke-RestMethod @RestSplat
            Write-Debug "Response status code: $StatusCode"
            Write-Debug "Response Content: $($Response | ConvertTo-Json)"
        } catch {
            throw $_
        }

        switch ($StatusCode) {
            { $_ -in 200, 204 } {
                if ($Response.status -eq 'error') {
                    throw "Error updating the access token: $($Response.errors)"
                } else {
                    Write-Verbose "Applying new tokens"
                    $TMSession.ParseCallbackData($Response, [TMCallbackDataType]::REST)
                    $TMSession.ApplyHeaderTokens()

                    # Make sure the token actually got updated
                    if ($TMSession.Authentication.OAuth.AccessToken -eq $OldToken) {
                        throw "A new token was not granted by the server"
                    }
                }
            }

            401 {
                throw "The request was unauthorized. Use New-TMsession to log into TransitionManager again"
            }

            403 {
                throw "The server refused the request. It is likely that the original token has expired. Use New-TMSession to receive a new access token."
            }

            default {
                throw "The response status code $StatusCode did not indicate success: $Response"
            }
        }
    }
}