DSCResources/ArcGIS_Server_TLS/ArcGIS_Server_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'))

Import-Module -Name (Join-Path -Path $modulePath `
        -ChildPath (Join-Path -Path 'ArcGIS.Client' `
            -ChildPath 'ArcGIS.Client.Server.psm1'))
<#
    .SYNOPSIS
        Creates a SelfSigned Certificate or Installs a SSL Certificated Provided and Configures it with Server
    .PARAMETER ServerHostName
        Optional Host Name or IP of the Machine on which the Server has been installed and is to be configured.
    .PARAMETER ServerType
        Site Name or Default Context of Server
    .PARAMETER SiteAdministrator
        A MSFT_Credential Object - Primary Site Administrator.
    .PARAMETER WebServerCertificateAlias
        WebServerCertificateAlias with which the Certificate will be associated.
    .PARAMETER CertificateFileLocation
        Certificate Path from where to fetch the certificate to be installed.
    .PARAMETER CertificatePassword
        Sercret Certificate Password or Key.
    .PARAMETER SslRootOrIntermediate
        Takes a JSON string list of all the root or intermediate certificates to import
    .PARAMETER EnableHTTPSOnly
        Enable only HTTPs protocol
    .PARAMETER EnableHSTS
        Enable HTTP Strict Transport Security (HSTS)
#>


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

    @{}
}

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

        [System.String]
        $ServerType,

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

        [System.String]
        $CertificateFileLocation,

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

        [System.Boolean]
        $EnableHTTPSOnly,

        [System.Boolean]
        $EnableHSTS,

        [System.String]
        $Version,

        [System.Boolean]
        $ImportCertificateChain = $true,

        [System.Boolean]
        $ForceImportCertificate = $false
    )

    if($CertificateFileLocation -and -not(Test-Path $CertificateFileLocation)){
        throw "Certificate File '$CertificateFileLocation' is not found or inaccessible"
    }
    
    $FQDN = if($ServerHostName){ Get-FQDN $ServerHostName }else{ Get-FQDN $env:COMPUTERNAME }
    
    $ServerBaseUrl = Get-ArcGISComponentBaseUrl -ComponentName $ServerType -FQDN $FQDN
    $Referer = $ServerBaseUrl
    Write-Verbose "Waiting for Server '$($ServerBaseUrl)' to initialize"
    Test-ArcGISComponentHealth -BaseURL $ServerBaseUrl -ComponentName $ServerType
                      
    $token = Get-ServerToken -URL $ServerBaseUrl -Credential $SiteAdministrator -Referer $Referer
    if(-not($token.token)){
        throw "Unable to retrieve token for Site Administrator"
    }

    $MachineName = $FQDN
    $RestartRequired = $False
    if($null -ne $SslRootOrIntermediate){ #RootOrIntermediateCertificate
        $RestartRequired = Set-ServerRootAndIntermdiateCertificates -URL $ServerBaseUrl -ServerType $ServerType `
                                                -Token $token.token -Referer $Referer -MachineName $MachineName `
                                                -SslRootOrIntermediate $SslRootOrIntermediate -Verbose
    }

    # Get the current security configuration only for GIS Servers
    if(Test-IfGISServer -ServerType $ServerType){
        $UpdateSecurityConfig = $False
        Write-Verbose 'Getting security config for site'
        $secConfig = Get-SecurityConfig -URL $ServerBaseUrl -Token $token.token -Referer $Referer
        
        if($EnableHTTPSOnly){
            if($secConfig.sslEnabled -and -not($secConfig.httpEnabled)){
                Write-Verbose "Https Only is enabled. No update required"
            }else{
                Write-Verbose "Https Only is disabled. Update required"
                $UpdateSecurityConfig = $True
            }
        }else{
            if($EnableHSTS){
                throw "Error: Enable HSTS porperty requires http protocol set to only HTTPS."
            }

            if(-not($secConfig.sslEnabled -and -not($secConfig.httpEnabled))){
                Write-Verbose "Https Only is disabled. No update required"
            }else{
                Write-Verbose "Https Only is enabled. Update required"
                $UpdateSecurityConfig = $True
            }
        }

        if(-not($UpdateSecurityConfig)){
            if($secConfig.HSTSEnabled -ine $EnableHSTS){
                Write-Verbose "Enable HSTS doesn't match the expected state $EnableHSTS"
                $UpdateSecurityConfig = $True
            }else{
                Write-Verbose "Enable HSTS matches the expected state $EnableHSTS"
            }
        }

        if($UpdateSecurityConfig){
            Update-SecurityConfig -URL $ServerBaseUrl -Token $token.token -Referer $Referer `
                                    -Properties $secConfig -EnableHTTPSOnly $EnableHTTPSOnly `
                                    -EnableHSTS $EnableHSTS -MaxAttempts 1 -Verbose
            
            # Changes will cause the web server to restart.
            Write-Verbose "Waiting 30 seconds before checking"
            Start-Sleep -Seconds 30

            Write-Verbose "Waiting for Server '$($ServerBaseUrl)'"
            Test-ArcGISComponentHealth -BaseURL $ServerBaseUrl -ComponentName $ServerType -SleepTimeInSeconds 15 -MaxWaitTimeInSeconds 150
        }
    }

    
    Test-MachineExists -URL $ServerBaseUrl -Token $token.token -Referer $Referer -MachineName $MachineName

    if($CertificateFileLocation){
        if(-not(Test-Path $CertificateFileLocation)){
            throw "Certificate File '$CertificateFileLocation' is not found"
        }
        if($WebServerCertificateAlias -as [ipaddress]) {
            Write-Verbose "Adding Host mapping for $WebServerCertificateAlias"
            Add-HostMapping -hostname $WebServerCertificateAlias -ipaddress $WebServerCertificateAlias        
        }

        $DeleteTempCert = $False
        $ImportCert = $False
        $UpdateWebAlias = $False
        $CertForMachine = Get-SSLCertificateForMachine -URL $ServerBaseUrl -Token $token.token -Referer $Referer -MachineName $MachineName -SSLCertName $WebServerCertificateAlias.ToLower()
        if($null -ne $CertForMachine){ # Certificate with CName Found
            $NewCertIssuer = $null
            $NewCertThumbprint = $null
            if($CertificateFileLocation -and ($null -ne $CertificatePassword)) {
                $cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2
                $cert.Import($CertificateFileLocation,$CertificatePassword.GetNetworkCredential().Password,'DefaultKeySet')
                $NewCertIssuer = $cert.Issuer
                $NewCertThumbprint = $cert.Thumbprint
                Write-Verbose "Issuer for the supplied certificate is $NewCertIssuer"
                Write-Verbose "Thumbprint for the supplied certificate is $NewCertThumbprint"
            }

            $ExistingCertIssuer = $CertForMachine.Issuer    
            $ExistingCertThumbprint = $CertForMachine.Thumbprint
            Write-Verbose "Existing Cert Issuer $ExistingCertIssuer with Thumbprint $ExistingCertThumbprint"
            $machineDetails = Get-MachineProperties -URL $ServerBaseUrl -Token $token.token -Referer $Referer -MachineName $MachineName
            if($ExistingCertThumbprint -ine $NewCertThumbprint -or $ForceImportCertificate){ #Certificate Thumbprint doesn't match
                if($WebServerCertificateAlias -ieq $machineDetails.webServerCertificateAlias -or $ForceImportCertificate){
                    $DeleteTempCert = $True
                    #Upload Temp Cert
                    Write-Verbose "Force import certificate is: $ForceImportCertificate"
                    Write-Verbose "Importing Supplied Certificate with Alias $($WebServerCertificateAlias)-temp"
                    Import-ExistingCertificate -URL $ServerBaseUrl -Token $token.token -Referer $Referer `
                        -MachineName $MachineName -CertAlias "$($WebServerCertificateAlias)-temp" -CertificatePassword $CertificatePassword `
                        -CertificateFilePath $CertificateFileLocation -ServerType $ServerType -ImportCertificateChain $ImportCertificateChain -Version $Version

                    $RestartRequired = $False
                    #Update Web Alias to Temp Cert
                    Write-Verbose "Updating to temp SSL Certificate for machine [$MachineName]"
                    Update-MachineProperties -URL $ServerBaseUrl `
                            -Token          $token.token `
                            -Referer        $Referer `
                            -MachineName    $MachineName `
                            -WebServerCertificateAlias "$($WebServerCertificateAlias)-temp" -Verbose
                    
                    Start-Sleep -Seconds 30

                    Write-Verbose "Waiting for Server '$ServerBaseUrl'"
                    Test-ArcGISComponentHealth -BaseURL $ServerBaseUrl -ComponentName $ServerType -SleepTimeInSeconds 15 -MaxWaitTimeInSeconds 150
                }

                #Delete Certificate
                Write-Verbose "Certificate with alias $WebServerCertificateAlias already exists for machine $MachineName. Deleting it"
                try {
                    $res = Invoke-DeleteSSLCertForMachine -URL $ServerBaseUrl -Token $token.token -Referer $Referer -MachineName $MachineName -SSLCertName $WebServerCertificateAlias.ToLower()
                    Write-Verbose "Delete Certificate Operation result - $($res | ConvertTo-Json)"
                }
                catch {
                    Write-Verbose "[WARNING] Error deleting SSL Cert with alias $WebServerCertificateAlias. Error:- $_"
                }
                $ImportCert = $True #Upload New Cert
                $UpdateWebAlias = $True #Update Web Alias
            }else{ # Thumbprint matches
                if($WebServerCertificateAlias -ine $machineDetails.webServerCertificateAlias){
                    Write-Verbose "Certificate with alias $WebServerCertificateAlias already exists for machine $MachineName, but web server certificate alias $($machineDetails.webServerCertificateAlias) doesn't match."
                    $UpdateWebAlias = $True #Update Web Alias
                } else { #Everything Matches
                    Write-Verbose "Certificate with alias $WebServerCertificateAlias already exists for machine $MachineName and matches all the requirements."
                }
            }
        }else{ #Certificate with CName/Alias not found
            $ImportCert = $True #Upload New Cert
            $UpdateWebAlias = $True #Update Web Alias
        }

        if($ImportCert){
            Write-Verbose "Waiting for Server '$ServerBaseUrl'"
            Test-ArcGISComponentHealth -BaseURL $ServerBaseUrl -ComponentName $ServerType -SleepTimeInSeconds 15 -MaxWaitTimeInSeconds 150

            # Import the Supplied Certificate
            Write-Verbose "Importing Supplied Certificate with Alias $WebServerCertificateAlias"
            Import-ExistingCertificate -URL $ServerBaseUrl -Token $token.token -Referer $Referer `
                    -MachineName $MachineName -CertAlias $WebServerCertificateAlias -CertificatePassword $CertificatePassword `
                    -CertificateFilePath $CertificateFileLocation -ServerType $ServerType -ImportCertificateChain $ImportCertificateChain -Version $Version
        }

        if($UpdateWebAlias){
            $RestartRequired = $False
            Write-Verbose "Updating SSL Certificate for machine [$MachineName]"
            Update-MachineProperties -URL            $ServerBaseUrl `
                                    -Token          $token.token `
                                    -Referer        $Referer `
                                    -MachineName    $MachineName `
                                    -WebServerCertificateAlias $WebServerCertificateAlias -Verbose

            Start-Sleep -Seconds 30

            Write-Verbose "Waiting for Server '$ServerBaseUrl'"
            Test-ArcGISComponentHealth -BaseURL $ServerBaseUrl -ComponentName $ServerType -SleepTimeInSeconds 15 -MaxWaitTimeInSeconds 150

            # Restart Geoevent
            if(Test-IfGISServer -ServerType $ServerType){ #TODO - This will cause issues in Azure, where we have Geoevent and WFM running.
                ### If the SSL Certificate is changed. Restart the GeoEvent Service so that it will pick up the new certificate
                $GeoEventServiceName = 'ArcGISGeoEvent' 
                $GeoEventService = Get-Service -Name $GeoEventServiceName -ErrorAction Ignore
                if($GeoEventService -and $GeoEventService.Status -ieq 'Running') {
                    $GeoEventServerHttpsUrl = Get-ArcGISComponentBaseUrl -ComponentName "GeoEventServer" -Context "geoevent"
                    Restart-ArcGISService -ServiceName $GeoEventServiceName -Verbose
                    Write-Verbose "Waiting for Url '$($GeoEventServerHttpsUrl)/rest' to respond"
                    Test-ArcGISComponentHealth -BaseURL $GeoEventServerHttpsUrl -ComponentName "GeoEvent" -SleepTimeInSeconds 20 -MaxWaitTimeInSeconds 150 -Verbose
                    Write-Verbose "Restarted Service $GeoEventServiceName"
                }
            }
        }
        
        if($DeleteTempCert){ #Delete Temp Cert
            try {
                Write-Verbose "Deleting Temp Certificate with alias $($WebServerCertificateAlias)-temp"
                $res = Invoke-DeleteSSLCertForMachine -URL $ServerBaseUrl -Token $token.token -Referer $Referer -MachineName $MachineName -SSLCertName "$($WebServerCertificateAlias)-temp".ToLower()
                Write-Verbose "Delete Temp Certificate Operation result - $($res | ConvertTo-Json)"
            }
            catch {
                Write-Verbose "[WARNING] Error deleting Temp SSL Cert with alias $($WebServerCertificateAlias)-temp. Error:- $_"
            }
        }
    }else{
        Write-Verbose "CertificateFileLocation not specified. Skipping web server certificate configuration"
    }

    if($RestartRequired)
    {
        Write-Verbose "Restart required."
        Restart-ArcGISService -ComponentName $ServerType -Verbose
        Write-Verbose "Waiting 30 seconds before checking for initialization"
        Start-Sleep -Seconds 30

        Write-Verbose "Waiting for Server '$ServerBaseUrl'"
        Test-ArcGISComponentHealth -BaseURL $ServerBaseUrl -ComponentName $ServerType -SleepTimeInSeconds 10 -MaxWaitTimeInSeconds 60
    }
}

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

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

        [System.String]
        $WebServerCertificateAlias,

        [System.String]
        $CertificateFileLocation,

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

        [System.Boolean]
        $EnableHTTPSOnly,

        [System.Boolean]
        $EnableHSTS,

        [System.String]
        $Version,

        [System.Boolean]
        $ImportCertificateChain = $true,

        [System.Boolean]
        $ForceImportCertificate = $false
    )

    if($CertificateFileLocation -and -not(Test-Path $CertificateFileLocation)){
        throw "Certificate File '$CertificateFileLocation' is not found or inaccessible"
    }

    [System.Reflection.Assembly]::LoadWithPartialName("System.Web") | Out-Null
    $result = $True

    $FQDN = if($ServerHostName){ Get-FQDN $ServerHostName }else{ Get-FQDN $env:COMPUTERNAME }

    $ServerBaseUrl = Get-ArcGISComponentBaseUrl -ComponentName $ServerType -FQDN $FQDN
    $Referer = $ServerBaseUrl
    Write-Verbose "Waiting for Server '$($ServerBaseUrl)'"
    Test-ArcGISComponentHealth -BaseURL $ServerBaseUrl -ComponentName $ServerType

    $Referer = $ServerBaseUrl
    $token = Get-ServerToken -URL $ServerBaseUrl -Credential $SiteAdministrator -Referer $Referer
    if(-not($token.token)){
        throw "Unable to retrieve token for Site Administrator"
    }

    # Only for GIS Server
    if(Test-IfGISServer -ServerType $ServerType){
        $secConfig = Get-SecurityConfig -URL $ServerBaseUrl -Token $token.token -Referer $Referer
        if($result){
            if($EnableHTTPSOnly){
                if($secConfig.sslEnabled -and -not($secConfig.httpEnabled)){
                    Write-Verbose "Https Only is enabled. No update required"
                }else{
                    Write-Verbose "Https Only is disabled. Update required"
                    $result = $false
                }
            }else{
                if($EnableHSTS){
                    throw "Error: Enable HSTS porperty requires http protocol set to only HTTPS."
                }
        
                if(-not($secConfig.sslEnabled -and -not($secConfig.httpEnabled))){
                    Write-Verbose "Https Only is disabled. No update required"
                }else{
                    Write-Verbose "Https Only is enabled. Update required."
                    $result = $false
                }
            }
        
            if($result){
                if($secConfig.HSTSEnabled -ine $EnableHSTS){
                    Write-Verbose "Enable HSTS doesn't match the expected state $EnableHSTS"
                    $result = $false
                }else{
                    Write-Verbose "Enable HSTS matches the expected state $EnableHSTS"
                }
            }
        }
    }

    $MachineName = $FQDN
    Test-MachineExists -URL $ServerBaseUrl -Token $token.token -Referer $Referer -MachineName $MachineName

    if($result){
        if($CertificateFileLocation){
            if(-not(Test-Path $CertificateFileLocation)){
                throw "Certificate File '$CertificateFileLocation' is not found"
            }
            
            $CertForMachine = Get-SSLCertificateForMachine -URL $ServerBaseUrl -Token $token.token -Referer $Referer -MachineName $MachineName -SSLCertName $WebServerCertificateAlias.ToLower() -Verbose
            if($null -ne $CertForMachine){ # Certificate with Alias Found
                $NewCertIssuer = $null
                $NewCertThumbprint = $null
                if($CertificateFileLocation -and ($null -ne $CertificatePassword)) {
                    $cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2
                    $cert.Import($CertificateFileLocation,$CertificatePassword.GetNetworkCredential().Password,'DefaultKeySet')
                    $NewCertIssuer = $cert.Issuer
                    $NewCertThumbprint = $cert.Thumbprint
                    Write-Verbose "Issuer for the supplied certificate is $NewCertIssuer"
                    Write-Verbose "Thumbprint for the supplied certificate is $NewCertThumbprint"
                }
                $ExistingCertIssuer = $CertForMachine.Issuer    
                $ExistingCertThumbprint = $CertForMachine.Thumbprint
                Write-Verbose "Existing Cert Issuer $ExistingCertIssuer and Thumbprint $ExistingCertThumbprint"

                # Compare thumbprints and alias
                if($ExistingCertThumbprint -ine $NewCertThumbprint){ #Certificate Thumbprint doesn't match
                    Write-Verbose "Thumbprints for Certificate with Alias $WebServerCertificateAlias doesn't match that of existing cetificate."
                    $result = $False
                }else{ # Thumbprint matches
                    $machineDetails = Get-MachineProperties -URL $ServerBaseUrl -Token $token.token -Referer $Referer -MachineName $MachineName
                    if($WebServerCertificateAlias -ine $machineDetails.webServerCertificateAlias){
                        Write-Verbose "Certificate with alias $WebServerCertificateAlias already exists for machine $MachineName, but web server certificate alias $($machineDetails.webServerCertificateAlias) doesn't match."
                        $result = $False
                    } else { #Everything Matches
                        Write-Verbose "Certificate with alias $WebServerCertificateAlias already exists for machine $MachineName and matches all the requirements."
                    }
                }
            }else{ #Certificate with CName/Alias not found
                Write-Verbose "Certificate with Alias $WebServerCertificateAlias not found for machine $MachineName"
                $result = $False
            }
        }
    }
    
    if($result -and $null -ne $SslRootOrIntermediate){
        $MissingCerts = Get-ServerRootAndIntermdiateCertificatesToUpdate -URL $ServerBaseUrl -ServerType $ServerType `
                                                -Token $token.token -Referer $Referer -MachineName $MachineName `
                                                -SslRootOrIntermediate $SslRootOrIntermediate -Verbose
        $result = ($MissingCerts.Length -eq 0)
        if(-not($result)){
            Write-Verbose "One or more root and intermediate certificate needs an update."
        }
    }

    if ($Version -and ([version]$Version -ge [version]"11.3") `
        -and (Test-IfGISServer -ServerType $ServerType) `
        -and ($ForceImportCertificate)) {
        $result = $false
        Write-Verbose "Force import certificate is True"
    }

    Write-Verbose "Returning $result from Test-TargetResource"
    $result
}

Export-ModuleMember -Function *-TargetResource