DSCResources/MSFT_AdfsFarm/MSFT_AdfsFarm.psm1

<#
    .SYNOPSIS
        DSC module for the ADFS Farm resource
 
    .DESCRIPTION
        The AdfsFarm DSC resource installs an Active Directory Federation Services server farm, and the primary node of
        the farm. To further manage the configuration of ADFS, the ADFSProperties DSC resource should be used.
 
        Note: removal of the ADFS server farm using this resource is not supported. Remove the Adfs-Federation role
        from the server instead.
 
    .PARAMETER FederationServiceName
        Key - String
        Specifies the DNS name of the federation service. This value must match the subject name of the specified
        certificate.
 
    .PARAMETER CertificateThumbprint
        Required - String
        Specifies the thumbprint of the certificate to use for HTTPS bindings and service communication for ADFS. This
        value should match the thumbprint of a valid certificate in the Local Computer certificate store.
 
    .PARAMETER Credential
        Required - String
        Specifies a PSCredential object that must have domain administrator privileges.
 
    .PARAMETER FederationServiceDisplayName
        Write - String
        Specifies the display name of the Federation Service.
 
    .PARAMETER GroupServiceAccountIdentifier
        Write - String
        Specifies the Group Managed Service Account under which the Active Directory Federation Services (AD FS)
        service runs.
 
    .PARAMETER OverwriteConfiguration
        Write - Boolean
        This parameter must be used to remove an existing Active Directory Federation Services (AD FS) configuration
        database and overwrite it with a new database.
 
    .PARAMETER ServiceAccountCredential
        Write - String
        Specifies the Active Directory account under which the AD FS service runs in the form: <domain name>\\<user
        name>.
 
    .PARAMETER SQLConnectionString
        Write - String
        Specifies the SQL Server database that will store the AD FS configuration settings. If not specified, the AD FS
        installer uses the Windows Internal Database to store configuration settings.
 
    .PARAMETER AdminConfiguration
        Write - HashTable
        Specifies the Admin Configuration
 
    .PARAMETER Ensure
        Read - String
        The state of the ADFS Farm.
#>


Set-StrictMode -Version 2.0

$script:dscModuleName = 'AdfsDsc'
$script:psModuleName = 'ADFS'
$script:dscResourceName = [System.IO.Path]::GetFileNameWithoutExtension($MyInvocation.MyCommand.Name)

$script:resourceModulePath = Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent
$script:modulesFolderPath = Join-Path -Path $script:resourceModulePath -ChildPath 'Modules'

$script:localizationModulePath = Join-Path -Path $script:modulesFolderPath -ChildPath "$($script:DSCModuleName).Common"
Import-Module -Name (Join-Path -Path $script:localizationModulePath -ChildPath "$($script:dscModuleName).Common.psm1")

$script:localizedData = Get-LocalizedData -ResourceName $script:dscResourceName

$script:adfsServiceName = 'adfssrv'

function Get-TargetResource
{
    <#
    .SYNOPSIS
        Get-TargetResource
 
    .NOTES
        Used Cmdlets/Functions:
 
        Name | Module
        ----------------------------|----------------
        Get-AdfsSslCertificate | Adfs
        Get-AdfsProperties | Adfs
        Get-AdfsCertificate | Adfs
        Assert-Module | AdfsDsc.Common
        Assert-DomainMember | AdfsDsc.Common
        Get-AdfsConfigurationStatus | AdfsDsc.Common
        Assert-AdfsService | AdfsDsc.Common
        Assert-GroupServiceAccount | AdfsDsc.Common
        New-CimCredentialInstance | AdfsDsc.Common
    #>


    [CmdletBinding()]
    [OutputType([System.Collections.Hashtable])]
    param
    (
        [Parameter(Mandatory = $true)]
        [System.String]
        $FederationServiceName,

        [Parameter(Mandatory = $true)]
        [System.Management.Automation.PSCredential]
        $Credential
    )

    # Set Verbose and Debug parameters
    $commonParms = @{
        Verbose = $VerbosePreference
        Debug   = $DebugPreference
    }

    Write-Verbose -Message ($script:localizedData.GettingResourceMessage -f $FederationServiceName)

    # Check of the Resource PowerShell module is installed
    Assert-Module -ModuleName $script:psModuleName

    # Test if the computer is a domain member
    Assert-DomainMember

    # Check if the ADFS service has been configured
    if ((Get-AdfsConfigurationStatus) -eq 'Configured')
    {
        # Resource is Present
        Write-Debug -Message ($script:localizedData.TargetResourcePresentDebugMessage -f $FederationServiceName)

        # Assert if the ADFS service exists and is running
        Assert-AdfsService @commonParms

        try
        {
            $adfsProperties = Get-AdfsProperties
        }
        catch
        {
            $errorMessage = $script:localizedData.GettingAdfsPropertiesErrorMessage -f $FederationServiceName
            New-InvalidOperationException -Message $errorMessage -ErrorRecord $_
        }

        try
        {
            $adfsSslCertificate = Get-AdfsSslCertificate | Select-Object -First 1
        }
        catch
        {
            $errorMessage = $script:localizedData.GettingAdfsSslCertificateErrorMessage -f $FederationServiceName
            New-InvalidOperationException -Message $errorMessage -ErrorRecord $_
        }

        if ($adfsSslCertificate)
        {
            $certificateThumbprint = $adfsSslCertificate.CertificateHash
            $CertificateDnsName = $adfsSslCertificate.HostName
        }
        else
        {
            $errorMessage = $script:localizedData.GettingAdfsSslCertificateErrorMessage -f $FederationServiceName
            New-InvalidOperationException -Message $errorMessage
        }

        # Token signing certificate
        try
        {
            $adfsTokenSigningCertificate = Get-AdfsCertificate -CertificateType 'Token-Signing' | Where-Object { $_.IsPrimary -eq $true }
        }
        catch
        {
            $errorMessage = $script:localizedData.GettingAdfsTokenSigningCertificateErrorMessage -f $FederationServiceName
            New-InvalidOperationException -Message $errorMessage -ErrorRecord $_
        }

        if ($adfsTokenSigningCertificate -and $adfsTokenSigningCertificate.Certificate)
        {
            $SigningCertificateDnsName = $adfsTokenSigningCertificate.Certificate.Subject
        }
        else
        {
            $errorMessage = $script:localizedData.GettingAdfsTokenSigningCertificateErrorMessage -f $FederationServiceName
            New-InvalidOperationException -Message $errorMessage
        }

        # Token decrypting certificate
        try
        {
            $adfsTokenDecryptingCertificate = Get-AdfsCertificate -CertificateType 'Token-Decrypting' | Where-Object { $_.IsPrimary -eq $true }
        }
        catch
        {
            $errorMessage = $script:localizedData.GettingAdfsTokenDecryptingCertificateErrorMessage -f $FederationServiceName
            New-InvalidOperationException -Message $errorMessage -ErrorRecord $_
        }

        if ($adfsTokenDecryptingCertificate -and $adfsTokenDecryptingCertificate.Certificate)
        {
            $DecryptionCertificateDnsName = $adfsTokenDecryptingCertificate.Certificate.Subject
        }
        else
        {
            $errorMessage = $script:localizedData.GettingAdfsTokenDecryptingCertificateErrorMessage -f $FederationServiceName
            New-InvalidOperationException -Message $errorMessage
        }

        # Get ADFS service StartName (log on as) property
        $adfsService = Get-CimInstance -ClassName Win32_Service `
            -Filter "Name='$script:AdfsServiceName'" `
            -Verbose:$false

        if ($adfsService)
        {
            $ServiceAccountName = $adfsService.StartName
        }
        else
        {
            $errorMessage = $script:localizedData.GettingAdfsServiceErrorMessage -f $FederationServiceName
            New-InvalidOperationException -Message $errorMessage
        }

        # Test if service account is a group managed service account
        if (Assert-GroupServiceAccount -Name $ServiceAccountName)
        {
            $groupServiceAccountIdentifier = $adfsService.StartName
            $serviceAccountCredential = $null
        }
        else
        {
            $serviceAccountCredential = New-CimCredentialInstance -UserName $adfsService.StartName
            $groupServiceAccountIdentifier = $null
        }

        # Get ADFS SQL Connection String
        try
        {
            $adfsSecurityTokenService = Get-CimInstance -Namespace 'root/ADFS' `
                -ClassName 'SecurityTokenService' -Verbose:$false
        }
        catch
        {
            $errorMessage = $script:localizedData.GettingAdfsSecurityTokenServiceErrorMessage -f $FederationServiceName
            New-InvalidOperationException -Message $errorMessage -ErrorRecord $_
        }

        $sqlConnectionString = $adfsSecurityTokenService.ConfigurationDatabaseConnectionString

        $returnValue = @{
            FederationServiceName         = $adfsProperties.HostName
            CertificateThumbprint         = $certificateThumbprint
            CertificateDnsName            = $CertificateDnsName
            SigningCertificateDnsName     = $SigningCertificateDnsName
            DecryptionCertificateDnsName  = $DecryptionCertificateDnsName
            FederationServiceDisplayName  = $adfsProperties.DisplayName
            GroupServiceAccountIdentifier = $groupServiceAccountIdentifier
            ServiceAccountCredential      = $serviceAccountCredential
            SQLConnectionString           = $sqlConnectionString
            Ensure                        = 'Present'
        }
    }
    else
    {
        # Resource is Absent
        Write-Debug -Message ($script:localizedData.TargetResourceAbsentDebugMessage -f $FederationServiceName)

        $returnValue = @{
            FederationServiceName         = $FederationServiceName
            CertificateThumbprint         = $null
            CertificateDnsName            = $null
            SigningCertificateDnsName     = $null
            DecryptionCertificateDnsName  = $null
            FederationServiceDisplayName  = $null
            GroupServiceAccountIdentifier = $null
            ServiceAccountCredential      = $null
            SQLConnectionString           = $null
            Ensure                        = 'Absent'
        }
    }

    $returnValue
}

function Set-TargetResource
{
    <#
    .SYNOPSIS
        Set-TargetResource
 
    .NOTES
        Used Cmdlets/Functions:
 
        Name | Module
        -----------------|----------------
        Install-AdfsFarm | Adfs
 
        Install-AdfsFarm returns a [Microsoft.IdentityServer.Deployment.Core.Result] object with
        the following properties:
 
            Context - string
            Message - string
            Status - Microsoft.IdentityServer.Deployment.Core.ResultStatus
 
        Examples:
 
            Message : The configuration completed successfully.
            Context : DeploymentSucceeded
            Status : Success
 
            Message : The AD FS Windows Service could not be started. Cannot start service adfssrv
                      on computer '.'.
            Context : DeploymentTask
            Status : Error
    #>


    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidGlobalVars', '',
        Justification = 'Set LCM DSCMachineStatus to indicate reboot required')]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [System.String]
        $FederationServiceName,

        [Parameter(Mandatory = $true)]
        [System.Management.Automation.PSCredential]
        $Credential,

        [Parameter()]
        [System.String]
        $CertificateThumbprint,

        [Parameter()]
        [System.String]
        $CertificateDnsName,

        [Parameter()]
        [System.String]
        $FederationServiceDisplayName,

        [Parameter()]
        [System.String]
        $GroupServiceAccountIdentifier,

        [Parameter()]
        [System.Boolean]
        $OverwriteConfiguration,

        [Parameter()]
        [System.Management.Automation.PSCredential]
        $ServiceAccountCredential,

        [Parameter()]
        [System.String]
        $SQLConnectionString,

        [Parameter()]
        [Microsoft.Management.Infrastructure.CimInstance[]]
        $AdminConfiguration,

        [Parameter()]
        [System.String]
        $SigningCertificateDnsName,

        [Parameter()]
        [System.String]
        $DecryptionCertificateDnsName
    )

    Write-Verbose -Message ($script:localizedData.SettingResourceMessage -f $FederationServiceName)

    # Remove any parameters not used in Splats
    [HashTable]$parameters = $PSBoundParameters
    $parameters.Remove('Verbose')

    # Check whether both credential parameters have been specified
    if ($PSBoundParameters.ContainsKey('ServiceAccountCredential') -and
        $PSBoundParameters.ContainsKey('GroupServiceAccountIdentifier'))
    {
        $errorMessage = $script:localizedData.ResourceDuplicateCredentialErrorMessage -f $FederationServiceName
        New-InvalidArgumentException -Message $errorMessage -ArgumentName 'ServiceAccountCredential'
    }

    # Check whether no credential parameters have been specified
    if (-not $PSBoundParameters.ContainsKey('ServiceAccountCredential') -and
        -not $PSBoundParameters.ContainsKey('GroupServiceAccountIdentifier'))
    {
        $errorMessage = $script:localizedData.ResourceMissingCredentialErrorMessage -f $FederationServiceName
        New-InvalidArgumentException -Message $errorMessage -ArgumentName 'ServiceAccountCredential'
    }

    # Check whether both service certificate parameters have been specified
    if ($PSBoundParameters.ContainsKey('CertificateThumbprint') -and
        $PSBoundParameters.ContainsKey('CertificateDnsName'))
    {
        $errorMessage = $script:localizedData.ResourceDuplicateServiceCertificateErrorMessage
        New-InvalidArgumentException -Message $errorMessage -ArgumentName 'CertificateThumbprint'
    }

    # Check whether no service certificate parameters have been specified
    if (-not $PSBoundParameters.ContainsKey('CertificateThumbprint') -and
        -not $PSBoundParameters.ContainsKey('CertificateDnsName'))
    {
        $errorMessage = $script:localizedData.ResourceMissingServiceCertificateErrorMessage
        New-InvalidArgumentException -Message $errorMessage -ArgumentName 'CertificateThumbprint'
    }

    # Check that either both signing and decryption certificate DNS name parameters have been specified or neither
    if ($PSBoundParameters.ContainsKey('SigningCertificateDnsName') -xor
        $PSBoundParameters.ContainsKey('DecryptionCertificateDnsName'))
    {
        $errorMessage = $script:localizedData.ResourceInvalidSignDecryptCertificateErrorMessage
        New-InvalidArgumentException -Message $errorMessage -ArgumentName 'SigningCertificateDnsName'
    }

    $GetTargetResourceParms = @{
        FederationServiceName = $FederationServiceName
        Credential            = $Credential
    }
    $targetResource = Get-TargetResource @GetTargetResourceParms

    if ($targetResource.Ensure -eq 'Absent')
    {
        # Resource is Absent
        Write-Debug -Message ($script:localizedData.TargetResourceAbsentDebugMessage -f $FederationServiceName)

        Write-Verbose -Message ($script:localizedData.InstallingResourceMessage -f $FederationServiceName)

        if ($PSBoundParameters.ContainsKey('AdminConfiguration'))
        {
            # Convert AdminConfiguration Parameter from CimInstance#MSFT_KeyValuePair to HashTable
            $adminConfigurationHashTable = @{}

            foreach ($KeyPair in $AdminConfiguration)
            {
                $adminConfigurationHashTable += @{
                    $KeyPair.Key = $Keypair.Value
                }
            }

            $parameters.AdminConfiguration = $adminConfigurationHashTable
        }

        $localMachineCertPath = 'cert:\LocalMachine\My\'

        if ($PSBoundParameters.ContainsKey('CertificateDnsName'))
        {
            $serviceCert = Get-ChildItem -Path $localMachineCertPath -DnsName $CertificateDnsName |
                Sort-Object -Property NotAfter -Descending | Select-Object -First 1
            if ($null -eq $serviceCert)
            {
                $errorMessage = $script:localizedData.CertificateNotFoundErrorMessage -f $CertificateDnsName
                New-InvalidArgumentException -Message $errorMessage -ArgumentName 'CertificateDnsName'
            }
            $parameters.CertificateThumbprint = $serviceCert.Thumbprint
            $parameters.Remove('CertificateDnsName')
        }

        if ($PSBoundParameters.ContainsKey('SigningCertificateDnsName'))
        {
            $signingCert = Get-ChildItem -Path $localMachineCertPath -DnsName $SigningCertificateDnsName |
                Sort-Object -Property NotAfter -Descending | Select-Object -First 1
            if ($null -eq $signingCert)
            {
                $errorMessage = $script:localizedData.CertificateNotFoundErrorMessage -f $SigningCertificateDnsName
                New-InvalidArgumentException -Message $errorMessage -ArgumentName 'SigningCertificateDnsName'
            }
            $parameters.Add("SigningCertificateThumbprint", $signingCert.Thumbprint)
            $parameters.Remove('SigningCertificateDnsName')
        }

        if ($PSBoundParameters.ContainsKey('DecryptionCertificateDnsName'))
        {
            $decryptionCert = Get-ChildItem -Path $localMachineCertPath -DnsName $DecryptionCertificateDnsName |
                Sort-Object -Property NotAfter -Descending | Select-Object -First 1
            if ($null -eq $decryptionCert)
            {
                $errorMessage = $script:localizedData.CertificateNotFoundErrorMessage -f $DecryptionCertificateDnsName
                New-InvalidArgumentException -Message $errorMessage -ArgumentName 'DecryptionCertificateDnsName'
            }
            $parameters.Add("DecryptionCertificateThumbprint", $decryptionCert.Thumbprint)
            $parameters.Remove('DecryptionCertificateDnsName')
        }

        try
        {
            $Result = Install-AdfsFarm @parameters -ErrorAction SilentlyContinue
        }
        catch [System.IO.FileNotFoundException]
        {
            Write-Verbose -Message ($script:localizedData.MissingAdfsAssembliesMessage)

            # Set DSC Reboot required flag
            [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '',
                Justification = 'Set LCM DSCMachineStatus to indicate reboot required')]
            $global:DSCMachineStatus = 1
            return
        }
        catch
        {
            $errorMessage = $script:localizedData.InstallationErrorMessage -f $FederationServiceName
            New-InvalidOperationException -Message $errorMessage -ErrorRecord $_
        }

        # Check if a group managed service account is specified and the service won't start.
        if ($Result.Status -eq 'Error' -and $Result.Message -like '*Cannot start service adfssrv*' -and
            $PSBoundParameters.ContainsKey('GroupServiceAccountIdentifier'))
        {
            # Check the Kerberos Encryption types
        }

        if ($Result.Status -eq 'Success')
        {
            Write-Verbose -Message ($script:localizedData.ResourceInstallSuccessMessage -f $FederationServiceName)

            # Set DSC Reboot required flag
            [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseDeclaredVarsMoreThanAssignments", "",
                Justification = 'Set LCM DSCMachineStatus to indicate reboot required')]
            $global:DSCMachineStatus = 1
        }
        else
        {
            New-InvalidOperationException -Message $Result.Message
        }
    }
}

function Test-TargetResource
{
    <#
        .SYNOPSIS
            Test-TargetResource
    #>


    [CmdletBinding()]
    [OutputType([System.Boolean])]
    param
    (
        [Parameter(Mandatory = $true)]
        [System.String]
        $FederationServiceName,

        [Parameter(Mandatory = $true)]
        [System.Management.Automation.PSCredential]
        $Credential,

        [Parameter()]
        [System.String]
        $CertificateThumbprint,

        [Parameter()]
        [System.String]
        $FederationServiceDisplayName,

        [Parameter()]
        [System.String]
        $GroupServiceAccountIdentifier,

        [Parameter()]
        [System.Boolean]
        $OverwriteConfiguration,

        [Parameter()]
        [System.Management.Automation.PSCredential]
        $ServiceAccountCredential,

        [Parameter()]
        [System.String]
        $SQLConnectionString,

        [Parameter()]
        [Microsoft.Management.Infrastructure.CimInstance[]]
        $AdminConfiguration,

        [Parameter()]
        [System.String]
        $CertificateDnsName,

        [Parameter()]
        [System.String]
        $SigningCertificateDnsName,

        [Parameter()]
        [System.String]
        $DecryptionCertificateDnsName
    )

    Write-Verbose -Message ($script:localizedData.TestingResourceMessage -f $FederationServiceName)

    $getTargetResourceParms = @{
        FederationServiceName = $FederationServiceName
        Credential            = $Credential
    }
    $targetResource = Get-TargetResource @getTargetResourceParms

    if ($targetResource.Ensure -eq 'Present')
    {
        # Resource is Present
        Write-Debug -Message ($script:localizedData.TargetResourcePresentDebugMessage -f $FederationServiceName)

        Write-Verbose -Message ($script:localizedData.ResourceInDesiredStateMessage -f $FederationServiceName)

        $inDesiredState = $true
    }
    else
    {
        # Resource is Absent
        Write-Debug -Message ($script:localizedData.TargetResourceAbsentDebugMessage -f $FederationServiceName)

        Write-Verbose -Message ($script:localizedData.ResourceIsAbsentButShouldBePresentMessage -f
            $FederationServiceName)

        $inDesiredState = $false
    }

    $inDesiredState
}

Export-ModuleMember -Function *-TargetResource