lib/Authentication.ps1


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

function New-SNSession {
    <#
    .SYNOPSIS
    Creates a connection to a ServiceNow instance
     
    .DESCRIPTION
    This function creates a connection to the specified ServiceNow instance using
    the provided credentials. This session can be used when calling other functions within
    the ServiceNow module
     
    .PARAMETER SessionName
    The name that will be used when referring to the created SNSession
     
    .PARAMETER Server
    The URI of the ServiceNow instance to connect to
     
    .PARAMETER Credential
    The credentials to be used twhen connecting to ServiceNow
     
    # .PARAMETER SNVersion
    # Used to override the API version used by other functions in the ServiceNow module
     
    .PARAMETER AllowInsecureSSL
    Boolean indicating whether or not an insecure SSL connection is allowed
 
    .PARAMETER Api
    Boolean indicating wheter or not to also login to the ServiceNow API where possible
     
    .PARAMETER Passthru
    Switch indicating that the newly created SNSession should be output
     
    .PARAMETER ProfileName
    The name of the stored SNProfile which contains the connection settings
     
    .EXAMPLE
    $Session = @{
        SessionName = 'Dev07'
        Server = 'dev68207.service-now.com'
        Credential = (Get-StoredCredential -Name 'dev68207.service-now.com')
    }
    New-SNSession @Session
 
    .EXAMPLE
    Get-SNProfile -Name 'Dev07' | New-SNSession
 
    .EXAMPLE
    New-SNSession -ProfileName 'Dev07'
     
    .OUTPUTS
    None
    #>


    [CmdletBinding()]
    [Alias('Connect-SNServer')]
    param(
        [Parameter(Mandatory = $false,
            ValueFromPipelineByPropertyName = $true)]
        [Alias('Name')]
        [String]$SessionName = 'Default',
        
        [Parameter(Mandatory = $true,
            ParameterSetName = 'ByProperty',
            ValueFromPipelineByPropertyName = $true)]
        [String]$Server,
        
        [Parameter(Mandatory = $true,
            ParameterSetName = 'ByProperty',
            ValueFromPipelineByPropertyName = $true)]
        [PSCredential]$Credential, 

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

        # [Parameter(Mandatory = $false,
        # ParameterSetName = 'ByProperty',
        # ValueFromPipelineByPropertyName = $true)]
        # [Bool]$Api = $true,
        
        [Parameter(Mandatory = $false)]
        [Switch]$Passthru,

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

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

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

    process {

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

        if ($SNProfile) {
            $SessionName = $SNProfile.Name
            $Server = $SNProfile.Server
            $Credential = $SNProfile.Credential
            $AllowInsecureSSL = $SNProfile.AllowInsecureSSL
            if ([String]::IsNullOrWhiteSpace($Project)) {
                $Project = $SNProfile.Project
            }
        }

        ## Check for Existing Session to this server
        if ($global:SNSessions.Keys -contains $SessionName) {
            $NewSNSession = $global:SNSessions[$SessionName]
        }
        else {

            ## Create a session object for this new connection
            $NewSNSession = [SNSession]::new($SessionName, $Server, $AllowInsecureSSL)
        }

        ## Trim the server name
        $Instance = $Server -Replace 'https://', '' -Replace 'http://', ''

        ## Add this shortened Instance name (Just the hostname) into the Session Object
        $NewSNSession.SNServer = $instance

        ##
        ## Create the ServiceNow Headers
        ##
        $base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:{1}" -f $Credential.UserName, $Credential.GetNetworkCredential().Password)))
        
        # Set proper Headers
        $RequestHeaders = New-Object "System.Collections.Generic.Dictionary[[String],[String]]"
        $RequestHeaders.Add('Authorization', ('Basic {0}' -f $base64AuthInfo))
        $RequestHeaders.Add('Accept', 'application/json')

        # Format the uri
        $Uri = "https://$Instance/now/cmdb/meta/cmdb_ci"
        
        $RestMethodSplat = @{
            Method                          = 'GET'
            Uri                             = $Uri 
            Headers                         = $RequestHeaders 
            Body                            = $PostBody 
            SessionVariable                 = 'SNRestSession' 
            PreserveAuthorizationOnRedirect = $true
            ContentType                     = $ContentType 
            SkipCertificateCheck            = $AllowInsecureSSL

        }

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

        ## Look for known conditions

        if ($Response -match 'Sign in to the site to wake your instance') {
            throw 'This instance is sleeping, please log into your ServiceNow Developer account to wake the instance.'
        }

            

        ## Create the SNSessionObject that will be checked/used by the rest of the ServiceNow Modules
        $NewSNSession.SNRestSession = $SNRestSession

        ## Add this Session to the SNSessions list
        $global:SNSessions[$SessionName] = $NewSNSession

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


function Get-SNVersion {
    <#
    .SYNOPSIS
    Gets the version of a given ServiceNow instance
     
    .DESCRIPTION
    This function queries the given ServiceNow 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-SNVersion -Server 'tmddev.ServiceNow.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
    )

    ## Select the Version formatter
    $regex = switch ($Format) {
        "VersionSemVer" { "/Version\ [0-9].[0-9].[0-9]/" }
        "SemVer" { "[0-9].[0-9].[0-9]" }
        "Minor" { "[0-9].[0-9]" }
        Default { "((.|\n)*)" }
    }

    #Honor SSL Settings
    $SNCertSettings = @{SkipCertificateCheck = $AllowInsecureSSL }

    $instance = $Server.Replace('/tdstm', '').Replace('https://', '').Replace('http://', '')

    # Check for a version 4.7, 5.0, 5.1
    $Uri = "https://$instance/tdstm/auth/loginInfo"
    $Response = Invoke-WebRequest -Method Get -Uri $Uri @SNCertSettings
    if ($Response.StatusCode -eq 200) {
        $BuildVersion = ($Response.Content | ConvertFrom-Json).data.buildVersion
        $FinalBuildNumber = Select-String -Pattern $regex -InputObject $BuildVersion
        if ($FinalBuildNumber.Matches.Count -gt 0) {
            $FinalBuildNumber.Matches | Select-Object -First 1 | Select-Object -ExpandProperty Value
        }
    }
    else {

        # Check for 4.6
        $Uri = "https://$instance/tdstm/auth/login"

        try {
            $Response = Invoke-WebRequest -Method Get -Uri $Uri @SNCertSettings
            if ($Response.StatusCode -eq 200) {

                $BuildNumbers = $Response.Content | Select-String -Pattern "Version\ [0-9]\.[0-9]\.[0-9]"
                if ($BuildNumbers.Matches.Count -gt 0) {
                    $BuildNumbers.Matches | Select-String -Pattern $regex | ForEach-Object { $_.Matches } | Select-Object -First 1
                } 
            }
        }
        catch {
            throw "Could not get version"
        }
    }
}


function Get-SNSession {
    <#
    .SYNOPSIS
    Gets a SNSession by Name, Server or Version
     
    .PARAMETER Name
    One or more SNSession names to get
     
    .PARAMETER Server
    One or more SN servers for which a SNSession has been created
     
    .PARAMETER Version
    One or more SN server versions for which a SNSession has been created
     
    .EXAMPLE
    Get-SNSession -Name 'Default', 'SNDDEV'
 
    .EXAMPLE
    Get-SNSession -Version '5.0.*'
 
    .EXAMPLE
    Get-SNSession -Server '*.ServiceNow.net'
     
    .OUTPUTS
    A hashtable with the SNSession details
    #>


    [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('SNServer')]
        [String[]]$Server,

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

    begin {
        $Keys = @($Global:SNSessions.Keys)
    }

    process {
        switch ($PSCmdlet.ParameterSetName) {
            'ByName' {
                if ($Name) {
                    $Name | ForEach-Object {
                        foreach ($Key in $Keys) {
                            if ($Key -match $_) {
                                $Global:SNSessions[$Key]
                            }
                        }
                    }
                }
                else {
                    foreach ($Key in $Keys) {
                        $Global:SNSessions[$Key]
                    }
                }
            }

            'ByServer' {
                $Server | ForEach-Object {
                    foreach ($Key in $Keys) {
                        if ($Global:SNSessions[$Key].SNServer -match $_) {
                            $Global:SNSessions[$Key]
                        }
                    }
                }
            }

            'ByVersion' {
                $Version | ForEach-Object {
                    foreach ($Key in $Keys) {
                        if ($Global:SNSessions[$Key].SNVersion.Value -match $_) {
                            $Global:SNSessions[$Key]
                        }
                    }
                }
            }

            default {
                $Global:SNSessions
            }
        }
    }

}


function Remove-SNSession {
    <#
    .SYNOPSIS
    Disconnects one or more sessions with a ServiceNow instance
     
    .DESCRIPTION
    This function signs out of one or more ServiceNow instances and removes the session
    from the $Global:SNSessions variable
     
    .PARAMETER Name
    The name of one or more SNSessions to disconnect and remove
     
    .PARAMETER InputObject
    One or more SNSessions to disconnect and remove
     
    .EXAMPLE
    Remove-SNSession -Name 'Default', 'SNDDEV'
     
    .EXAMPLE
    Get-SNSession | Remove-SNSession
     
    .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:SNSessions[$_] }
            }
            
            'ByObject' {
                $SessionsToRemove = $InputObject
            }
        }

        # Iterate over each session that was passed and sign out of SN
        foreach ($Session in $SessionsToRemove) {
            Write-Host "Logging out of ServiceNow instance [ " -NoNewline
            Write-Host $Session.SNServer -ForegroundColor Cyan -NoNewline
            Write-Host " ]"
            
            # Format the parameters for the sign out request
            $WebRequestSplat = @{
                Method                          = 'POST'
                Uri                             = "https://$($Session.SNServer)/tdstm/auth/signOut"
                WebSession                      = $Session.SNWebSession
                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:SNSessions.Remove($Session.Name)
                }
                else {
                    Write-Error "Could not sign out of ServiceNow"
                }
            }
            catch {
                Write-Error $_
            }
        }
    }
}


function New-SNProfile {
    <#
    .SYNOPSIS
    Creates a ServiceNow connection profile on the local hard drive
     
    .DESCRIPTION
    This function creates a ServiceNow connection profile on the localk hard drive
    that can be used with the New-SNSession function to more easily create connections to
    ServiceNow instances
     
    .PARAMETER Name
    The name name of the profile that will be saved. This name will be used to reference the
    SNProfile in other functions.
     
    .PARAMETER Server
    The URI of the ServiceNow instance
     
    .PARAMETER Project
    The name of the project on the ServiceNow instance
     
    .PARAMETER Credential
    The credentials used to connect to ServiceNow
     
    .PARAMETER AllowInsecureSSL
    Boolean indicating whether or not an insecure SSL connection is allowed
 
    .PARAMETER Passthru
    Switch indicating that the newly created SNProfile should be output
     
    .EXAMPLE
    New-SNProfile -Name 'SNDDEV-RVTools' -Server 'tmddev.ServiceNow.net' -Project 'RD - RVTools' -Credential (Get-Credential)
 
    .EXAMPLE
    $Profile = @{
        Name = 'SNDDEV2'
        Server = 'tmddev2.ServiceNow.net'
        Credential = (Get-StoredCredential -Name 'ME')
    }
    New-SNProfile @Profile -Passthru | New-SNSession
     
    .OUTPUTS
    If Passthru switch is used, a SNProfile 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)]
        [Bool]$Api = $true,

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

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

        $SNProfile = [SNProfile]::new($Name, $Server, $Project, $Credential, $AllowInsecureSSL, $Api)
        
        $SNProfile | Export-Clixml -Path (Join-Path $ProfileDirectory "$Name.tmprofile")

        if ($Passthru) {
            $SNProfile
        }
    }
}


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


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

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

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


function Remove-SNProfile {
    <#
    .SYNOPSIS
    Removes a previously stored SNProfile 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 SNProfile to be removed
     
    .PARAMETER OverwriteCount
    The number of times to overwrite the file before removing it
     
    .EXAMPLE
    Remove-SNProfile -Name 'SNDDEV2'
 
    .EXAMPLE
    Get-SNProfile | Remove-SNProfile
     
    .NOTES
    General notes
    #>

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

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

    process {
        $ProfileFilePath = Join-Path $HOME 'TMD_Files' '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 ServiceNow profile with the name '$Name' does not exist."),
                "SND.AUTH02",
                [System.Management.Automation.ErrorCategory]::InvalidArgument,
                $ProfileFilePath
            )
            $PSCmdlet.WriteError($ErrorRecord)
        }
    }
}