From 65daba17cd9874bc7c77176dfe250fd3b0a819bd Mon Sep 17 00:00:00 2001 From: Michael Zanatta Date: Sun, 7 May 2023 18:41:23 +1000 Subject: [PATCH 1/3] Updating Set-ModuleParams to support CertificateThumbprint Public Key Cert is exported to ProgramData Updated Add-SRDSCNode to support automatic cert enrollment --- .../Private/Module/Set-ModuleParameters.ps1 | 11 ++- Module/Public/Add-SRDSCNode.ps1 | 50 ++++++---- Module/Public/Initialize-SRDSC.ps1 | 96 ++++++++++--------- Tests/Private/Set-ModuleParameters.tests.ps1 | 4 +- 4 files changed, 96 insertions(+), 65 deletions(-) diff --git a/Module/Private/Module/Set-ModuleParameters.ps1 b/Module/Private/Module/Set-ModuleParameters.ps1 index 1716062..7b51c9b 100644 --- a/Module/Private/Module/Set-ModuleParameters.ps1 +++ b/Module/Private/Module/Set-ModuleParameters.ps1 @@ -46,7 +46,8 @@ $params = @{ PullServerRegistrationKey = $CliXML.PullServerRegistrationKey DSCPullServer = $CliXML.DSCPullServer DSCPullServerHTTP = $CliXML.DSCPullServerHTTP - ScriptRunnerURL = $CliXML.ScriptRunnerURL + ScriptRunnerURL = $CliXML.ScriptRunnerURL + CertificateThumbprint = $CliXML.CertificateThumbprint } # Load the Global Settings @@ -77,7 +78,10 @@ Set's Global Configuration paramters used by the SRDSC Module. $DSCPullServerHTTP, [Parameter(Mandatory)] [String] - $ScriptRunnerURL + $ScriptRunnerURL, + [Parameter(Mandatory)] + [String] + $CertificateThumbprint ) $Global:SRDSC = [PSCustomObject]@{ @@ -98,7 +102,8 @@ Set's Global Configuration paramters used by the SRDSC Module. DSCPullServerMOFPath = 'C$\Program Files\WindowsPowerShell\DscService\Configuration\' DSCPullServerResourceModules = 'C$\Program Files\WindowsPowerShell\DscService\Modules\' DSCPullServerWebAddress = '{0}://{1}:8080' -f $DSCPullServerHTTP, $DSCPullServer - PullServerRegistrationKey = $PullServerRegistrationKey + PullServerRegistrationKey = $PullServerRegistrationKey + CertificateThumbprint = $CertificateThumbprint } DatumModule = [PSCustomObject]@{ diff --git a/Module/Public/Add-SRDSCNode.ps1 b/Module/Public/Add-SRDSCNode.ps1 index 3705c46..e8bad6c 100644 --- a/Module/Public/Add-SRDSCNode.ps1 +++ b/Module/Public/Add-SRDSCNode.ps1 @@ -55,17 +55,18 @@ function Add-SRDSCNode { Write-Host "[Add-SRDSCNode] PowerShell Remoting to $NodeName to Register LCM to $DSCPullServer" $RegistrationKey = $Global:SRDSC.DSCPullServer.PullServerRegistrationKey - + $CertificateByteArray = Get-Content -LiteralPath "{0}\PowerShell\SRDSC\PullServer.cer" -f $Env:ProgramData -Raw -Encoding Byte + # Load the DSC Server Configuration Data $invokeCommandParams = @{ - ArgumentList = $DSCPullServer,$Force,$RegistrationKey,$UseConfigurationIDs + ArgumentList = $DSCPullServer,$Force,$RegistrationKey,$UseConfigurationIDs,$CertificateByteArray ComputerName = $NodeName ErrorAction = 'Stop' } $NodeDSCLCMConfiguration = Invoke-Command @invokeCommandParams -ScriptBlock { - param($DSCPullServer, $Force, $RegistrationKey, $UseConfigurationIDs) + param($DSCPullServer, $Force, $RegistrationKey, $UseConfigurationIDs, $CertificateByteArray) # # Functions @@ -107,7 +108,27 @@ function Add-SRDSCNode { return Get-DscLocalConfigurationManager } - + # + # Install the DSC Certificate + # + + $certificateLocation = 'C:\Windows\Temp\SRDSCClientCertificate.cer' + $certificateStore = 'Cert:\LocalMachine\Root' + + # Remove any existing certificate at the specified location + Remove-Item -LiteralPath $certificateLocation -Force -ErrorAction SilentlyContinue + + # Save the certificate to the file system + [IO.File]::WriteAllBytes($certificateLocation, $CertificateByteArray) + + # Remove any existing certificates with matching subject name from the root store + Get-ChildItem $certificateStore -Recurse | + Where-Object {$_.Subject -like "*DSC.$ENV:USERDNSDOMAIN"} | + Remove-Item -Force -Confirm:$false + + # Import the certificate into the root store + Import-Certificate -FilePath $certificateLocation -CertStoreLocation $certificateStore + # # Compile the DSC Resource # @@ -148,7 +169,7 @@ function Add-SRDSCNode { [DSCLocalConfigurationManager()] configuration PullClientConfigNames { - + Node localhost { @@ -181,24 +202,21 @@ function Add-SRDSCNode { if ($UseConfigurationIDs.IsPresent) { Write-Host "[Add-SRDSCNode] Writing ConfigurationID of Node as [SecureString]" - - # - # The LCM Configuration is needed to register the ConfigurationID. - # This is used by the datum configuration to rename the mof files + + # Import existing node registrations from file, if any $DatumLCMConfiguration = @() - if (Test-Path -LiteralPath $Global:SRDSC.DatumModule.NodeRegistrationFile) { - $NodeRegistrationFile += Import-Clixml -LiteralPath $Global:SRDSC.DatumModule.NodeRegistrationFile - # Filter out the existing node node. This enable rewrites - $DatumLCMConfiguration = @() - $DatumLCMConfiguration += $NodeRegistrationFile | Where-Object {$_.NodeName -ne $NodeName} + $NodeRegistrationFile = Import-Clixml -LiteralPath $Global:SRDSC.DatumModule.NodeRegistrationFile + # Filter out the existing node to enable rewrites + $DatumLCMConfiguration = $NodeRegistrationFile | Where-Object {$_.NodeName -ne $NodeName} } - + + # Add the new node registration to the configuration list $DatumLCMConfiguration += [PSCustomObject]@{ NodeName = $NodeName ConfigurationID = [String]$NodeDSCLCMConfiguration.ConfigurationID | ConvertTo-SecureString -AsPlainText -Force } - + # Export it again $DatumLCMConfiguration | Export-Clixml -LiteralPath $Global:SRDSC.DatumModule.NodeRegistrationFile diff --git a/Module/Public/Initialize-SRDSC.ps1 b/Module/Public/Initialize-SRDSC.ps1 index 30929ea..b121e84 100644 --- a/Module/Public/Initialize-SRDSC.ps1 +++ b/Module/Public/Initialize-SRDSC.ps1 @@ -161,29 +161,9 @@ Onboarding script to install DSC Pull Server, Datum, and ScriptRunner Scripts. # Create Configuration file to store the datum module information $ConfigurationPath = "{0}\PowerShell\SRDSC\Configuration.clixml" -f $Env:ProgramData + $ClientCertificatePath = "{0}\PowerShell\SRDSC\PullServer.cer" -f $Env:ProgramData $ConfigurationParentPath = Split-Path $ConfigurationPath -Parent - # - # Set the Global Vars - - $SRConfiguration = @{ - DatumModulePath = $DatumModulePath - ScriptRunnerModulePath = Split-Path (Get-Module SRDSC).Path -Parent - ScriptRunnerServerPath = $ScriptRunnerServerPath - PullServerRegistrationKey = [guid]::newGuid().Guid - DSCPullServer = "DSC.{0}" -f $ComputerDomainName - DSCPullServerHTTP = $( - if ($UseSelfSignedCertificate.IsPresent -or $PFXCertificatePath) { - 'https' - } else { - 'http' - } - ) - ScriptRunnerURL = $ScriptRunnerURL - } - - Set-ModuleParameters @SRConfiguration - # # Load SSL Certificates @@ -211,6 +191,7 @@ Onboarding script to install DSC Pull Server, Datum, and ScriptRunner Scripts. # # If the SSL Certificate Path parameter was specified, import the cert + if ($PFXCertificatePath) { # @@ -225,42 +206,60 @@ Onboarding script to install DSC Pull Server, Datum, and ScriptRunner Scripts. $certificate = Import-PfxCertificate @params } - $xDscPullServerRegistrationParams = @{ + # + # Set the Global Vars - NodeName = 'localhost' - - xDscWebServiceRegistrationParams = @{ + $SRConfiguration = @{ + DatumModulePath = $DatumModulePath + ScriptRunnerModulePath = Split-Path (Get-Module SRDSC).Path -Parent + ScriptRunnerServerPath = $ScriptRunnerServerPath + PullServerRegistrationKey = [guid]::newGuid().Guid + DSCPullServer = "DSC.{0}" -f $ComputerDomainName + DSCPullServerHTTP = $( + if ($UseSelfSignedCertificate.IsPresent -or $PFXCertificatePath) { + 'https' + } else { + 'http' + } + ) + ScriptRunnerURL = $ScriptRunnerURL + CertificateThumbPrint = $certificate.Thumbprint + } - RegistrationKey = $SRConfiguration.PullServerRegistrationKey - WebServerFilePath = $PullWebServerPath - CertificateThumbPrint = $certificate.Thumbprint + Set-ModuleParameters @SRConfiguration - } + # + # Define a hashtable containing parameters for registering the local node with a DSC pull server + $xDscPullServerRegistrationParams = @{ + NodeName = 'localhost' # Specify the name of the local node to register + xDscWebServiceRegistrationParams = @{ + RegistrationKey = $SRConfiguration.PullServerRegistrationKey # Specify the registration key for the pull server + WebServerFilePath = $PullWebServerPath # Specify the path to the pull server web service endpoint + CertificateThumbPrint = $certificate.Thumbprint # Specify the thumbprint of the certificate to use for authentication + } xDscDatumModuleRegistrationParams = @{ - - DatumModulePath = $Global:SRDSC.DatumModule.DatumModulePath - DatumModuleTemplatePath = "{0}\{1}" -f $Global:SRDSC.DatumModule.DatumTemplates, (Split-Path $Global:SRDSC.ScriptRunner.NodeTemplateFile -Leaf) - SRDSCTemplateFile = $Global:SRDSC.ScriptRunner.NodeTemplateFile - + DatumModulePath = $Global:SRDSC.DatumModule.DatumModulePath # Specify the path to the datum module used by the script runner + DatumModuleTemplatePath = "{0}\{1}" -f $Global:SRDSC.DatumModule.DatumTemplates, (Split-Path $Global:SRDSC.ScriptRunner.NodeTemplateFile -Leaf) # Specify the path to the datum module template used by the script runner + SRDSCTemplateFile = $Global:SRDSC.ScriptRunner.NodeTemplateFile # Specify the path to the script runner node template file } - xDscSRDSCModuleRegistrationParams = @{ - ConfigurationParentPath = $ConfigurationParentPath - ScriptRunnerDSCRepository = $Global:SRDSC.ScriptRunner.ScriptRunnerDSCRepository + ConfigurationParentPath = $ConfigurationParentPath # Specify the path to the parent configuration directory for the script runner + ScriptRunnerDSCRepository = $Global:SRDSC.ScriptRunner.ScriptRunnerDSCRepository # Specify the path to the script runner DSC repository Files = @( - "{0}\Template\Publish-SRAction.ps1" -f $ModuleDirectory - "{0}\Template\Start-SRDSC.ps1" -f $ModuleDirectory - "{0}\Template\New-VirtualMachine.ps1" -f $ModuleDirectory + "{0}\Template\Publish-SRAction.ps1" -f $ModuleDirectory # Specify the path to the Publish-SRAction.ps1 script used by the script runner + "{0}\Template\Start-SRDSC.ps1" -f $ModuleDirectory # Specify the path to the Start-SRDSC.ps1 script used by the script runner + "{0}\Template\New-VirtualMachine.ps1" -f $ModuleDirectory # Specify the path to the New-VirtualMachine.ps1 script used by the script runner ) } - - OutputPath = 'C:\Windows\Temp\' - + OutputPath = 'C:\Windows\Temp\' # Specify the output path for the registration files } - + + # Register the local node with a DSC pull server using the parameters defined in the $xDscPullServerRegistrationParams hashtable xDscPullServerRegistration @xDscPullServerRegistrationParams - Start-DscConfiguration -Path 'C:\Windows\Temp' -Wait -Verbose -Force + + # Start a DSC configuration at the specified path, wait for it to complete, and output verbose messages + Start-DscConfiguration -Path 'C:\Windows\Temp' -Wait -Verbose -Force # # Use PowerShell Remoting and Invoke-DSCResource to create an C-NAME @@ -298,6 +297,13 @@ Onboarding script to install DSC Pull Server, Datum, and ScriptRunner Scripts. # Export the Configuration ([PSCustomObject]$SRConfiguration) | Export-Clixml -LiteralPath $ConfigurationPath + # + # Export the Public Certificate to ProgramData\PowerShell\SRDSC + # This will be used to onboard nodes into the DSC Pull Server + + (Get-ChildItem Cert:\LocalMachine\ -Recurse | Where-Object {$_.Subject -like ('*DSC.{0}*' -f $ENV:USERDNSDOMAIN)})[0] | + Export-Certificate -Force -FilePath $ClientCertificatePath + # # Clone the DSCWorkshop PowerShell Module (contains Datum) Write-Warning "[Initialize-SRDSC] Installing DSCWorkshop:" diff --git a/Tests/Private/Set-ModuleParameters.tests.ps1 b/Tests/Private/Set-ModuleParameters.tests.ps1 index 3e5ad46..d9336c3 100644 --- a/Tests/Private/Set-ModuleParameters.tests.ps1 +++ b/Tests/Private/Set-ModuleParameters.tests.ps1 @@ -20,7 +20,8 @@ Describe "Testing Set-Module Parameters" { PullServerRegistrationKey = 'MOCK' DSCPullServer = 'MOCK' DSCPullServerHTTP = 'https' - ScriptRunnerURL = 'MOCK' + ScriptRunnerURL = 'MOCK' + CertificateThumbPrint = 'MOCK' } # @@ -41,6 +42,7 @@ Describe "Testing Set-Module Parameters" { $Global:SRDSC.DSCPullServer.DSCPullServerResourceModules | Should -Not -BeNullorEmpty $Global:SRDSC.DSCPullServer.DSCPullServerWebAddress | Should -Not -BeNullorEmpty $Global:SRDSC.DSCPullServer.PullServerRegistrationKey | Should -Not -BeNullorEmpty + $Global:SRDSC.DSCPullServer.CertificateThumbPrint | Should -Not -BeNullOrEmpty $Global:SRDSC.DatumModule.DatumModulePath | Should -Not -BeNullOrEmpty $Global:SRDSC.DatumModule.DatumTemplates | Should -Not -BeNullOrEmpty From 52c644ef96aff72807e8565340fd40b74808faef Mon Sep 17 00:00:00 2001 From: Michael Zanatta Date: Wed, 14 Jun 2023 17:49:05 +1000 Subject: [PATCH 2/3] Updating Unit Tests Updating Version --- .../Module/ConvertTo-PowerShellParameter.ps1 | 18 +++++ .../ConvertTo-PowerShellParameter.tests.ps1 | 69 ++++++++++++++++++- _Build/BuildVersion.txt | 3 +- 3 files changed, 86 insertions(+), 4 deletions(-) diff --git a/Module/Private/Module/ConvertTo-PowerShellParameter.ps1 b/Module/Private/Module/ConvertTo-PowerShellParameter.ps1 index 7f793b6..cc22a42 100644 --- a/Module/Private/Module/ConvertTo-PowerShellParameter.ps1 +++ b/Module/Private/Module/ConvertTo-PowerShellParameter.ps1 @@ -54,6 +54,24 @@ Converts the Datum and Template configuration into PowerShell Script Parameters. Process { + # + # If the configuration is null, throw an error. + if ($null -eq $ConfigurationTemplates) { + throw "ConfigurationTemplates cannot be null" + } + + # + # If the DatumConfiguration is null, throw an error. + if ($null -eq $ConfigurationTemplates.DatumConfiguration) { + throw "ConfigurationTemplates.DatumConfiguration cannot be null" + } + + # + # If the TemplateConfiguration is null, throw an error. + if ($null -eq $ConfigurationTemplates.TemplateConfiguration) { + throw "ConfigurationTemplates.TemplateConfiguration cannot be null" + } + # # NodeTemplateConfiguration items have higher precidence then automatic values. # However it's possible to define positions within the configuration. diff --git a/Tests/Private/ConvertTo-PowerShellParameter.tests.ps1 b/Tests/Private/ConvertTo-PowerShellParameter.tests.ps1 index 5fcd620..2242e4b 100644 --- a/Tests/Private/ConvertTo-PowerShellParameter.tests.ps1 +++ b/Tests/Private/ConvertTo-PowerShellParameter.tests.ps1 @@ -1,4 +1,67 @@ -Describe "Testing ConvertTo-PowerShellParameter" -Skip { +Describe "ConvertTo-PowerShellParameter" { - -} \ No newline at end of file + BeforeAll { + $configurationTemplates = @{ + DatumConfiguration = @( + @{ + ParameterName = "NodeName" + ParameterValues = "Node1", "Node2", "Node3" + isOverwritten = $false + }, + @{ + ParameterName = "IPAddress" + ParameterValues = "10.0.0.1", "10.0.0.2", "10.0.0.3" + isOverwritten = $true + } + ) + TemplateConfiguration = @( + @{ + ParameterName = "VMName" + YAMLPath = "VirtualMachine/Name" + }, + @{ + ParameterName = "VMSize" + YAMLPath = "VirtualMachine/Size" + } + ) + } + } + + It "returns a string of PowerShell parameters" { + $result = ConvertTo-PowerShellParameter -ConfigurationTemplates $configurationTemplates + $result | Should -BeOfType [String] + } + + Context "when given valid input" { + It "returns a string with mandatory parameters" { + $result = ConvertTo-PowerShellParameter -ConfigurationTemplates $configurationTemplates + $result | Should -Match "`t\[Parameter\(Mandatory\)\]" + } + + It "returns a string with ValidateSet attributes" { + $result = ConvertTo-PowerShellParameter -ConfigurationTemplates $configurationTemplates + $result | Should -Match "`t\[ValidateSet\('.+'\)\]*" + } + + It "returns a string with String data type" { + $result = ConvertTo-PowerShellParameter -ConfigurationTemplates $configurationTemplates + $result | Should -Match "`t\[String\]" + } + } + + Context "when given invalid input" { + It "throws an error when ConfigurationTemplates is null" { + { ConvertTo-PowerShellParameter -ConfigurationTemplates $null } | Should -Throw + } + + It "throws an error when DatumConfiguration is null" { + $configurationTemplates.DatumConfiguration = $null + { ConvertTo-PowerShellParameter -ConfigurationTemplates $configurationTemplates } | Should -Throw + } + + It "throws an error when TemplateConfiguration is null" { + $configurationTemplates.TemplateConfiguration = $null + { ConvertTo-PowerShellParameter -ConfigurationTemplates $configurationTemplates } | Should -Throw + } + } +} diff --git a/_Build/BuildVersion.txt b/_Build/BuildVersion.txt index 156b09b..6bd5f84 100644 --- a/_Build/BuildVersion.txt +++ b/_Build/BuildVersion.txt @@ -1,4 +1,5 @@ 0.0.1-alpha 0.0.2-alpha 0.0.3-alpha -0.0.4-alpha \ No newline at end of file +0.0.4-alpha +0.0.5-alpha \ No newline at end of file From 1197a798d2090ffa743499e7745ccd4c1b7ad1b0 Mon Sep 17 00:00:00 2001 From: Michael Zanatta Date: Wed, 14 Jun 2023 18:13:08 +1000 Subject: [PATCH 3/3] Fixing Unit Tests --- .../Module/ConvertTo-PowerShellParameter.ps1 | 4 +-- .../ConvertTo-PowerShellParameter.tests.ps1 | 34 +++++++++++++++++-- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/Module/Private/Module/ConvertTo-PowerShellParameter.ps1 b/Module/Private/Module/ConvertTo-PowerShellParameter.ps1 index 5cc7e62..c229f8f 100644 --- a/Module/Private/Module/ConvertTo-PowerShellParameter.ps1 +++ b/Module/Private/Module/ConvertTo-PowerShellParameter.ps1 @@ -48,13 +48,13 @@ Converts the Datum and Template configuration into PowerShell Script Parameters. # # If the DatumConfiguration is null, throw an error. - if ($null -eq $ConfigurationTemplates.DatumConfiguration) { + if (($null -eq $ConfigurationTemplates.DatumConfiguration) -or ($ConfigurationTemplates.DatumConfiguration.Count -eq 0)) { throw "ConfigurationTemplates.DatumConfiguration cannot be null" } # # If the TemplateConfiguration is null, throw an error. - if ($null -eq $ConfigurationTemplates.TemplateConfiguration) { + if (($null -eq $ConfigurationTemplates.TemplateConfiguration) -or ($ConfigurationTemplates.TemplateConfiguration.Count -eq 0)) { throw "ConfigurationTemplates.TemplateConfiguration cannot be null" } diff --git a/Tests/Private/ConvertTo-PowerShellParameter.tests.ps1 b/Tests/Private/ConvertTo-PowerShellParameter.tests.ps1 index 987c5be..29e6568 100644 --- a/Tests/Private/ConvertTo-PowerShellParameter.tests.ps1 +++ b/Tests/Private/ConvertTo-PowerShellParameter.tests.ps1 @@ -101,17 +101,47 @@ Describe "Testing ConvertTo-PowerShellParameter" { } Context "when given invalid input" { + It "throws an error when ConfigurationTemplates is null" { { ConvertTo-PowerShellParameter -ConfigurationTemplates $null } | Should -Throw } + It "throws an error when DatumConfiguration contains an empty array" { + + $configurationTemplates = @{ + DatumConfiguration = @() + TemplateConfiguration = @('value1', 'value2') + } + + { ConvertTo-PowerShellParameter -ConfigurationTemplates $configurationTemplates } | Should -Throw + } + + It "throws an error when TemplateConfiguration contains an empty array" { + + $configurationTemplates = @{ + DatumConfiguration = @('value1', 'value2') + TemplateConfiguration = @() + } + + { ConvertTo-PowerShellParameter -ConfigurationTemplates $configurationTemplates } | Should -Throw + } + It "throws an error when DatumConfiguration is null" { - $configurationTemplates.DatumConfiguration = $null + + $configurationTemplates = @{ + DatumConfiguration = $null + TemplateConfiguration = 'value1' + } + { ConvertTo-PowerShellParameter -ConfigurationTemplates $configurationTemplates } | Should -Throw } It "throws an error when TemplateConfiguration is null" { - $configurationTemplates.TemplateConfiguration = $null + $configurationTemplates = @{ + DatumConfiguration = 'value1' + TemplateConfiguration = $null + } + { ConvertTo-PowerShellParameter -ConfigurationTemplates $configurationTemplates } | Should -Throw } }