Functions/CertificateManagement/Update-FpsCertificate.ps1

<#
.SYNOPSIS
    This cmdlet installs a certificate, inherits permissions from the previous certificate, and reconfigures Business Central ServerInstances and IIS Websites where the last certificate is used.
.DESCRIPTION
    This cmdlet can be used either for updating Business Central ServerInstances and IIS Website from one certificate thumbprint to another only or to
     install a new certificate that will replace a previous certificate first and than update the BC ServerInstances and IIS Websites.
 
    Installation flow:
        1. Imports the new certificate
            (if $CertFilePath and $CertPassword are set)
        2. Inherit the permission from the previous certificate
            (if $CertFilePath and $CertPassword are set)
        3. Update thumbprint on Business Central Server Instances
            (Unless SkipUpdateBcServerInstances is set)
        4. Update thumbprint on IIS website https bindings
            (Unless SkipUpdateWebSiteBindings is set)
        5. Restarts the Business Central Instances and IIS Website
            (Only when AutoRestartServices is set)
        6. Removes the previous certificate
            (Only when RemovePreviousCert is set)
.EXAMPLE
    Update-FpsCertificate -CertFilePath 'c:\temp\mycert.pfx' -CertPassword ('myPassword' | ConvertTo-SecureString -AsPlainText -Force) -CertToReplaceThumbprint '008CEE1FEA5RANDOM2AF4F603EBPRINTBB0341D1'
.EXAMPLE
    $arguments = @{
        'CertFilePath' = 'c:\temp\mycert.pfx'
        'CertPassword' = 'myPassword' | ConvertTo-SecureString -AsPlainText -Force
        'CertToReplaceThumbprint' = 'A185C96BRANDOM8AEF1814CPFX7F94EDPRINTCE8'
        'CertStorePath' = 'cert:\LocalMachine\My'
        'RemovePreviousCert' = $false
        'AutoRestartServices' = $true
    }
    Update-FpsCertificate @arguments
.EXAMPLE
    Update-FpsCertificate -Thumbprint '008CEE1FEA5RANDOM2AF4F603EBPRINTBB0341D1' -CertToReplaceThumbprint 'A185C96BRANDOM8AEF1814CPFX7F94EDPRINTCE8' -RemovePreviousCert
#>

function Update-FpsCertificate{
    [CmdletBinding(DefaultParameterSetName='InstallNewCert')]  
    param(
        # The file path to the certificate (pfx) file to install.
        [Parameter(ParameterSetName='InstallNewCert', Mandatory=$true, ValueFromPipelineByPropertyName=$true)]
        [string] $CertFilePath,

        # The password required to install the certificate.
        [Parameter(ParameterSetName='InstallNewCert', Mandatory=$true, ValueFromPipelineByPropertyName=$true)]
        [SecureString] $CertPassword,
        
        # The certificate unique thumbprint of the new certificate.
        [Parameter(ParameterSetName='NewCertAlreadyInstalled', Mandatory=$true, ValueFromPipelineByPropertyName=$true)]
        [ValidatePattern('^[a-zA-Z\d]+$')]
        [string] $Thumbprint,

        # The certificate unique thumbprint of the old certificate.
        [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true)]
        [ValidatePattern('^[a-zA-Z\d]+$')]
        [string] $CertToReplaceThumbprint,

        # The certificate provider path where to scan for certificates. Path should start with 'Cert:'. E.g. 'cert:\LocalMachine\WebHosting'.
        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [string] $CertStorePath = 'cert:\LocalMachine\My',

        # When enabled removes the CertToReplaceThumbprint certificate after the new certificate in installed.
        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [switch] $RemovePreviousCert, 

        # Disables the automatic update of the Business Central ServerInstance configuration from the previous cert thumbprint to the new cert thumbprint.
        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [switch] $SkipUpdateBcServerInstances,

        # Disables the automatic update of the IIS Web Sites HTTPS Bindings from the previous cert to the new cert.
        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [switch] $SkipUpdateWebSiteBindings,

        # When enabled the updated Business Central ServerInstances and IIS websites will be restarted.
        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [switch] $AutoRestartServices
    )

    'Looking up previous certificate with thumbprint ''{0}'' in certificate store ''{1}''.' -f $CertToReplaceThumbprint, $CertStorePath | Write-Host
    $oldCert = Get-FpsCertificate -ThumbPrint $CertToReplaceThumbprint -CertStorePath $CertStorePath

    if($oldCert){
        ' Previous certificate ''{0}'' with thumbprint {1} is found.' -f $oldCert.FriendlyName, $oldCert.Thumbprint
    } elseif(-not $Thumbprint){
        Write-Warning ('Certificate with thumbprint {0} not found in Certificate Store {1}. Use either $Thumbprint parameter with Update-FpsCertificate or use Install-FpsCertificate if there is no certificate to replace.' -f $CertToReplaceThumbprint, $CertStorePath)
        return
    } else{
        Write-Warning ('Certificate with thumbprint {0} not found in Certificate Store {1}.' -f $CertToReplaceThumbprint, $CertStorePath)
        $RemovePreviousCert = $false
        $oldCert = @{
            'ThumbPrint' = $CertToReplaceThumbprint
        }
    }

    if(-not $Thumbprint){
        $param = @{
            'CertFilePath'  = $CertFilePath
            'CertPassword'  = $CertPassword
            'CertStorePath' = $CertStorePath
        }
        if($oldCert.UsersWithReadAccess){
            $param += @{'ServiceAccounts' = $oldCert.UsersWithReadAccess}
        }
        $newCert = Install-FpsCertificate @param
        
        $newCertThumbPrint = $newCert.ThumbPrint
    } else {
        $newCertThumbPrint = $Thumbprint
    }

    # Update thumbprint on Business Central Server Instances
    if(!$SkipUpdateBcServerInstances){
        
        $bcServerInstances = Get-BCServerInstance
        $bcServerInstancesToUpdate = $bcServerInstances | Where-Object {$_.AppSettings.ServicesCertificateThumbprint -eq $oldCert.Thumbprint}
        if($bcServerInstancesToUpdate){
            'Updating certificate thumbprint from ''{0}'' to ''{1}'' on Business Central Server Instances: {2}.' -f 
                $oldCert.Thumbprint, $newCertThumbPrint, ($bcServerInstancesToUpdate.ServerInstance -join ', ') | Write-Host
            if($AutoRestartServices){
                'AutoRestartServices is enabled. Updated Business Central instances will be restarted right away. This may take a while.' | Write-Host
            }
            Set-BcCertificateThumbPrint -ServerInstances $bcServerInstancesToUpdate.ServerInstance -ThumbPrint $newCertThumbPrint -AutoRestartServices:$AutoRestartServices
        } else {
            'No Business Central Server Instances found with previous thumbprint.' | Write-Host
        }
    }
    
    # Update thumbprint on IIS website https bindings
    if(!$SkipUpdateWebSiteBindings){
        $flag = $false
        $sites = Get-Website
        foreach($site in $sites){
            foreach ($binding in $site.bindings.Collection){
                if($binding.protocol -eq 'https' -and $binding.certificateHash -eq $oldCert.Thumbprint){
                    
                    'Updating binding {0} on website {1} from previous certificate with thumbprint {2} to new certificate with thumbprint {3}' -f 
                        $binding.bindingInformation, $site.Name, $oldCert.Thumbprint, $newCertThumbPrint | Write-Host
                    
                    # If the web binding uses the IIS Central Certificate Store the other https bindings of all web sites using the central store are update too.
                    # If Server Name Idication (SNI) is enabled on the binding only this specific binding is updated to the new certificate.
                    $binding.AddSslCertificate($newCertThumbPrint, (Split-Path $CertStorePath -Leaf))

                    if($AutoRestartServices){
                        $site.Stop()
                        $site.Start()
                    }

                    $flag = $true
                }
            }
        }

        if($flag -eq $false){
            'No IIS Web Site Bindings found with previous thumbprint.' | Write-Host
        }
    }

    if($RemovePreviousCert){
        Uninstall-FpsCertificate -ThumbPrint $oldCert.Thumbprint
    }

    Get-FpsCertificate -ThumbPrint $newCertThumbPrint
}

Export-ModuleMember -Function Update-FpsCertificate

function Set-BcCertificateThumbPrint{

    param(

        [Parameter(Mandatory=$true)] 
        [string[]] $ServerInstances,
        
        [Parameter(Mandatory=$true)]
        [string] $ThumbPrint,

        [switch] $AutoRestartServices
    )

    $scriptBlock = [scriptblock] {
        param(
            [string] $ServerInstance,
            [string] $ThumbPrint,
            [bool] $AutoRestartServices
        )

        Write-Host (' Loading Module for {0}' -f $ServerInstance)

        Import-Module fpsgeneral -DisableNameChecking -Force
        Import-Module fpsbcdeployment -Force
        Import-BcModule -ServerInstance $ServerInstance -ManagementModule
        $state = (Get-BCServerInstance $ServerInstance).State

        Write-Host( ' ThumbPrint: {0} ServerInstance: {1}' -f $ThumbPrint, $ServerInstance)
        Set-NAVServerConfiguration -ServerInstance $ServerInstance -KeyName 'ServicesCertificateThumbprint' -KeyValue $ThumbPrint
        
        if($AutoRestartServices -and $state -eq 'Running'){
            Write-Host (' Restarting ServerInstance: {0}' -f $ServerInstance)
            Restart-NAVServerInstance -ServerInstance $ServerInstance
        } elseif($AutoRestartServices -and $state -ne 'Running'){
            Write-Host (' Skipping restart ServerInstance: {0}. Current state {1} is not equal to running.' -f 
                            $ServerInstance, $state)
        }
    }

    $jobs = @()

    #loops through the array and starts a new job at each item
    Write-Host 'Starting PowerShell jobs to update the Business Central instances with the new certificate thumbprint...'

    foreach ($ServerInstance in $ServerInstances) {
        Write-Verbose ('Starting job number {0}' -f $ServerInstance)
        # Start the job and add it to the job list
        $jobs += Start-Job -ScriptBlock $scriptBlock -ArgumentList @( $ServerInstance, $ThumbPrint, $AutoRestartServices)
    }

    #wait for all the jobs to be completed
    Write-Verbose 'Waiting for all jobs to complete..'
    $complete = $false
    while (-not $complete) {
        
        # Get all the running jobs from the joblist
        $RunningJobs = $Jobs | Where-Object { $_.State -match 'running' }
        
        # If there are no more running jobs, go out the while loop
        if (-not $RunningJobs) { 
            Write-Verbose ('All jobs have been completed')
            $complete = $true 
        }
        Start-Sleep -Seconds 1
    }


    #get the result from the job for each job that was created
    foreach($job in $jobs){
        
        Write-Host ("`nReceiving output for job {0} with the state {1}" -f $job.Name, $job.State)
        Receive-Job -Job $job
       
    }
}