DSCResources/ArcGIS_Portal_TLS/ArcGIS_Portal_TLS.psm1

$modulePath = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath 'Modules'

# Import the ArcGIS Common Modules
Import-Module -Name (Join-Path -Path $modulePath `
        -ChildPath (Join-Path -Path 'ArcGIS.Common' `
            -ChildPath 'ArcGIS.Common.psm1'))

<#
    .SYNOPSIS
        Creates a SelfSigned Certificate or Installs a SSL Certificated Provided and Configures it with Portal.
    .PARAMETER PortalHostName
        Portal Endpoint with which the Certificate will be associated.
    .PARAMETER SiteAdministrator
        A MSFT_Credential Object - Primary Site Administrator.
    .PARAMETER CertificateFileLocation
        Certificate Path from where to fetch the certificate to be installed.
    .PARAMETER CertificatePassword
        Sercret Certificate Password or Key.
    .PARAMETER WebServerCertificateAlias
        CName/Alias with which the Certificate will be associated.
    .PARAMETER SslRootOrIntermediate
        List of RootOrIntermediate Certificates
#>


function Get-TargetResource
{
    [CmdletBinding()]
    [OutputType([System.Collections.Hashtable])]
    param
    (
        [parameter(Mandatory = $true)]
        [System.String]
        $PortalHostName
    )

    $null 
}

function Set-TargetResource
{
    [CmdletBinding()]
    param
    (
        [parameter(Mandatory = $true)]
        [System.String]
        $PortalHostName,

        [System.Management.Automation.PSCredential]
        $SiteAdministrator,
        
        [System.String]
        $CertificateFileLocation,

        [System.Management.Automation.PSCredential]
        $CertificatePassword,

        [System.String]
        $WebServerCertificateAlias,

        [System.String]
        $SslRootOrIntermediate
    )

    [System.Reflection.Assembly]::LoadWithPartialName("System.Web") | Out-Null
    
    $VersionGreaterThan1071 = $true
    $FQDN = if($PortalHostName) { Get-FQDN $PortalHostName} else { Get-FQDN $env:COMPUTERNAME }
    $PortalUrl = "https://$($FQDN):7443"  
    $Referer = $PortalUrl
    try{
        Wait-ForUrl "$PortalURL/arcgis/portaladmin/healthCheck/?f=json" -Verbose
        Wait-ForUrl "$PortalURL/arcgis/sharing/rest/generateToken" -Verbose
        $token = Get-PortalToken -PortalHostName $FQDN -Credential $SiteAdministrator -Referer $Referer 
    }catch{
        throw "[WARNING] Unable to get token:- $_"
    }
    if(-not($token.token)){
        throw "Unable to retrieve Portal Token for '$($PortalAdministrator.UserName)'"
    }else{
        Write-Verbose "Retrieved Portal Token"
    }
    $Info = Invoke-ArcGISWebRequest -Url "$($PortalUrl)/arcgis/portaladmin/" -HttpFormParameters @{ f = 'json'; token = $token.token; } -Referer $Referer -Verbose -HttpMethod 'GET'
    $VersionArray = "$($Info.version)".Split('.')
    [System.Boolean]$VersionGreaterThan1071 = ($VersionArray[0] -eq 11 -or ($VersionArray[0] -eq 10 -and $VersionArray[1] -gt 7))

    if($CertificateFileLocation) 
    {
        if($WebServerCertificateAlias -and $WebServerCertificateAlias -as [ipaddress]) {
            Write-Verbose "Adding Host mapping for $WebServerCertificateAlias"
            Add-HostMapping -hostname $WebServerCertificateAlias -ipaddress $WebServerCertificateAlias        
        }

        if((Test-Path $CertificateFileLocation))
        {
            try{
                $Certs = Get-SSLCertificatesForPortal -PortalURL $PortalURL -Token $token.token -Referer $Referer -MachineName $FQDN -VersionGreaterThan1071 $VersionGreaterThan1071 
            }catch{
                throw "[WARNING] Unable to get SSL-CertificatesForPortal:- $_"
            }
            Write-Verbose "Current Alias for SSL Certificate:- '$($Certs.webServerCertificateAlias)' Certificates:- '$($Certs.sslCertificates -join ',')'"

            $ImportExistingCertFlag = $False
            $DeleteTempCert = $False
            if(-not($Certs.sslCertificates -icontains $WebServerCertificateAlias)){
                Write-Verbose "Importing SSL Certificate with alias $WebServerCertificateAlias"
                $ImportExistingCertFlag = $True
            }else{
                Write-Verbose "SSL Certificate with alias $WebServerCertificateAlias already exists"
                $CertForMachine = Get-SSLCertificatesForPortal -PortalURL $PortalURL -Token $token.token -Referer $Referer -WebServerCertificateAlias $WebServerCertificateAlias.ToLower() -MachineName $FQDN -VersionGreaterThan1071 $VersionGreaterThan1071 
                Write-Verbose "Examine certificate from $CertificateFileLocation"
                $cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2
                $cert.Import($CertificateFileLocation, $CertificatePassword.GetNetworkCredential().Password, 'DefaultKeySet')
                $NewCertThumbprint = $cert.Thumbprint
                Write-Verbose "Thumbprint for the supplied certificate is $NewCertThumbprint"
                if($CertForMachine.sha1Fingerprint -ine $NewCertThumbprint){
                    $ImportExistingCertFlag = $True
                    Write-Verbose "Importing exsting certificate with alias $($WebServerCertificateAlias)-temp"
                    try{
                        Import-ExistingCertificate -PortalURL $PortalURL -Token $token.token `
                                                    -Referer $Referer -CertAlias "$($WebServerCertificateAlias)-temp" -CertificateFilePath $CertificateFileLocation `
                                                    -CertificatePassword $CertificatePassword -MachineName $FQDN -VersionGreaterThan1071 $VersionGreaterThan1071
                        $DeleteTempCert = $True
                    }catch{
                        throw "[WARNING] Error Import-ExistingCertificate:- $_"
                    }

                    try{
                        Update-PortalSSLCertificate -PortalURL $PortalURL -Token $token.token -Referer $Referer -CertAlias "$($WebServerCertificateAlias)-temp" -MachineName $FQDN -VersionGreaterThan1071 $VersionGreaterThan1071 -Verbose
                        Write-Verbose "Updating to a temp SSL Certificate causes the web server to restart asynchronously. Waiting 60 seconds before checking for intitialization"
                        Start-Sleep -Seconds 60
                        Wait-ForUrl "$PortalURL/arcgis/portaladmin/healthCheck/?f=json" -Verbose
                        Wait-ForUrl "$PortalURL/arcgis/sharing/rest/generateToken" -Verbose
                    }catch{
                        throw "[WARNING] Unable to Update-PortalSSLCertificate:- $_"
                    }
                    try{
                        Write-Verbose "Deleting Portal Certificate with alias $WebServerCertificateAlias"
                        Invoke-DeletePortalCertificate -PortalURL $PortalURL -Token $token.token -Referer $Referer -WebServerCertificateAlias $WebServerCertificateAlias -MachineName $FQDN -VersionGreaterThan1071 $VersionGreaterThan1071
                    }catch{
                        throw "[WARNING] Unable to Invoke-DeletePortalCertificate:- $_"
                    }
                }
            }

            if($ImportExistingCertFlag){
                Write-Verbose "Importing exsting certificate with alias $WebServerCertificateAlias"
                try{
                    Import-ExistingCertificate -PortalURL $PortalURL -Token $token.token `
                        -Referer $Referer -CertAlias $WebServerCertificateAlias -CertificateFilePath $CertificateFileLocation -CertificatePassword $CertificatePassword -MachineName $FQDN -VersionGreaterThan1071 $VersionGreaterThan1071 
                }catch{
                    throw "[WARNING] Error Import-ExistingCertificate:- $_"
                }
            }
            
            $Certs = Get-SSLCertificatesForPortal -PortalURL $PortalURL -Token $token.token -Referer $Referer -MachineName $FQDN -VersionGreaterThan1071 $VersionGreaterThan1071
            if(($Certs.webServerCertificateAlias -ine $WebServerCertificateAlias) -or $ImportExistingCertFlag) {
                Write-Verbose "Updating Alias to use $WebServerCertificateAlias"
                try{
                    Update-PortalSSLCertificate -PortalURL $PortalURL -Token $token.token -Referer $Referer -CertAlias $WebServerCertificateAlias -MachineName $FQDN -VersionGreaterThan1071 $VersionGreaterThan1071 -Verbose
                    Write-Verbose "Updating an SSL Certificate causes the web server to restart asynchronously. Waiting 60 seconds before checking for intitialization"
                    Start-Sleep -Seconds 60
                    Wait-ForUrl "$PortalURL/arcgis/portaladmin/healthCheck/?f=json" -Verbose
                    Wait-ForUrl "$PortalURL/arcgis/sharing/rest/generateToken" -Verbose

                    if($DeleteTempCert){
                        Write-Verbose "Deleting Temp Certificate with alias $($WebServerCertificateAlias)-temp"
                        Invoke-DeletePortalCertificate -PortalURL $PortalURL -Token $token.token -Referer $Referer -WebServerCertificateAlias "$($WebServerCertificateAlias)-temp" -MachineName $FQDN -VersionGreaterThan1071 $VersionGreaterThan1071 
                    }
                }catch{
                    throw "[WARNING] Unable to Update-PortalSSLCertificate:- $_"
                }
            }else{
                Write-Verbose "SSL Certificate alias $WebServerCertificateAlias is the current one"
            }     
            
            Wait-ForUrl "$PortalURL/arcgis/portaladmin/healthCheck/?f=json" -Verbose
            Wait-ForUrl "$PortalURL/arcgis/sharing/rest/generateToken" -Verbose
        }else{
            throw "[ERROR] - CertificateFileLocation '$CertificateFileLocation' is not acccesible"
        }
    }else{
        Write-Verbose "CertificateFileLocation not specified. Skipping web server certificate configuration"
    }

    # test and set RootOrIntermediateCertificate
    if($null -ne $SslRootOrIntermediate){
        $Certs = Get-SSLCertificatesForPortal -PortalURL $PortalURL -Token $token.token -Referer $Referer -MachineName $FQDN -VersionGreaterThan1071 $VersionGreaterThan1071 -ErrorAction SilentlyContinue
        $RestartRequired = $False
        foreach ($key in ($SslRootOrIntermediate | ConvertFrom-Json)){
            if ($Certs.sslCertificates -icontains $key.Alias){
                Write-Verbose "Set RootOrIntermediate $($key.Alias) is in List of SSL-Certificates no Action Required"
            }else{
                Write-Verbose "Set RootOrIntermediate $($key.Alias) is NOT in List of SSL-Certificates Import-RootOrIntermediate"
                try{
                    Import-RootOrIntermediateCertificate -PortalURL $PortalURL -Token $token.token -Referer $Referer -CertAlias $key.Alias -CertificateFilePath $key.Path -MachineName $FQDN -VersionGreaterThan1071 $VersionGreaterThan1071
                    if(-not($RestartRequired)){
                        $RestartRequired = $True
                    }
                }catch{
                    Write-Verbose "Error in Import-RootOrIntermediateCertificate :- $_"
                }
            }
        }
        if($RestartRequired){
            Write-Verbose "Portal Root and intermediate certificates were updated. Restarting Portal."
            Restart-ArcGISService -ServiceName 'Portal for ArcGIS' -Verbose
            Write-Verbose "Waiting 30 seconds before checking for intitialization"
            Start-Sleep -Seconds 30
            Wait-ForUrl "$PortalURL/arcgis/portaladmin/healthCheck/?f=json" -Verbose
            Wait-ForUrl "$PortalURL/arcgis/sharing/rest/generateToken" -Verbose
        }
    }
}

function Test-TargetResource
{
    [CmdletBinding()]
    [OutputType([System.Boolean])]
    param
    (
        [parameter(Mandatory = $true)]
        [System.String]
        $PortalHostName,

        [System.Management.Automation.PSCredential]
        $SiteAdministrator,

        [System.String]
        $CertificateFileLocation,

        [System.Management.Automation.PSCredential]
        $CertificatePassword,

        [System.String]
        $WebServerCertificateAlias,

        [System.String]
        $SslRootOrIntermediate
    )

    [System.Reflection.Assembly]::LoadWithPartialName("System.Web") | Out-Null
    $result = $True
    $FQDN = if($PortalHostName) { Get-FQDN $PortalHostName } else { Get-FQDN $env:COMPUTERNAME }
    $PortalURL = "https://$($FQDN):7443" 
    $Referer = $PortalURL
    $token = $null
    Wait-ForUrl "$PortalURL/arcgis/portaladmin/healthCheck/?f=json" -Verbose
    Wait-ForUrl "$PortalURL/arcgis/sharing/rest/generateToken" -Verbose
    try{ 
        $token = Get-PortalToken -PortalHostName $FQDN -Credential $SiteAdministrator -Referer $Referer -MaxAttempts 30
    } catch {
        Write-Verbose "[WARNING] Unable to get token:- $_."
    }
    if(-not($token.token)) {
        throw "Unable to retrieve Portal Token for '$($SiteAdministrator.UserName)'"
    }else {
        Write-Verbose "Retrieved Portal Token"
    }

    $Info = Invoke-ArcGISWebRequest -Url "$($PortalURL)/arcgis/portaladmin/" -HttpFormParameters @{f = 'json'; token = $token.token; } -Referer $Referer -HttpMethod 'GET'
    $VersionArray = "$($Info.version)".Split('.')
    [System.Boolean]$VersionGreaterThan1071 = ($VersionArray[0] -eq 11 -or ($VersionArray[0] -eq 10 -and $VersionArray[1] -gt 7))

    if($WebServerCertificateAlias){
        Write-Verbose "Retrieve SSL Certificate for Portal from $FQDN and checking for Alias $WebServerCertificateAlias"
        $Certs = $null
        try{
            $Certs = Get-SSLCertificatesForPortal -PortalURL $PortalURL -Token $token.token -Referer $Referer -MachineName $FQDN -VersionGreaterThan1071 $VersionGreaterThan1071
            Write-Verbose "Number of certificates:- $($Certs.sslCertificates.Length) Certificates:- '$($Certs.sslCertificates -join ',')' Current Alias :- '$($Certs.webServerCertificateAlias)'"   
        }catch{
            Write-Verbose "Error in Get-SSLCertificatesForPortal:- $_"
            throw $_
        }

        if(($null -ne $Certs) -and ($Certs.sslCertificates -iContains $WebServerCertificateAlias) -and ($Certs.webServerCertificateAlias -ieq $WebServerCertificateAlias)){
            Write-Verbose "Certificate $($Certs.webServerCertificateAlias) matches expected alias of '$WebServerCertificateAlias'"
            $CertForMachine = Get-SSLCertificatesForPortal -PortalURL $PortalURL -Token $token.token -Referer $Referer -WebServerCertificateAlias $WebServerCertificateAlias -VersionGreaterThan1071 $VersionGreaterThan1071 -MachineName $FQDN
            if($CertificateFileLocation -and ($null -ne $CertificatePassword)) {
                Write-Verbose "Examine certificate from $CertificateFileLocation"
                $cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2
                $cert.Import($CertificateFileLocation, $CertificatePassword.GetNetworkCredential().Password, 'DefaultKeySet')
                $NewCertThumbprint = $cert.Thumbprint
                Write-Verbose "Thumbprint for the supplied certificate is $NewCertThumbprint"
                if($CertForMachine.sha1Fingerprint -ine $NewCertThumbprint){
                    Write-Verbose "Thumbprint for the supplied certificate doesn't match the existing one"
                    $result = $false
                }else{
                    Write-Verbose "Thumbprint for the supplied certificate matches the existing one"
                    $result = $True
                }
            }
        }
        else {
            Write-Verbose "Certificate $($Certs.webServerCertificateAlias) does not match expected alias of '$WebServerCertificateAlias'"
            $result = $False
        }
    }

    if ($result -and -not([string]::IsNullOrEmpty($SslRootOrIntermediate))) { 
        $Certs = Get-SSLCertificatesForPortal -PortalURL $PortalURL -Token $token.token -Referer $Referer -MachineName $FQDN -VersionGreaterThan1071 $VersionGreaterThan1071 -ErrorAction SilentlyContinue
        foreach ($key in ($SslRootOrIntermediate | ConvertFrom-Json)){
            if ($Certs.sslCertificates -icontains $key.Alias){
                Write-Verbose "Test RootOrIntermediate $($key.Alias) is in List of SSL-Certificates"
                #TODO - Check thumbprint
            }else{
                $result = $False
                Write-Verbose "Test RootOrIntermediate $($key.Alias) is NOT in List of SSL-Certificates"
                break;
            }
        }
    }

    $result
}

function Get-SSLCertificatesForPortal
{
    [CmdletBinding()]
    param(
        [System.String]
        $PortalURL,

        [System.String]
        $WebServerCertificateAlias,

        [System.String]
        $Token,

        [System.String]
        $Referer,

        [System.String]
        $MachineName,

        [System.Boolean]
        $VersionGreaterThan1071 
    )

    try {
        $URL = if($VersionGreaterThan1071){ $PortalURL.TrimEnd("/") + "/arcgis/portaladmin/machines/$MachineName/sslCertificates" } else { $PortalURL.TrimEnd("/") + "/arcgis/portaladmin/security/sslCertificates" }
        if($WebServerCertificateAlias){ $URL = $URL + "/$($WebServerCertificateAlias)" }
        Invoke-ArcGISWebRequest -Url $URL -HttpFormParameters @{ f = 'json'; token = $Token } -Referer $Referer -HttpMethod 'GET' -TimeOutSec 120
    }
    catch {
        Write-Verbose "[WARNING]:- Get-SSLCertificatesForPortal encountered an error during execution. Error:- $_"
    }
}

function Invoke-DeletePortalCertificate{
    [CmdletBinding()]
    param(
        [System.String]
        $PortalURL,

        [System.String]
        $WebServerCertificateAlias,

        [System.String]
        $Token,

        [System.String]
        $Referer,

        [System.String]
        $MachineName,

        [System.Boolean]
        $VersionGreaterThan1071
    )
    try {
        $URL = if($VersionGreaterThan1071){ $PortalURL.TrimEnd("/") + "/arcgis/portaladmin/machines/$MachineName/sslCertificates/$($WebServerCertificateAlias)/delete" }else{ $PortalURL.TrimEnd("/") + "/arcgis/portaladmin/security/sslCertificates/$($WebServerCertificateAlias)/delete" }
        Invoke-ArcGISWebRequest -Url $URL -HttpFormParameters @{ f = 'json'; token = $Token } -Referer $Referer -HttpMethod 'POST' -TimeOutSec 120
    }catch{
        Write-Verbose "[WARNING]:- Invoke-DeletePortalCertificate encountered an error during execution. Error:- $_"
    }
}

function Import-ExistingCertificate
{
    [CmdletBinding()]
    param(
        [System.String]
        $PortalURL, 

        [System.String]
        $Token, 

        [System.String]
        $Referer, 

        [System.String]
        $CertAlias, 

        [System.Management.Automation.PSCredential]
        $CertificatePassword, 

        [System.String]
        $CertificateFilePath,

        [System.String]
        $MachineName,

        [System.Boolean]
        $VersionGreaterThan1071
    )
    $ImportCertUrl = if($VersionGreaterThan1071){ $PortalURL.TrimEnd("/") + "/arcgis/portaladmin/machines/$MachineName/sslCertificates/importExistingServerCertificate" }else{ $PortalURL.TrimEnd("/") + "/arcgis/portaladmin/security/sslCertificates/importExistingServerCertificate" }
    
    $props = @{ f= 'json'; token = $Token; alias = $CertAlias; password = $CertificatePassword.GetNetworkCredential().Password  }    
    $res = Invoke-UploadFile -url $ImportCertUrl -filePath $CertificateFilePath -fileContentType 'application/x-pkcs12' -formParams $props -Referer $Referer -fileParameterName 'file'    
    if($res -and $res.Content) {
        $response = $res | ConvertFrom-Json
        Confirm-ResponseStatus $response -Url $ImportCertUrl
    } else {
        Write-Verbose "[WARNING] Response from $ImportCertUrl was null"
    }
}

function Import-RootOrIntermediateCertificate
{
    [CmdletBinding()]
    param(
        [System.String]
        $PortalURL, 

        [System.String]
        $Token, 

        [System.String]
        $Referer, 

        [System.String]
        $CertAlias, 

        [System.String]
        $CertificateFilePath,

        [System.String]
        $MachineName,

        [System.Boolean]
        $VersionGreaterThan1071
    )

    $ImportCertUrl = if($VersionGreaterThan1071){ $PortalURL.TrimEnd("/") + "/arcgis/portaladmin/machines/$MachineName/sslCertificates/importRootOrIntermediate" }else{ $PortalURL.TrimEnd("/") + "/arcgis/portaladmin/security/sslCertificates/importRootOrIntermediate" }
    $props = @{ f= 'json'; token = $Token; alias = $CertAlias; norestart = $true }
    $res = Invoke-UploadFile -url $ImportCertUrl -filePath $CertificateFilePath -fileContentType 'application/x-pkcs12' -formParams $props -Referer $Referer -fileParameterName 'file'    
    if($res -and $res.Content) {
        $response = $res | ConvertFrom-Json
        Confirm-ResponseStatus $response -Url $ImportCertUrl
    } else {
        Write-Verbose "[WARNING] Response from $ImportCertUrl was null"
    }
}

function Update-PortalSSLCertificate
{
    [CmdletBinding()]
    param(
        [System.String]
        $PortalURL, 

        [System.String]
        $Token, 

        [System.String]
        $Referer, 

        [System.String]
        $CertAlias,

        [System.String]
        $MachineName,

        [System.Boolean]
        $VersionGreaterThan1071
    )

    $URL = if($VersionGreaterThan1071){ $PortalURL.TrimEnd("/") + "/arcgis/portaladmin/machines/$MachineName/sslCertificates/update" }else{ $PortalURL.TrimEnd("/") + "/arcgis/portaladmin/security/sslCertificates/update" }

    $SSLCertsObject = Get-SSLCertificatesForPortal -PortalURL $PortalURL -Token $Token -Referer $Referer -MachineName $MachineName -VersionGreaterThan1071 $VersionGreaterThan1071

    $sslProtocols = if($null -eq $SSLCertsObject.cipherSuites) {"TLSv1.2,TLSv1.1,TLSv1"}else{$SSLCertsObject.sslProtocols}
    $cipherSuites = if($null -eq $SSLCertsObject.cipherSuites){ "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384,TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,TLS_DHE_RSA_WITH_AES_256_GCM_SHA384,TLS_DHE_RSA_WITH_AES_256_CBC_SHA256,TLS_DHE_RSA_WITH_AES_256_CBC_SHA,TLS_RSA_WITH_AES_256_GCM_SHA384,TLS_RSA_WITH_AES_256_CBC_SHA256,TLS_RSA_WITH_AES_256_CBC_SHA,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256,TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,TLS_DHE_RSA_WITH_AES_128_GCM_SHA256,TLS_DHE_RSA_WITH_AES_128_CBC_SHA256,TLS_DHE_RSA_WITH_AES_128_CBC_SHA,TLS_RSA_WITH_AES_128_GCM_SHA256,TLS_RSA_WITH_AES_128_CBC_SHA256,TLS_RSA_WITH_AES_128_CBC_SHA" }else{ $SSLCertsObject.cipherSuites }
    $WebParams = @{ f = 'json'; token = $Token; webServerCertificateAlias = $CertAlias; sslProtocols = $sslProtocols ; cipherSuites = $cipherSuites;}
    if($VersionGreaterThan1071){ $WebParams.HSTSEnabled = $False; }
   
    Invoke-ArcGISWebRequest -Url $URL -HttpFormParameters $WebParams -Referer $Referer
}

Export-ModuleMember -Function *-TargetResource