From 1844836e29df25a70743aa6b246a782828518f79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20Both?= Date: Wed, 9 Oct 2019 21:07:58 +0200 Subject: [PATCH 01/10] Adding xRDCertificateConfiguration --- .../MSFT_xRDCertificateConfiguration.psm1 | 102 ++++++++++ ...SFT_xRDCertificateConfiguration.schema.mof | 22 +++ ...T_xRDCertificateConfiguration.strings.psd1 | 5 + ...MSFT_xRDCertificateConfiguration.tests.ps1 | 174 ++++++++++++++++++ .../MSFT_xRDServer/MSFT_xRDServer.schema.mof | 1 + .../xRemoteDesktopSessionHostCommon.psd1 | 1 + .../xRemoteDesktopSessionHostCommon.psm1 | 61 ++++++ 7 files changed, 366 insertions(+) create mode 100644 DSCResources/MSFT_xRDCertificateConfiguration/MSFT_xRDCertificateConfiguration.psm1 create mode 100644 DSCResources/MSFT_xRDCertificateConfiguration/MSFT_xRDCertificateConfiguration.schema.mof create mode 100644 DSCResources/MSFT_xRDCertificateConfiguration/en-US/MSFT_xRDCertificateConfiguration.strings.psd1 create mode 100644 Tests/Unit/MSFT_xRDCertificateConfiguration.tests.ps1 diff --git a/DSCResources/MSFT_xRDCertificateConfiguration/MSFT_xRDCertificateConfiguration.psm1 b/DSCResources/MSFT_xRDCertificateConfiguration/MSFT_xRDCertificateConfiguration.psm1 new file mode 100644 index 0000000..00f9fa4 --- /dev/null +++ b/DSCResources/MSFT_xRDCertificateConfiguration/MSFT_xRDCertificateConfiguration.psm1 @@ -0,0 +1,102 @@ +Import-Module -Name "$PSScriptRoot\..\..\xRemoteDesktopSessionHostCommon.psm1" +if (!(Test-xRemoteDesktopSessionHostOsRequirement)) { Throw "The minimum OS requirement was not met."} +Import-Module RemoteDesktop +$script:localizedData = Get-LocalizedData -ResourceName 'MSFT_xRDCertificateConfiguration' + +####################################################################### +# The Get-TargetResource cmdlet. +####################################################################### +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] $Role, + + [Parameter(Mandatory = $true)] + [System.String] $ConnectionBroker, + + [Parameter(Mandatory = $true)] + [System.String] $ImportPath, + + [Parameter(Mandatory = $true)] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Credential + ) + + Get-RDCertificate -Role $Role -ConnectionBroker $ConnectionBroker +} + + +######################################################################## +# The Set-TargetResource cmdlet. +######################################################################## +function Set-TargetResource + +{ + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidGlobalVars", "global:DSCMachineStatus")] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] $Role, + + [Parameter(Mandatory = $true)] + [System.String] $ConnectionBroker, + + [Parameter(Mandatory = $true)] + [System.String] $ImportPath, + + [Parameter(Mandatory = $true)] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Credential + ) + + try + { + Set-RDCertificate -Role $Role -ConnectionBroker $ConnectionBroker -ImportPath $ImportPath -Password $Credential.Password -Force + } + catch + { + Write-Error -Message ( + $script:localizedData.ErrorSettingCertificate -f $ImportPath, $Role, $ConnectionBroker, $_ + ) + } +} + + +####################################################################### +# The Test-TargetResource cmdlet. +####################################################################### +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] $Role, + + [Parameter(Mandatory = $true)] + [System.String] $ConnectionBroker, + + [Parameter(Mandatory = $true)] + [System.String] $ImportPath, + + [Parameter(Mandatory = $true)] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Credential + ) + + $pfxCertificate = (Get-PfxData -FilePath $ImportPath -Password ($Credential).Password).EndEntityCertificates + $currentCertificate = Get-TargetResource @PSBoundParameters + + $currentCertificate.Thumbprint -eq $pfxCertificate.Thumbprint +} + +Export-ModuleMember -Function *-TargetResource diff --git a/DSCResources/MSFT_xRDCertificateConfiguration/MSFT_xRDCertificateConfiguration.schema.mof b/DSCResources/MSFT_xRDCertificateConfiguration/MSFT_xRDCertificateConfiguration.schema.mof new file mode 100644 index 0000000..6ec979e --- /dev/null +++ b/DSCResources/MSFT_xRDCertificateConfiguration/MSFT_xRDCertificateConfiguration.schema.mof @@ -0,0 +1,22 @@ +[ClassVersion("1.0.0.0"), FriendlyName("xRDCertificateConfiguration")] +class MSFT_xRDCertificateConfiguration : OMI_BaseResource +{ + [key, + Description ("The role to apply this certificate configuration to."), + ValueMap {"RDRedirector", "RDPublishing", "RDWebAccess", "RDGateway"}, + Values {"RDRedirector", "RDPublishing", "RDWebAccess", "RDGateway"} + ] string Role; + + [key, + Description ("The connection broker that this certificate configuration is applied to.") + ] string ConnectionBroker; + + [write, + Description ("The certificate that should be used, should point to a PFX file on the filesystem.") + ] string ImportPath; + + [write, + Description("Specifies the password used to decrypt the PFX file. The username is ignored."), + EmbeddedInstance("MSFT_Credential") + ] string Credential; +}; diff --git a/DSCResources/MSFT_xRDCertificateConfiguration/en-US/MSFT_xRDCertificateConfiguration.strings.psd1 b/DSCResources/MSFT_xRDCertificateConfiguration/en-US/MSFT_xRDCertificateConfiguration.strings.psd1 new file mode 100644 index 0000000..6eee6dd --- /dev/null +++ b/DSCResources/MSFT_xRDCertificateConfiguration/en-US/MSFT_xRDCertificateConfiguration.strings.psd1 @@ -0,0 +1,5 @@ +# Localized resources for MSFT_xRDCertificateConfiguration + +ConvertFrom-StringData @' + ErrorSettingCertificate = Failed to apply certificate from path '{0}' to role '{1}' on connection broker '{2}'. Error: '{3}' +'@ diff --git a/Tests/Unit/MSFT_xRDCertificateConfiguration.tests.ps1 b/Tests/Unit/MSFT_xRDCertificateConfiguration.tests.ps1 new file mode 100644 index 0000000..71aaad2 --- /dev/null +++ b/Tests/Unit/MSFT_xRDCertificateConfiguration.tests.ps1 @@ -0,0 +1,174 @@ +$script:DSCModuleName = '.\xRemoteDesktopSessionHost' +$script:DSCResourceName = 'MSFT_xRDCertificateConfiguration' + +#region HEADER + +# Unit Test Template Version: 1.2.1 +$script:moduleRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot) +Write-Output @('clone','https://github.com/PowerShell/DscResource.Tests.git',"'"+(Join-Path -Path $script:moduleRoot -ChildPath '\DSCResource.Tests')+"'") + +if ( (-not (Test-Path -Path (Join-Path -Path $script:moduleRoot -ChildPath 'DSCResource.Tests'))) -or ` + (-not (Test-Path -Path (Join-Path -Path $script:moduleRoot -ChildPath 'DSCResource.Tests\TestHelper.psm1'))) ) +{ + & git @('clone','https://github.com/PowerShell/DscResource.Tests.git',(Join-Path -Path $script:moduleRoot -ChildPath 'DSCResource.Tests'),'--verbose') +} + +Import-Module -Name (Join-Path -Path $script:moduleRoot -ChildPath (Join-Path -Path 'DSCResource.Tests' -ChildPath 'TestHelper.psm1')) -Force + +$TestEnvironment = Initialize-TestEnvironment ` + -DSCModuleName $script:DSCModuleName ` + -DSCResourceName $script:DSCResourceName ` + -TestType Unit + +#endregion HEADER + +function Invoke-TestSetup { + +} + +function Invoke-TestCleanup { + Restore-TestEnvironment -TestEnvironment $TestEnvironment +} + +# Begin Testing + +try +{ + Invoke-TestSetup + + InModuleScope $script:DSCResourceName { + $script:DSCResourceName = 'MSFT_xRDCertificateConfiguration' + + Import-Module RemoteDesktop -Force + + #region Function Get-TargetResource + Describe "Testing $($script:DSCResourceName)" { + + Mock -CommandName Set-RDCertificate + + Context 'When a certificate is not configured' { + + Mock -CommandName Get-RDCertificate -MockWith { + [pscustomobject]@{ + Thumbprint = $null + Role = 'RDPublishing' + } + } -ParameterFilter {$Role -eq 'RDPublishing'} + + Mock -CommandName Get-PfxData -MockWith { + [pscustomobject]@{ + EndEntityCertificates = [pscustomobject]@{ + Thumbprint = '53086BBC44A3AB668A3B02CE0B258FEAEC1AFA8B' + } + } + } -ParameterFilter {$ImportPath -eq 'testdrive:\RDPublishing.pfx'} + + $resourceNotConfiguredSplat = @{ + Role = 'RDPublishing' + ConnectionBroker = 'connectionbroker.lan' + ImportPath = 'testdrive:\RDPublishing.pfx' + Credential = [pscredential]::new( + 'Test', + (ConvertTo-SecureString -AsPlainText -String 'pester' -Force) + ) + } + + It 'Given the certificate is not configured, no thumbprint is returned' { + (Get-TargetResource @resourceNotConfiguredSplat).Thumbprint | Should -BeNullOrEmpty + } + + It 'Given the certificate is not configured, Test-TargetResource returns false' { + Test-TargetResource @resourceNotConfiguredSplat | Should -BeFalse + } + + It 'Given the certificate is not configured, Set-TargetResource runs Set-RDCertificate' { + Set-TargetResource @resourceNotConfiguredSplat + Assert-MockCalled -CommandName Set-RDCertificate -Times 1 -Exactly + } + } + + Context 'When the proper certificate is configured' { + + Mock -CommandName Get-RDCertificate -MockWith { + [pscustomobject]@{ + Thumbprint = '53086BBC44A3AB668A3B02CE0B258FEAEC1AFA8A' + Role = 'RDRedirector' + } + } -ParameterFilter {$Role -eq 'RDRedirector'} + + Mock -CommandName Get-PfxData -MockWith { + [pscustomobject]@{ + EndEntityCertificates = [pscustomobject]@{ + Thumbprint = '53086BBC44A3AB668A3B02CE0B258FEAEC1AFA8A' + } + } + } -ParameterFilter {$ImportPath -eq 'testdrive:\RDRedirector.pfx'} + + $resourceConfiguredSplat = @{ + Role = 'RDRedirector' + ConnectionBroker = 'connectionbroker.lan' + ImportPath = 'testdrive:\RDRedirector.pfx' + Credential = [pscredential]::new( + 'Test', + (ConvertTo-SecureString -AsPlainText -String 'pester' -Force) + ) + } + + It 'Given the certificate is configured properly, the correct thumbprint is returned' { + (Get-TargetResource @resourceConfiguredSplat).Thumbprint | Should -Be '53086BBC44A3AB668A3B02CE0B258FEAEC1AFA8A' + } + + It 'Given the certificate is configured properly, Test-TargetResource returns true' { + Test-TargetResource @resourceConfiguredSplat | Should -BeTrue + } + } + + Context 'When a wrong certificate is configured' { + + Mock -CommandName Get-RDCertificate -MockWith { + [pscustomobject]@{ + Thumbprint = '53086BBC44A3AB668A3B02CE0B258FEAEC1AFA8C' + Role = 'RDGateway' + } + } -ParameterFilter {$Role -eq 'RDGateway'} + + Mock -CommandName Get-PfxData -MockWith { + [pscustomobject]@{ + EndEntityCertificates = [pscustomobject]@{ + Thumbprint = '53086BBC44A3AB668A3B02CE0B258FEAEC1AFA8B' + } + } + } -ParameterFilter {$ImportPath -eq 'testdrive:\RDGateway.pfx'} + + $resourceWrongConfiguredSplat = @{ + Role = 'RDGateway' + ConnectionBroker = 'connectionbroker.lan' + ImportPath = 'testdrive:\RDGateway.pfx' + Credential = [pscredential]::new( + 'Test', + (ConvertTo-SecureString -AsPlainText -String 'pester' -Force) + ) + } + + It 'Given the wrong certificate is configured, the thumbprint of the currently configured certificate is returned' { + (Get-TargetResource @resourceWrongConfiguredSplat).Thumbprint | Should -Be '53086BBC44A3AB668A3B02CE0B258FEAEC1AFA8C' + } + + It 'Given the wrong certificate is configured, Test-TargetResource returns false' { + Test-TargetResource @resourceWrongConfiguredSplat | Should -BeFalse + } + + It 'Given the wrong certificate is configured, Set-TargetResource runs Set-RDCertificate' { + Set-TargetResource @resourceWrongConfiguredSplat + Assert-MockCalled -CommandName Set-RDCertificate -Times 1 -Exactly + } + } + } + } +} +finally +{ + #region FOOTER + Invoke-TestCleanup + #endregion +} diff --git a/source/DSCResources/MSFT_xRDServer/MSFT_xRDServer.schema.mof b/source/DSCResources/MSFT_xRDServer/MSFT_xRDServer.schema.mof index 65c04e5..0f3ca82 100644 --- a/source/DSCResources/MSFT_xRDServer/MSFT_xRDServer.schema.mof +++ b/source/DSCResources/MSFT_xRDServer/MSFT_xRDServer.schema.mof @@ -10,3 +10,4 @@ class MSFT_xRDServer : OMI_BaseResource [write] string GatewayExternalFqdn; }; + diff --git a/source/Modules/xRemoteDesktopSessionHostCommon.psd1 b/source/Modules/xRemoteDesktopSessionHostCommon.psd1 index 608f5e5..841f390 100644 --- a/source/Modules/xRemoteDesktopSessionHostCommon.psd1 +++ b/source/Modules/xRemoteDesktopSessionHostCommon.psd1 @@ -70,6 +70,7 @@ Description = 'Functions used by the DSC resources in xRemoteDesktopSessionHost. # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. FunctionsToExport = @( + 'Get-LocalizedData' 'Get-xRemoteDesktopSessionHostOsVersion' 'Test-xRemoteDesktopSessionHostOsRequirement' ) diff --git a/source/Modules/xRemoteDesktopSessionHostCommon.psm1 b/source/Modules/xRemoteDesktopSessionHostCommon.psm1 index 5b56315..71b71b9 100644 --- a/source/Modules/xRemoteDesktopSessionHostCommon.psm1 +++ b/source/Modules/xRemoteDesktopSessionHostCommon.psm1 @@ -7,3 +7,64 @@ function Get-xRemoteDesktopSessionHostOsVersion { return [Environment]::OSVersion.Version } + +<# + .SYNOPSIS + Retrieves the localized string data based on the machine's culture. + Falls back to en-US strings if the machine's culture is not supported. + .PARAMETER ResourceName + The name of the resource as it appears before '.strings.psd1' of the localized string file. + For example: + For WindowsOptionalFeature: MSFT_WindowsOptionalFeature + For Service: MSFT_ServiceResource + For Registry: MSFT_RegistryResource + For Helper: SqlServerDscHelper + .PARAMETER ScriptRoot + Optional. The root path where to expect to find the culture folder. This is only needed + for localization in helper modules. This should not normally be used for resources. + .NOTES + To be able to use localization in the helper function, this function must + be first in the file, before Get-LocalizedData is used by itself to load + localized data for this helper module (see directly after this function). +#> +function Get-LocalizedData +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $ResourceName, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $ScriptRoot + ) + + if (-not $ScriptRoot) + { + $dscResourcesFolder = Join-Path -Path $PSScriptRoot -ChildPath 'DSCResources' + $resourceDirectory = Join-Path -Path $dscResourcesFolder -ChildPath $ResourceName + } + else + { + $resourceDirectory = $ScriptRoot + } + + $localizedStringFileLocation = Join-Path -Path $resourceDirectory -ChildPath $PSUICulture + + if (-not (Test-Path -Path $localizedStringFileLocation)) + { + # Fallback to en-US + $localizedStringFileLocation = Join-Path -Path $resourceDirectory -ChildPath 'en-US' + } + + Import-LocalizedData ` + -BindingVariable 'localizedData' ` + -FileName "$ResourceName.strings.psd1" ` + -BaseDirectory $localizedStringFileLocation + + return $localizedData +} From 738109889a10b9f71fee1411074ce10eaa535b6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20Both?= Date: Wed, 9 Oct 2019 22:35:17 +0200 Subject: [PATCH 02/10] Credential not mandatory and add test for error condition --- .../MSFT_xRDCertificateConfiguration.psm1 | 62 ++++++++++--- ...SFT_xRDCertificateConfiguration.schema.mof | 2 +- ...MSFT_xRDCertificateConfiguration.tests.ps1 | 91 +++++++++++++++++-- 3 files changed, 129 insertions(+), 26 deletions(-) diff --git a/DSCResources/MSFT_xRDCertificateConfiguration/MSFT_xRDCertificateConfiguration.psm1 b/DSCResources/MSFT_xRDCertificateConfiguration/MSFT_xRDCertificateConfiguration.psm1 index 00f9fa4..555b4ce 100644 --- a/DSCResources/MSFT_xRDCertificateConfiguration/MSFT_xRDCertificateConfiguration.psm1 +++ b/DSCResources/MSFT_xRDCertificateConfiguration/MSFT_xRDCertificateConfiguration.psm1 @@ -13,15 +13,19 @@ function Get-TargetResource param ( [Parameter(Mandatory = $true)] - [System.String] $Role, + [ValidateSet('RDRedirector', 'RDPublishing', 'RDWebAccess', 'RDGateway')] + [System.String] + $Role, [Parameter(Mandatory = $true)] - [System.String] $ConnectionBroker, + [System.String] + $ConnectionBroker, [Parameter(Mandatory = $true)] - [System.String] $ImportPath, + [System.String] + $ImportPath, - [Parameter(Mandatory = $true)] + [Parameter()] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $Credential @@ -37,28 +41,43 @@ function Get-TargetResource function Set-TargetResource { - [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidGlobalVars", "global:DSCMachineStatus")] [CmdletBinding()] param ( [Parameter(Mandatory = $true)] - [System.String] $Role, + [ValidateSet('RDRedirector', 'RDPublishing', 'RDWebAccess', 'RDGateway')] + [System.String] + $Role, [Parameter(Mandatory = $true)] - [System.String] $ConnectionBroker, + [System.String] + $ConnectionBroker, [Parameter(Mandatory = $true)] - [System.String] $ImportPath, + [System.String] + $ImportPath, - [Parameter(Mandatory = $true)] + [Parameter()] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $Credential ) + $rdCertificateSplat = @{ + Role = $Role + ConnectionBroker = $ConnectionBroker + ImportPath = $ImportPath + Force = $true + } + + if ($Credential -ne [pscredential]::Empty) + { + $rdCertificateSplat.Add('Password', $Credential.Password) + } + try { - Set-RDCertificate -Role $Role -ConnectionBroker $ConnectionBroker -ImportPath $ImportPath -Password $Credential.Password -Force + Set-RDCertificate @rdCertificateSplat } catch { @@ -79,21 +98,34 @@ function Test-TargetResource param ( [Parameter(Mandatory = $true)] - [System.String] $Role, + [ValidateSet('RDRedirector', 'RDPublishing', 'RDWebAccess', 'RDGateway')] + [System.String] + $Role, [Parameter(Mandatory = $true)] - [System.String] $ConnectionBroker, + [System.String] + $ConnectionBroker, [Parameter(Mandatory = $true)] - [System.String] $ImportPath, + [System.String] + $ImportPath, - [Parameter(Mandatory = $true)] + [Parameter()] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $Credential ) - $pfxCertificate = (Get-PfxData -FilePath $ImportPath -Password ($Credential).Password).EndEntityCertificates + $getPfxDataSplat = @{ + FilePath = $ImportPath + } + + if ($Credential -ne [pscredential]::Empty) + { + $getPfxDataSplat.Add('Password', $Credential.Password) + } + + $pfxCertificate = (Get-PfxData @getPfxDataSplat).EndEntityCertificates $currentCertificate = Get-TargetResource @PSBoundParameters $currentCertificate.Thumbprint -eq $pfxCertificate.Thumbprint diff --git a/DSCResources/MSFT_xRDCertificateConfiguration/MSFT_xRDCertificateConfiguration.schema.mof b/DSCResources/MSFT_xRDCertificateConfiguration/MSFT_xRDCertificateConfiguration.schema.mof index 6ec979e..153a0c5 100644 --- a/DSCResources/MSFT_xRDCertificateConfiguration/MSFT_xRDCertificateConfiguration.schema.mof +++ b/DSCResources/MSFT_xRDCertificateConfiguration/MSFT_xRDCertificateConfiguration.schema.mof @@ -11,7 +11,7 @@ class MSFT_xRDCertificateConfiguration : OMI_BaseResource Description ("The connection broker that this certificate configuration is applied to.") ] string ConnectionBroker; - [write, + [write, required, Description ("The certificate that should be used, should point to a PFX file on the filesystem.") ] string ImportPath; diff --git a/Tests/Unit/MSFT_xRDCertificateConfiguration.tests.ps1 b/Tests/Unit/MSFT_xRDCertificateConfiguration.tests.ps1 index 71aaad2..7cfa4bd 100644 --- a/Tests/Unit/MSFT_xRDCertificateConfiguration.tests.ps1 +++ b/Tests/Unit/MSFT_xRDCertificateConfiguration.tests.ps1 @@ -73,17 +73,23 @@ try ) } - It 'Given the certificate is not configured, no thumbprint is returned' { + It 'Get-TargetResource returns no thumbprint' { (Get-TargetResource @resourceNotConfiguredSplat).Thumbprint | Should -BeNullOrEmpty } - It 'Given the certificate is not configured, Test-TargetResource returns false' { + It 'Test-TargetResource returns false' { Test-TargetResource @resourceNotConfiguredSplat | Should -BeFalse } - It 'Given the certificate is not configured, Set-TargetResource runs Set-RDCertificate' { + It 'Set-TargetResource runs Set-RDCertificate' { Set-TargetResource @resourceNotConfiguredSplat - Assert-MockCalled -CommandName Set-RDCertificate -Times 1 -Exactly + Assert-MockCalled -CommandName Set-RDCertificate -Times 1 -Exactly -ParameterFilter { + $Role -eq $resourceNotConfiguredSplat.Role -and + $ConnectionBroker -eq $resourceNotConfiguredSplat.ConnectionBroker -and + $ImportPath -eq $resourceNotConfiguredSplat.ImportPath -and + $Password -eq $resourceNotConfiguredSplat.Credential.Password -and + $Force -eq $true + } } } @@ -114,11 +120,11 @@ try ) } - It 'Given the certificate is configured properly, the correct thumbprint is returned' { + It 'Get-TargetResource returns the correct thumbprint' { (Get-TargetResource @resourceConfiguredSplat).Thumbprint | Should -Be '53086BBC44A3AB668A3B02CE0B258FEAEC1AFA8A' } - It 'Given the certificate is configured properly, Test-TargetResource returns true' { + It 'Test-TargetResource returns true' { Test-TargetResource @resourceConfiguredSplat | Should -BeTrue } } @@ -150,17 +156,82 @@ try ) } - It 'Given the wrong certificate is configured, the thumbprint of the currently configured certificate is returned' { + It 'Get-TargetResource returns the thumbprint of the currently configured certificate' { + (Get-TargetResource @resourceWrongConfiguredSplat).Thumbprint | Should -Be '53086BBC44A3AB668A3B02CE0B258FEAEC1AFA8C' + } + + It 'Test-TargetResource returns false' { + Test-TargetResource @resourceWrongConfiguredSplat | Should -BeFalse + } + + It 'Set-TargetResource runs Set-RDCertificate' { + Set-TargetResource @resourceWrongConfiguredSplat + Assert-MockCalled -CommandName Set-RDCertificate -Times 1 -Exactly -ParameterFilter { + $Role -eq $resourceWrongConfiguredSplat.Role -and + $ConnectionBroker -eq $resourceWrongConfiguredSplat.ConnectionBroker -and + $ImportPath -eq $resourceWrongConfiguredSplat.ImportPath -and + $Password -eq $resourceWrongConfiguredSplat.Credential.Password -and + $Force -eq $true + } + } + } + + Context 'When a wrong certificate is configured and the PFX file is protected based on group membership (ProtectTo)' { + Mock -CommandName Get-RDCertificate -MockWith { + [pscustomobject]@{ + Thumbprint = '53086BBC44A3AB668A3B02CE0B258FEAEC1AFA8C' + Role = 'RDGateway' + } + } -ParameterFilter {$Role -eq 'RDGateway'} + + Mock -CommandName Get-PfxData -MockWith { + [pscustomobject]@{ + EndEntityCertificates = [pscustomobject]@{ + Thumbprint = '53086BBC44A3AB668A3B02CE0B258FEAEC1AFA8B' + } + } + } -ParameterFilter {$ImportPath -eq 'testdrive:\RDGateway.pfx'} + + $resourceWrongConfiguredSplat = @{ + Role = 'RDGateway' + ConnectionBroker = 'connectionbroker.lan' + ImportPath = 'testdrive:\RDGateway.pfx' + } + + It 'Get-TargetResource returns the thumbprint of the currently configured certificate' { (Get-TargetResource @resourceWrongConfiguredSplat).Thumbprint | Should -Be '53086BBC44A3AB668A3B02CE0B258FEAEC1AFA8C' } - It 'Given the wrong certificate is configured, Test-TargetResource returns false' { + It 'Test-TargetResource returns false' { Test-TargetResource @resourceWrongConfiguredSplat | Should -BeFalse } - It 'Given the wrong certificate is configured, Set-TargetResource runs Set-RDCertificate' { + It 'Set-TargetResource runs Set-RDCertificate without password' { Set-TargetResource @resourceWrongConfiguredSplat - Assert-MockCalled -CommandName Set-RDCertificate -Times 1 -Exactly + Assert-MockCalled -CommandName Set-RDCertificate -Times 1 -Exactly -ParameterFilter { + $Role -eq $resourceWrongConfiguredSplat.Role -and + $ConnectionBroker -eq $resourceWrongConfiguredSplat.ConnectionBroker -and + $ImportPath -eq $resourceWrongConfiguredSplat.ImportPath -and + $Force -eq $true + } + } + } + + Context 'When a certificate fails to set' { + + Mock Set-RDCertificate -MockWith { + throw 'Failed to apply certificate' + } + + $resourceWrongConfiguredSplat = @{ + Role = 'RDGateway' + ConnectionBroker = 'connectionbroker.lan' + ImportPath = 'testdrive:\RDGateway.pfx' + } + + It 'Set-TargetResource returns an error when the certificate could not be applied' { + $errorMessage = Set-TargetResource @resourceWrongConfiguredSplat 2>&1 + $errorMessage | Should -Not -BeNullOrEmpty } } } From 967662a019a1125e7239992b745fb460b0db8d8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20Both?= Date: Thu, 10 Oct 2019 08:04:01 +0200 Subject: [PATCH 03/10] Adding verbose message and error handling for Get-PfxData --- .../MSFT_xRDCertificateConfiguration.psm1 | 31 +++++++++++++++++-- ...T_xRDCertificateConfiguration.strings.psd1 | 5 +++ ...MSFT_xRDCertificateConfiguration.tests.ps1 | 22 +++++++++++++ 3 files changed, 55 insertions(+), 3 deletions(-) diff --git a/DSCResources/MSFT_xRDCertificateConfiguration/MSFT_xRDCertificateConfiguration.psm1 b/DSCResources/MSFT_xRDCertificateConfiguration/MSFT_xRDCertificateConfiguration.psm1 index 555b4ce..55d6fbb 100644 --- a/DSCResources/MSFT_xRDCertificateConfiguration/MSFT_xRDCertificateConfiguration.psm1 +++ b/DSCResources/MSFT_xRDCertificateConfiguration/MSFT_xRDCertificateConfiguration.psm1 @@ -1,5 +1,5 @@ Import-Module -Name "$PSScriptRoot\..\..\xRemoteDesktopSessionHostCommon.psm1" -if (!(Test-xRemoteDesktopSessionHostOsRequirement)) { Throw "The minimum OS requirement was not met."} +if (!(Test-xRemoteDesktopSessionHostOsRequirement)) { throw "The minimum OS requirement was not met."} Import-Module RemoteDesktop $script:localizedData = Get-LocalizedData -ResourceName 'MSFT_xRDCertificateConfiguration' @@ -31,6 +31,9 @@ function Get-TargetResource $Credential ) + Write-Verbose -Message ( + $script:localizedData.GetCertificate -f $Role, $ConnectionBroker + ) Get-RDCertificate -Role $Role -ConnectionBroker $ConnectionBroker } @@ -77,6 +80,9 @@ function Set-TargetResource try { + Write-Verbose -Message ( + $script:localizedData.SetCertificate -f $Role, $ImportPath + ) Set-RDCertificate @rdCertificateSplat } catch @@ -118,6 +124,7 @@ function Test-TargetResource $getPfxDataSplat = @{ FilePath = $ImportPath + ErrorAction = 'Stop' } if ($Credential -ne [pscredential]::Empty) @@ -125,10 +132,28 @@ function Test-TargetResource $getPfxDataSplat.Add('Password', $Credential.Password) } - $pfxCertificate = (Get-PfxData @getPfxDataSplat).EndEntityCertificates $currentCertificate = Get-TargetResource @PSBoundParameters + Write-Verbose -Message ( + $script:localizedData.VerboseCurrentCertificate -f $Role, $currentCertificate.Thumbprint + ) + + try + { + $pfxCertificate = (Get-PfxData @getPfxDataSplat).EndEntityCertificates + Write-Verbose -Message ( + $script:localizedData.VerbosePfxCertificate -f $Role, $pfxCertificate.Thumbprint + ) - $currentCertificate.Thumbprint -eq $pfxCertificate.Thumbprint + return ($currentCertificate.Thumbprint -eq $pfxCertificate.Thumbprint) + } + catch + { + Write-Warning -Message ( + $script:localizedData.WarningPfxDataImportFailed -f $ImportPath, $_ + ) + + return $false + } } Export-ModuleMember -Function *-TargetResource diff --git a/DSCResources/MSFT_xRDCertificateConfiguration/en-US/MSFT_xRDCertificateConfiguration.strings.psd1 b/DSCResources/MSFT_xRDCertificateConfiguration/en-US/MSFT_xRDCertificateConfiguration.strings.psd1 index 6eee6dd..0d97318 100644 --- a/DSCResources/MSFT_xRDCertificateConfiguration/en-US/MSFT_xRDCertificateConfiguration.strings.psd1 +++ b/DSCResources/MSFT_xRDCertificateConfiguration/en-US/MSFT_xRDCertificateConfiguration.strings.psd1 @@ -2,4 +2,9 @@ ConvertFrom-StringData @' ErrorSettingCertificate = Failed to apply certificate from path '{0}' to role '{1}' on connection broker '{2}'. Error: '{3}' + VerboseCurrentCertificate = Thumbprint of currently configured certificate for role '{0}': '{1}' + VerboseGetCertificate = Get current certificate for role '{0}' from connection broker '{1}' + VerbosePfxCertificate = Thumbprint of certificate for role '{0}' in .pfx file: '{1}' + VerboseSetCertificate = Importing certificate for role '{0}' from file '{1}' + WarningPfxDataImportFailed = Failed to import certificate from pfx file '{0}'. Error message: '{1}' '@ diff --git a/Tests/Unit/MSFT_xRDCertificateConfiguration.tests.ps1 b/Tests/Unit/MSFT_xRDCertificateConfiguration.tests.ps1 index 7cfa4bd..5a2159d 100644 --- a/Tests/Unit/MSFT_xRDCertificateConfiguration.tests.ps1 +++ b/Tests/Unit/MSFT_xRDCertificateConfiguration.tests.ps1 @@ -217,6 +217,28 @@ try } } + Context 'When a certificate fails to test' { + Mock Get-RDCertificate + Mock Get-PfxData -MockWith { + throw "Cannot import PFX file" + } + + $resourceWrongConfiguredSplat = @{ + Role = 'RDGateway' + ConnectionBroker = 'connectionbroker.lan' + ImportPath = 'testdrive:\RDGateway.pfx' + } + + It 'Test-TargetResource displays a warning when a certificate fails to test' { + $message = Test-TargetResource @resourceWrongConfiguredSplat 3>&1 + $message | Should -Not -BeNullOrEmpty + } + + It 'Test-TargetResource returns false' { + Test-TargetResource @resourceWrongConfiguredSplat | Should -BeFalse + } + } + Context 'When a certificate fails to set' { Mock Set-RDCertificate -MockWith { From 5502cf4aed96475ab3a91bd1eb5edf01a635df5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20Both?= Date: Thu, 10 Oct 2019 08:09:34 +0200 Subject: [PATCH 04/10] Some minor formatting updates and adding explicit ErrorAction --- .../MSFT_xRDCertificateConfiguration.psm1 | 3 +++ 1 file changed, 3 insertions(+) diff --git a/DSCResources/MSFT_xRDCertificateConfiguration/MSFT_xRDCertificateConfiguration.psm1 b/DSCResources/MSFT_xRDCertificateConfiguration/MSFT_xRDCertificateConfiguration.psm1 index 55d6fbb..c01a5e3 100644 --- a/DSCResources/MSFT_xRDCertificateConfiguration/MSFT_xRDCertificateConfiguration.psm1 +++ b/DSCResources/MSFT_xRDCertificateConfiguration/MSFT_xRDCertificateConfiguration.psm1 @@ -34,6 +34,7 @@ function Get-TargetResource Write-Verbose -Message ( $script:localizedData.GetCertificate -f $Role, $ConnectionBroker ) + Get-RDCertificate -Role $Role -ConnectionBroker $ConnectionBroker } @@ -71,6 +72,7 @@ function Set-TargetResource ConnectionBroker = $ConnectionBroker ImportPath = $ImportPath Force = $true + ErrorAction = 'Stop' } if ($Credential -ne [pscredential]::Empty) @@ -83,6 +85,7 @@ function Set-TargetResource Write-Verbose -Message ( $script:localizedData.SetCertificate -f $Role, $ImportPath ) + Set-RDCertificate @rdCertificateSplat } catch From 4e0abe327e4e30ce1b3987c1d442077171fa9d76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20Both?= Date: Thu, 10 Oct 2019 18:19:28 +0200 Subject: [PATCH 05/10] Update README changelog, documentation and add example --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 52d556b..bea19e5 100644 --- a/README.md +++ b/README.md @@ -113,3 +113,10 @@ Please check out common DSC Community [contributing guidelines](https://dsccommu * **ConnectionBroker**: Specifies the Remote Desktop Connection Broker (RD Connection Broker) server for a Remote Desktop deployment. * **LicenseServers**: An array of servers to use for RD licensing * **LicenseMode**: The RD licensing mode to use. PerUser, PerDevice, or NotConfigured. + +### xRDCertificateConfiguration + +* **Role**: Specifies the role that nees to be configured ('RDRedirector', 'RDPublishing', 'RDWebAccess', 'RDGateway'). +* **ConnectionBroker**: Specifies the Remote Desktop Connection Broker (RD Connection Broker) server for a Remote Desktop deployment. +* **ImportPath**: The certificate that should be used, should point to a PFX file on the filesystem. +* **Credential**: The password (if applicable) for the PFX file. The username is ignored. From 83f6852936c09c29dbadf9019f60e557188812e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20Both?= Date: Sun, 5 Jan 2020 22:07:03 +0100 Subject: [PATCH 06/10] Moving files to right location and adding change to changelog --- CHANGELOG.MD | 2 ++ .../MSFT_xRDCertificateConfiguration.psm1 | 0 .../MSFT_xRDCertificateConfiguration.schema.mof | 0 .../en-US/MSFT_xRDCertificateConfiguration.strings.psd1 | 0 source/xRemoteDesktopSessionHost.psd1 | 1 + 5 files changed, 3 insertions(+) rename {DSCResources => source/DSCResources}/MSFT_xRDCertificateConfiguration/MSFT_xRDCertificateConfiguration.psm1 (100%) rename {DSCResources => source/DSCResources}/MSFT_xRDCertificateConfiguration/MSFT_xRDCertificateConfiguration.schema.mof (100%) rename {DSCResources => source/DSCResources}/MSFT_xRDCertificateConfiguration/en-US/MSFT_xRDCertificateConfiguration.strings.psd1 (100%) diff --git a/CHANGELOG.MD b/CHANGELOG.MD index f9c085c..45345cb 100644 --- a/CHANGELOG.MD +++ b/CHANGELOG.MD @@ -10,6 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - xRemoteDesktopSessionHost - Added automatic release with a new CI pipeline. - Added DSC HQRM Tests +- xRDCertificateConfiguration + - New resource to configure the used certificate on a deployment ### Changed diff --git a/DSCResources/MSFT_xRDCertificateConfiguration/MSFT_xRDCertificateConfiguration.psm1 b/source/DSCResources/MSFT_xRDCertificateConfiguration/MSFT_xRDCertificateConfiguration.psm1 similarity index 100% rename from DSCResources/MSFT_xRDCertificateConfiguration/MSFT_xRDCertificateConfiguration.psm1 rename to source/DSCResources/MSFT_xRDCertificateConfiguration/MSFT_xRDCertificateConfiguration.psm1 diff --git a/DSCResources/MSFT_xRDCertificateConfiguration/MSFT_xRDCertificateConfiguration.schema.mof b/source/DSCResources/MSFT_xRDCertificateConfiguration/MSFT_xRDCertificateConfiguration.schema.mof similarity index 100% rename from DSCResources/MSFT_xRDCertificateConfiguration/MSFT_xRDCertificateConfiguration.schema.mof rename to source/DSCResources/MSFT_xRDCertificateConfiguration/MSFT_xRDCertificateConfiguration.schema.mof diff --git a/DSCResources/MSFT_xRDCertificateConfiguration/en-US/MSFT_xRDCertificateConfiguration.strings.psd1 b/source/DSCResources/MSFT_xRDCertificateConfiguration/en-US/MSFT_xRDCertificateConfiguration.strings.psd1 similarity index 100% rename from DSCResources/MSFT_xRDCertificateConfiguration/en-US/MSFT_xRDCertificateConfiguration.strings.psd1 rename to source/DSCResources/MSFT_xRDCertificateConfiguration/en-US/MSFT_xRDCertificateConfiguration.strings.psd1 diff --git a/source/xRemoteDesktopSessionHost.psd1 b/source/xRemoteDesktopSessionHost.psd1 index 51ef6cf..06495be 100644 --- a/source/xRemoteDesktopSessionHost.psd1 +++ b/source/xRemoteDesktopSessionHost.psd1 @@ -31,6 +31,7 @@ # DSC resources to export from this module DscResourcesToExport = @( + 'xRDCertificateConfiguration' 'xRDGatewayConfiguration' 'xRDLicenseConfiguration' 'xRDRemoteApp' From 766d02055768f9649f0417106f1a56239fca625c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20Both?= Date: Sun, 5 Jan 2020 22:19:01 +0100 Subject: [PATCH 07/10] Update reference to xRemoteDesktopSessionHostCommon --- .../MSFT_xRDCertificateConfiguration.psm1 | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/source/DSCResources/MSFT_xRDCertificateConfiguration/MSFT_xRDCertificateConfiguration.psm1 b/source/DSCResources/MSFT_xRDCertificateConfiguration/MSFT_xRDCertificateConfiguration.psm1 index c01a5e3..0ccd1cc 100644 --- a/source/DSCResources/MSFT_xRDCertificateConfiguration/MSFT_xRDCertificateConfiguration.psm1 +++ b/source/DSCResources/MSFT_xRDCertificateConfiguration/MSFT_xRDCertificateConfiguration.psm1 @@ -1,5 +1,8 @@ -Import-Module -Name "$PSScriptRoot\..\..\xRemoteDesktopSessionHostCommon.psm1" -if (!(Test-xRemoteDesktopSessionHostOsRequirement)) { throw "The minimum OS requirement was not met."} +Import-Module -Name "$PSScriptRoot\..\..\Modules\xRemoteDesktopSessionHostCommon.psm1" +if (!(Test-xRemoteDesktopSessionHostOsRequirement)) +{ + throw "The minimum OS requirement was not met." +} Import-Module RemoteDesktop $script:localizedData = Get-LocalizedData -ResourceName 'MSFT_xRDCertificateConfiguration' From cd22bbd14363a64bd8ff67f52d03e28a78354789 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20Both?= Date: Sun, 5 Jan 2020 23:47:46 +0100 Subject: [PATCH 08/10] Update Get-LocalizedData --- source/Modules/xRemoteDesktopSessionHostCommon.psm1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/Modules/xRemoteDesktopSessionHostCommon.psm1 b/source/Modules/xRemoteDesktopSessionHostCommon.psm1 index 71b71b9..66d73a0 100644 --- a/source/Modules/xRemoteDesktopSessionHostCommon.psm1 +++ b/source/Modules/xRemoteDesktopSessionHostCommon.psm1 @@ -45,7 +45,7 @@ function Get-LocalizedData if (-not $ScriptRoot) { - $dscResourcesFolder = Join-Path -Path $PSScriptRoot -ChildPath 'DSCResources' + $dscResourcesFolder = Join-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -ChildPath 'DSCResources' $resourceDirectory = Join-Path -Path $dscResourcesFolder -ChildPath $ResourceName } else From 6ea61bd44d8bf070bbe3082bd6ef30f449c43b76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20Both?= Date: Mon, 6 Jan 2020 00:00:17 +0100 Subject: [PATCH 09/10] Update xRDCertificateConfiguration tests to new template --- ...MSFT_xRDCertificateConfiguration.tests.ps1 | 46 ++++++++----------- 1 file changed, 20 insertions(+), 26 deletions(-) diff --git a/Tests/Unit/MSFT_xRDCertificateConfiguration.tests.ps1 b/Tests/Unit/MSFT_xRDCertificateConfiguration.tests.ps1 index 5a2159d..e32d1b6 100644 --- a/Tests/Unit/MSFT_xRDCertificateConfiguration.tests.ps1 +++ b/Tests/Unit/MSFT_xRDCertificateConfiguration.tests.ps1 @@ -1,42 +1,36 @@ -$script:DSCModuleName = '.\xRemoteDesktopSessionHost' +$script:DSCModuleName = 'xRemoteDesktopSessionHost' $script:DSCResourceName = 'MSFT_xRDCertificateConfiguration' #region HEADER -# Unit Test Template Version: 1.2.1 -$script:moduleRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot) -Write-Output @('clone','https://github.com/PowerShell/DscResource.Tests.git',"'"+(Join-Path -Path $script:moduleRoot -ChildPath '\DSCResource.Tests')+"'") - -if ( (-not (Test-Path -Path (Join-Path -Path $script:moduleRoot -ChildPath 'DSCResource.Tests'))) -or ` - (-not (Test-Path -Path (Join-Path -Path $script:moduleRoot -ChildPath 'DSCResource.Tests\TestHelper.psm1'))) ) +function Invoke-TestSetup { - & git @('clone','https://github.com/PowerShell/DscResource.Tests.git',(Join-Path -Path $script:moduleRoot -ChildPath 'DSCResource.Tests'),'--verbose') -} - -Import-Module -Name (Join-Path -Path $script:moduleRoot -ChildPath (Join-Path -Path 'DSCResource.Tests' -ChildPath 'TestHelper.psm1')) -Force - -$TestEnvironment = Initialize-TestEnvironment ` - -DSCModuleName $script:DSCModuleName ` - -DSCResourceName $script:DSCResourceName ` - -TestType Unit - -#endregion HEADER - -function Invoke-TestSetup { + try + { + Import-Module -Name DscResource.Test -Force + } + catch [System.IO.FileNotFoundException] + { + throw 'DscResource.Test module dependency not found. Please run ".\build.ps1 -Tasks build" first.' + } + $script:testEnvironment = Initialize-TestEnvironment ` + -DSCModuleName $script:dscModuleName ` + -DSCResourceName $script:dscResourceName ` + -ResourceType 'Mof' ` + -TestType 'Unit' } -function Invoke-TestCleanup { - Restore-TestEnvironment -TestEnvironment $TestEnvironment +function Invoke-TestCleanup +{ + Restore-TestEnvironment -TestEnvironment $script:testEnvironment } -# Begin Testing +Invoke-TestSetup try { - Invoke-TestSetup - - InModuleScope $script:DSCResourceName { + InModuleScope $script:dscResourceName { $script:DSCResourceName = 'MSFT_xRDCertificateConfiguration' Import-Module RemoteDesktop -Force From e746158eb697c2fcbd0d478a46c4244a22101f72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20Both?= Date: Mon, 6 Jan 2020 08:35:58 +0100 Subject: [PATCH 10/10] Update test for error to use Should -Throw instead --- Tests/Unit/MSFT_xRDCertificateConfiguration.tests.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/Unit/MSFT_xRDCertificateConfiguration.tests.ps1 b/Tests/Unit/MSFT_xRDCertificateConfiguration.tests.ps1 index e32d1b6..0a0b9bb 100644 --- a/Tests/Unit/MSFT_xRDCertificateConfiguration.tests.ps1 +++ b/Tests/Unit/MSFT_xRDCertificateConfiguration.tests.ps1 @@ -246,8 +246,8 @@ try } It 'Set-TargetResource returns an error when the certificate could not be applied' { - $errorMessage = Set-TargetResource @resourceWrongConfiguredSplat 2>&1 - $errorMessage | Should -Not -BeNullOrEmpty + { Set-TargetResource @resourceWrongConfiguredSplat -ErrorAction Stop } | + Should -Throw 'Failed to apply certificate' } } }