lib/Authentication.ps1


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

function New-ZertoSession {
    <#
    .SYNOPSIS
    Creates a connection to a Zerto VM instance
 
    .DESCRIPTION
    This function creates a connection to the specified Zerto instance using
    the provided credentials. This session can be used when calling other functions within
    the Zerto module
 
    .PARAMETER SessionName
    The name that will be used when referring to the created ZertoSession
 
    .PARAMETER Server
    The URI of the Zerto instance to connect to
 
    .PARAMETER Credential
    The credentials to be used twhen connecting to Zerto
 
    .PARAMETER AllowInsecureSSL
    Boolean indicating whether or not an insecure SSL connection is allowed
 
    .PARAMETER ProfileName
    The name of the stored ZertoProfile which contains the connection settings
 
    .EXAMPLE
    $Session = @{
        SessionName = 'TMDDEV'
        Server = 'tmddev.Zerto.net'
        Credential = (Get-StoredCredential -Name 'ME')
    }
    New-ZertoSession @Session
 
    .EXAMPLE
    Get-ZertoProfile -Name 'Zerto.example.com' | New-ZertoSession
 
    .EXAMPLE
    New-ZertoSession -ProfileName 'Zerto.example.com'
 
    .OUTPUTS
    None
    #>


    [CmdletBinding()]
    [Alias('Connect-ZertoServer')]
    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)]
        [Int]$Port = 9669,

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

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

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

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

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

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

    process {

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

        if ($ZertoProfile) {
            $SessionName = (($global:ZertoSessions.Keys -contains 'Default') -and ($SessionName -eq 'Default')) ? $ZertoProfile.Name : 'Default'
            $Server = $ZertoProfile.Server
            $Credential = $ZertoProfile.Credential
            $AllowInsecureSSL = $ZertoProfile.AllowInsecureSSL
        }
        else {
            if ($SessionName -eq 'Default') {
                $SessionName = (($global:ZertoSessions.Keys -contains 'Default') -and ($SessionName -eq 'Default')) ? $Server : 'Default'
            }
        }

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

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

        ## Honor SSL Settings from the user
        $CertSettings = @{ SkipCertificateCheck = $AllowInsecureSSL }

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

        ## Save the Instance and Port used
        $NewZertoSession.ZertoServer = $Instance
        $NewZertoSession.ZertoPort = $Port

        ## Prepare Request Headers for use in the Session Header Cache
        $ContentType = 'application/json;charset=UTF-8'

        # Format the uri
        $uri = "https://$($Instance):$($Port)/v1/session/add"

        # Authenticating with Zerto APIs - Basic AUTH over SSL
        if ($Credential.GetNetworkCredential().domain) {
            $authInfo = ("{0}\{1}:{2}" -f $Credential.GetNetworkCredential().domain , $Credential.GetNetworkCredential().UserName, $Credential.GetNetworkCredential().Password )
        }
        else {
            $authInfo = ("{0}:{1}" -f $Credential.GetNetworkCredential().UserName, $Credential.GetNetworkCredential().Password )
        }
        $authInfo = [System.Text.Encoding]::UTF8.GetBytes($authInfo)
        $authInfo = [System.Convert]::ToBase64String($authInfo)
        $RequestHeaders = @{
            "Authorization"  = ("Basic {0}" -f $authInfo)
            "Accept-Version" = "1.0";
            "Content-Type"   = $ContentType;
            "Accept"         = "application/json";
            "Cache-Control"  = "no-cache";
        }

        $WebRequestSplat = @{
            Method                          = 'POST'
            Uri                             = $uri
            Headers                         = $RequestHeaders
            SessionVariable                 = 'ZertoWebSession'
            PreserveAuthorizationOnRedirect = $true
            ContentType                     = $ContentType
        }

        ## Attempt Login
        if ($VerbosePreference -eq 'Continue') {
            Write-Host "Logging into Zerto 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 @CertSettings -ProgressAction 'SilentlyContinue'
            Write-Verbose "Response status code: $($Response.StatusCode)"
            Write-Verbose "Response Content: $($Response.Content)"
            
            ## Check the Response code for 200
            if ($Response.StatusCode -eq 200) {
                $ZertoSessionToken = $Response.headers.get_item("x-zerto-session") | Select-Object -First 1
                $ZertoWebSession.Headers["x-zerto-session"] = $ZertoSessionToken
                $ZertoWebSession.Headers.Remove('Authorization')
                $NewZertoSession.ZertoVersion = 9
            }
        }
        catch {
            ## Check for a 404, which would possibly indicate that the server is a v10
            if ($_.Exception.StatusCode -eq [System.Net.HttpStatusCode]::NotFound) {
                Write-Verbose "Session endpoint is missing, this might be a v10 system"
            }
            else {
                throw $_
            }
        }

        ## Check for API Version change 9 -> 10
        if (-Not $Response) {

            # Make the request
            try {
                $uri = "https://$($Instance):$($Port)/auth/realms/zerto/protocol/openid-connect/token"
                $WebRequestSplat = @{
                    Method                          = 'POST'
                    Uri                             = $uri
                    SessionVariable                 = 'ZertoWebSession'
                    PreserveAuthorizationOnRedirect = $true
                    Body                            = @{
                        grant_type = 'password'
                        client_id  = 'zerto-client'
                        username   = $Credential.UserName
                        password   = $Credential.GetNetworkCredential().Password
                    }
                }
                Write-Verbose "Web Request Parameters:"
                Write-Verbose ($WebRequestSplat | ConvertTo-Json -Depth 10)
                Write-Verbose "Invoking web request"
        
                ## Honor SSL Settings from the user
                $CertSettings = @{ SkipCertificateCheck = $AllowInsecureSSL }
                $Response = Invoke-WebRequest @WebRequestSplat @CertSettings -ProgressAction 'SilentlyContinue'
                Write-Verbose "Response status code: $($Response.StatusCode)"
                Write-Verbose "Response Content: $($Response.Content)"

                ## Check the Response code for 200
                if ($Response.StatusCode -eq 200) {
                    $ResponseContent = $Response.Content | ConvertFrom-Json
                    $ZertoWebSession.Headers['Authorization'] = "Bearer $($ResponseContent.access_token)"
                    $NewZertoSession.ZertoVersion = 10
                    Write-Verbose "Now: $(Get-Date)"
                    
                    $NewZertoSession.AccessToken = $ResponseContent.access_token
                    $NewZertoSession.TokenExpires = (Get-Date).AddSeconds($ResponseContent.expires_in)
                    Write-Verbose "Token Expires: $($NewZertoSession.TokenExpires)"
                    
                    $NewZertoSession.RefreshToken = $ResponseContent.refresh_token
                    $NewZertoSession.RefreshTokenExpires = (Get-Date).AddSeconds($ResponseContent.refresh_expires_in)
                    Write-Verbose "Refresh Token Expires: $($NewZertoSession.RefreshTokenExpires)"
                }
            }
            catch {
                Write-Host $_.Exception.Message
                Write-Host $_.Exception.InnerException.Message
                throw $_
            }
        }

        $NewZertoSession.ZertoWebSession = $ZertoWebSession

        ## Add this Session to the ZertoSessions list
        $global:ZertoSessions[$SessionName] = $NewZertoSession

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

Function Get-ZertoSession {
    [CmdletBinding(DefaultParameterSetName = 'ListAll')]
    param(
        [Parameter(HelpMessage = 'Zerto Session Name', ParameterSetName='ByName')][String]$SessionName = 'Default',
        [Parameter(HelpMessage = 'List All Zerto Sessions', ParameterSetName='ListAll')][switch]$List
    )
    
    if($List.IsPresent){
        return $global:ZertoSessions.Keys
    }

    ## Check for Existing Session to this server
    if ($global:ZertoSessions -and $global:ZertoSessions.Keys -contains $SessionName) {
        $ZertoSessionConfig = $global:ZertoSessions[$SessionName]
    } else {
        Write-Host 'ZertoSession: [' -NoNewline
        Write-Host $SessionName -ForegroundColor Cyan
        Write-Host '] was not Found. Please use the New-ZertoSession command.'
        Throw "Zerto Session [$($SessionName)] Not Found. Use New-ZertoSession command before using features."
    }

    if ($ZertoSessionConfig.ZertoVersion -eq "10") {
        
        ## Set time of Now with a bit of an offset to force refresh before expiration
        $Now = (Get-Date).AddSeconds(-10)
    
        if($ZertoSessionConfig.TokenExpires -lt $Now){
            Write-Verbose "Token Is Expired"
            if($ZertoSessionConfig.RefreshTokenExpires -gt $Now){
                
                 Write-Verbose "Token Refresh is still valid. Refreshing Token."
                # Make the request
                $uri = "https://$($ZertoSessionConfig.ZertoServer):$($ZertoSessionConfig.ZertoPort)/auth/realms/zerto/protocol/openid-connect/token"
                $WebRequestSplat = @{
                    Method                          = 'POST'
                    Uri                             = $uri
                    SessionVariable                 = 'ZertoWebSession'
                    PreserveAuthorizationOnRedirect = $true
                    Body                            = @{
                        grant_type = 'refresh_token'
                        client_id = 'zerto-client' 
                        refresh_token  = $ZertoSessionConfig.RefreshToken
                    }
                }

                Write-Verbose "Web Request Parameters:"
                Write-Verbose ($WebRequestSplat | ConvertTo-Json -Depth 10)
                Write-Verbose "Invoking web request"
                $CertSettings = @{ SkipCertificateCheck = $ZertoSessionConfig.AllowInsecureSSL }
                $Response = Invoke-WebRequest @WebRequestSplat @CertSettings -ProgressAction 'SilentlyContinue'
                Write-Verbose "Response status code: $($Response.StatusCode)"
                Write-Verbose "Response Content: $($Response.Content)"

                ## Check the Response code for 200
                if ($Response.StatusCode -eq 200) {
                    $ResponseContent = $Response.Content | ConvertFrom-Json
                    $ZertoSessionConfig.ZertoWebSession.Headers['Authorization'] = "Bearer $($ResponseContent.access_token)"
                    $ZertoSessionConfig.ZertoVersion = 10
                    
                    $ZertoSessionConfig.AccessToken = $ResponseContent.access_token
                    $ZertoSessionConfig.TokenExpires = (Get-Date).AddSeconds($ResponseContent.expires_in)
                    
                    $ZertoSessionConfig.RefreshToken = $ResponseContent.refresh_token
                    $ZertoSessionConfig.RefreshTokenExpires = (Get-Date).AddSeconds($ResponseContent.refresh_expires_in)    
                }
            }
        }
    }

    return $ZertoSessionConfig
}

function Remove-ZertoSession {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $false,
            ValueFromPipelineByPropertyName = $true)]
        [Alias('Name')]
        [String]$SessionName = 'Default'
    )

    ## Check for Existing Session to this server
    if ($global:ZertoSessions.Keys -contains $SessionName) {
        $TheSession = $global:ZertoSessions[$SessionName]
    }
    # $ThisSession.Remove()
}