diff --git a/powershell-adapter/Tests/TestClassResource/0.0.1/TestClassResource.psm1 b/powershell-adapter/Tests/TestClassResource/0.0.1/TestClassResource.psm1 index 44b9128a6..44571f9a7 100644 --- a/powershell-adapter/Tests/TestClassResource/0.0.1/TestClassResource.psm1 +++ b/powershell-adapter/Tests/TestClassResource/0.0.1/TestClassResource.psm1 @@ -29,6 +29,9 @@ class TestClassResource : BaseTestClass [DscProperty()] [string] $EnumProp + [DscProperty()] + [PSCredential] $Credential + [string] $NonDscProperty # This property shouldn't be in results data hidden diff --git a/powershell-adapter/Tests/powershellgroup.config.tests.ps1 b/powershell-adapter/Tests/powershellgroup.config.tests.ps1 index 23d52e75f..9cdfdb264 100644 --- a/powershell-adapter/Tests/powershellgroup.config.tests.ps1 +++ b/powershell-adapter/Tests/powershellgroup.config.tests.ps1 @@ -237,4 +237,40 @@ Describe 'PowerShell adapter resource tests' { } } } + + It 'Config works with credential object' { + $yaml = @" + `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + resources: + - name: Class-resource Info + type: TestClassResource/TestClassResource + properties: + Name: 'TestClassResource' + Credential: + UserName: 'User' + Password: 'Password' +"@ + $out = dsc config get -i $yaml | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 + $out.results.result.actualstate.Credential.UserName | Should -Be 'User' + $out.results.result.actualState.result.Credential.Password.Length | Should -Not -BeNullOrEmpty + } + + It 'Config does not work when credential properties are missing required fields' { + $yaml = @" + `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + resources: + - name: Class-resource credential info + type: TestClassResource/TestClassResource + properties: + Name: 'TestClassResource' + Credential: + UserName: 'User' + OtherProperty: 'Password' +"@ + $out = dsc config get -i $yaml 2>&1 | Out-String + $LASTEXITCODE | Should -Be 2 + $out | Should -Not -BeNullOrEmpty + $out | Should -BeLike "*ERROR*Credential object 'Credential' requires both 'username' and 'password' properties*" + } } diff --git a/powershell-adapter/Tests/win_powershellgroup.tests.ps1 b/powershell-adapter/Tests/win_powershellgroup.tests.ps1 index 65c5ffe64..b13b2b8b3 100644 --- a/powershell-adapter/Tests/win_powershellgroup.tests.ps1 +++ b/powershell-adapter/Tests/win_powershellgroup.tests.ps1 @@ -3,107 +3,110 @@ Describe 'WindowsPowerShell adapter resource tests - requires elevated permissions' { - BeforeAll { - if ($isWindows) { - winrm quickconfig -quiet -force - } - $OldPSModulePath = $env:PSModulePath - $env:PSModulePath += [System.IO.Path]::PathSeparator + $PSScriptRoot - - $winpsConfigPath = Join-path $PSScriptRoot "winps_resource.dsc.yaml" - if ($isWindows) { - $cacheFilePath_v5 = Join-Path $env:LocalAppData "dsc" "WindowsPSAdapterCache.json" - } - } - AfterAll { - $env:PSModulePath = $OldPSModulePath + BeforeAll { + if ($isWindows) { + winrm quickconfig -quiet -force } + $OldPSModulePath = $env:PSModulePath + $env:PSModulePath += [System.IO.Path]::PathSeparator + $PSScriptRoot - BeforeEach { - if ($isWindows) { - Remove-Item -Force -ea SilentlyContinue -Path $cacheFilePath_v5 - } + $winpsConfigPath = Join-path $PSScriptRoot "winps_resource.dsc.yaml" + if ($isWindows) { + $cacheFilePath_v5 = Join-Path $env:LocalAppData "dsc" "WindowsPSAdapterCache.json" } + } + AfterAll { + $env:PSModulePath = $OldPSModulePath - It 'Windows PowerShell adapter supports File resource' -Skip:(!$IsWindows){ + # Remove after all the tests are done + Remove-Module $script:winPSModule -Force -ErrorAction Ignore + } - $r = dsc resource list --adapter Microsoft.Windows/WindowsPowerShell - $LASTEXITCODE | Should -Be 0 - $resources = $r | ConvertFrom-Json - ($resources | Where-Object {$_.Type -eq 'PSDesiredStateConfiguration/File'}).Count | Should -Be 1 + BeforeEach { + if ($isWindows) { + Remove-Item -Force -ea SilentlyContinue -Path $cacheFilePath_v5 } + } - It 'Get works on Binary "File" resource' -Skip:(!$IsWindows){ + It 'Windows PowerShell adapter supports File resource' -Skip:(!$IsWindows) { - $testFile = "$testdrive\test.txt" - 'test' | Set-Content -Path $testFile -Force - $r = '{"DestinationPath":"' + $testFile.replace('\','\\') + '"}' | dsc resource get -r 'PSDesiredStateConfiguration/File' -f - - $LASTEXITCODE | Should -Be 0 - $res = $r | ConvertFrom-Json - $res.actualState.DestinationPath | Should -Be "$testFile" - } + $r = dsc resource list --adapter Microsoft.Windows/WindowsPowerShell + $LASTEXITCODE | Should -Be 0 + $resources = $r | ConvertFrom-Json + ($resources | Where-Object { $_.Type -eq 'PSDesiredStateConfiguration/File' }).Count | Should -Be 1 + } - It 'Set works on Binary "File" resource' -Skip:(!$IsWindows){ + It 'Get works on Binary "File" resource' -Skip:(!$IsWindows) { - $testFile = "$testdrive\test.txt" - $null = '{"DestinationPath":"' + $testFile.replace('\','\\') + '", type: File, contents: HelloWorld, Ensure: present}' | dsc resource set -r 'PSDesiredStateConfiguration/File' -f - - $LASTEXITCODE | Should -Be 0 - Get-Content -Raw -Path $testFile | Should -Be "HelloWorld" - } + $testFile = "$testdrive\test.txt" + 'test' | Set-Content -Path $testFile -Force + $r = '{"DestinationPath":"' + $testFile.replace('\', '\\') + '"}' | dsc resource get -r 'PSDesiredStateConfiguration/File' -f - + $LASTEXITCODE | Should -Be 0 + $res = $r | ConvertFrom-Json + $res.actualState.DestinationPath | Should -Be "$testFile" + } - It 'Get works on traditional "Script" resource' -Skip:(!$IsWindows){ + It 'Set works on Binary "File" resource' -Skip:(!$IsWindows) { - $testFile = "$testdrive\test.txt" - 'test' | Set-Content -Path $testFile -Force - $r = '{"GetScript": "@{result = $(Get-Content ' + $testFile.replace('\','\\') + ')}", "SetScript": "throw", "TestScript": "throw"}' | dsc resource get -r 'PSDesiredStateConfiguration/Script' -f - - $LASTEXITCODE | Should -Be 0 - $res = $r | ConvertFrom-Json - $res.actualState.result | Should -Be 'test' - } + $testFile = "$testdrive\test.txt" + $null = '{"DestinationPath":"' + $testFile.replace('\', '\\') + '", type: File, contents: HelloWorld, Ensure: present}' | dsc resource set -r 'PSDesiredStateConfiguration/File' -f - + $LASTEXITCODE | Should -Be 0 + Get-Content -Raw -Path $testFile | Should -Be "HelloWorld" + } - It 'Get works on config with File resource for WinPS' -Skip:(!$IsWindows){ + It 'Get works on traditional "Script" resource' -Skip:(!$IsWindows) { - $testFile = "$testdrive\test.txt" - 'test' | Set-Content -Path $testFile -Force - $r = (Get-Content -Raw $winpsConfigPath).Replace('c:\test.txt',"$testFile") | dsc config get -f - - $LASTEXITCODE | Should -Be 0 - $res = $r | ConvertFrom-Json - $res.results[0].result.actualState.result[0].properties.DestinationPath | Should -Be "$testFile" - } + $testFile = "$testdrive\test.txt" + 'test' | Set-Content -Path $testFile -Force + $r = '{"GetScript": "@{result = $(Get-Content ' + $testFile.replace('\', '\\') + ')}", "SetScript": "throw", "TestScript": "throw"}' | dsc resource get -r 'PSDesiredStateConfiguration/Script' -f - + $LASTEXITCODE | Should -Be 0 + $res = $r | ConvertFrom-Json + $res.actualState.result | Should -Be 'test' + } - It 'Verify that there are no cache rebuilds for several sequential executions' -Skip:(!$IsWindows) { - # remove cache file - $cacheFilePath = Join-Path $env:LocalAppData "dsc\WindowsPSAdapterCache.json" - Remove-Item -Force -Path $cacheFilePath -ErrorAction Ignore + It 'Get works on config with File resource for WinPS' -Skip:(!$IsWindows) { - # first execution should build the cache - dsc -l trace resource list -a Microsoft.Windows/WindowsPowerShell 2> $TestDrive/tracing.txt - "$TestDrive/tracing.txt" | Should -FileContentMatchExactly 'Constructing Get-DscResource cache' + $testFile = "$testdrive\test.txt" + 'test' | Set-Content -Path $testFile -Force + $r = (Get-Content -Raw $winpsConfigPath).Replace('c:\test.txt', "$testFile") | dsc config get -f - + $LASTEXITCODE | Should -Be 0 + $res = $r | ConvertFrom-Json + $res.results[0].result.actualState.result[0].properties.DestinationPath | Should -Be "$testFile" + } - # next executions following shortly after should Not rebuild the cache - 1..3 | ForEach-Object { - dsc -l trace resource list -a Microsoft.Windows/WindowsPowerShell 2> $TestDrive/tracing.txt - "$TestDrive/tracing.txt" | Should -Not -FileContentMatchExactly 'Constructing Get-DscResource cache' - } + It 'Verify that there are no cache rebuilds for several sequential executions' -Skip:(!$IsWindows) { + # remove cache file + $cacheFilePath = Join-Path $env:LocalAppData "dsc\WindowsPSAdapterCache.json" + Remove-Item -Force -Path $cacheFilePath -ErrorAction Ignore + + # first execution should build the cache + dsc -l trace resource list -a Microsoft.Windows/WindowsPowerShell 2> $TestDrive/tracing.txt + "$TestDrive/tracing.txt" | Should -FileContentMatchExactly 'Constructing Get-DscResource cache' + + # next executions following shortly after should Not rebuild the cache + 1..3 | ForEach-Object { + dsc -l trace resource list -a Microsoft.Windows/WindowsPowerShell 2> $TestDrive/tracing.txt + "$TestDrive/tracing.txt" | Should -Not -FileContentMatchExactly 'Constructing Get-DscResource cache' } + } - It 'Verify if assertion is used that no module is cleared in the cache' -Skip:(!$IsWindows) { - # create a test file in the test drive - $testFile = "$testdrive\test.txt" - New-Item -Path $testFile -ItemType File -Force | Out-Null + It 'Verify if assertion is used that no module is cleared in the cache' -Skip:(!$IsWindows) { + # create a test file in the test drive + $testFile = "$testdrive\test.txt" + New-Item -Path $testFile -ItemType File -Force | Out-Null - # remove cache file - $cacheFilePath = Join-Path $env:LocalAppData "dsc\WindowsPSAdapterCache.json" - Remove-Item -Force -Path $cacheFilePath -ErrorAction Ignore + # remove cache file + $cacheFilePath = Join-Path $env:LocalAppData "dsc\WindowsPSAdapterCache.json" + Remove-Item -Force -Path $cacheFilePath -ErrorAction Ignore - # build the cache - dsc resource list --adapter Microsoft.Windows/WindowsPowerShell | Out-Null + # build the cache + dsc resource list --adapter Microsoft.Windows/WindowsPowerShell | Out-Null - # Create a test module in the test drive - $testModuleDir = "$testdrive\TestModule\1.0.0" - New-Item -Path $testModuleDir -ItemType Directory -Force | Out-Null + # Create a test module in the test drive + $testModuleDir = "$testdrive\TestModule\1.0.0" + New-Item -Path $testModuleDir -ItemType Directory -Force | Out-Null - $manifestContent = @" + $manifestContent = @" @{ RootModule = 'TestModule.psm1' ModuleVersion = '1.0.0' @@ -120,17 +123,17 @@ Describe 'WindowsPowerShell adapter resource tests - requires elevated permissio AliasesToExport = @() } "@ - Set-Content -Path "$testModuleDir\TestModule.psd1" -Value $manifestContent + Set-Content -Path "$testModuleDir\TestModule.psd1" -Value $manifestContent - $scriptContent = @" + $scriptContent = @" Write-Host 'The DSC world!' "@ - Set-Content -Path "$testModuleDir\TestModule.psm1" -Value $scriptContent + Set-Content -Path "$testModuleDir\TestModule.psm1" -Value $scriptContent - # Add the test module directory to PSModulePath - $env:PSModulePath += [System.IO.Path]::PathSeparator + $testdrive + # Add the test module directory to PSModulePath + $env:PSModulePath += [System.IO.Path]::PathSeparator + $testdrive - $yaml = @" + $yaml = @" `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json resources: - name: File @@ -164,25 +167,25 @@ resources: - "[resourceId('Microsoft.Windows/WindowsPowerShell', 'File')]" - "[resourceId('Microsoft.DSC/Assertion', 'File present')]" "@ - # output to file for Windows PowerShell 5.1 - $filePath = "$testdrive\test.assertion.dsc.resource.yaml" - $yaml | Set-Content -Path $filePath -Force - dsc config test -f $filePath 2> "$TestDrive/error.txt" - $LASTEXITCODE | Should -Be 2 - - $cache = Get-Content -Path $cacheFilePath -Raw | ConvertFrom-Json - $cache.ResourceCache.Type | Should -Contain 'PSTestModule/TestPSRepository' - $cache.ResourceCache.Type | Should -Contain 'PSDesiredStateConfiguration/File' - } + # output to file for Windows PowerShell 5.1 + $filePath = "$testdrive\test.assertion.dsc.resource.yaml" + $yaml | Set-Content -Path $filePath -Force + dsc config test -f $filePath 2> "$TestDrive/error.txt" + $LASTEXITCODE | Should -Be 2 + + $cache = Get-Content -Path $cacheFilePath -Raw | ConvertFrom-Json + $cache.ResourceCache.Type | Should -Contain 'PSTestModule/TestPSRepository' + $cache.ResourceCache.Type | Should -Contain 'PSDesiredStateConfiguration/File' + } - It '_inDesiredState is returned correction: ' -Skip:(!$IsWindows) -TestCases @( - @{ Context = 'Both running'; FirstState = 'Running'; SecondState = 'Running' } - @{ Context = 'Both stopped'; FirstState = 'Stopped'; SecondState = 'Stopped' } - @{ Context = 'First Stopped'; FirstState = 'Stopped'; SecondState = 'Running' } - @{ Context = 'First Running'; FirstState = 'Running'; SecondState = 'Stopped' } - ) { - param($Context, $FirstState, $SecondState) - $yaml = @" + It '_inDesiredState is returned correction: ' -Skip:(!$IsWindows) -TestCases @( + @{ Context = 'Both running'; FirstState = 'Running'; SecondState = 'Running' } + @{ Context = 'Both stopped'; FirstState = 'Stopped'; SecondState = 'Stopped' } + @{ Context = 'First Stopped'; FirstState = 'Stopped'; SecondState = 'Running' } + @{ Context = 'First Running'; FirstState = 'Running'; SecondState = 'Stopped' } + ) { + param($Context, $FirstState, $SecondState) + $yaml = @" `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json resources: - name: Use Windows PowerShell resources @@ -201,14 +204,68 @@ resources: State: $SecondState "@ - $inDesiredState = if ($FirstState -eq $SecondState) { - $FirstState -eq (Get-Service Spooler).Status - } else { - $false - } + $inDesiredState = if ($FirstState -eq $SecondState) { + $FirstState -eq (Get-Service Spooler).Status + } + else { + $false + } + + $out = dsc config test -i $yaml | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 + $out.results[0].result.inDesiredState | Should -Be $inDesiredState + } - $out = dsc config test -i $yaml | ConvertFrom-Json - $LASTEXITCODE | Should -Be 0 - $out.results[0].result.inDesiredState | Should -Be $inDesiredState + It 'Config works with credential object' -Skip:(!$IsWindows) { + BeforeDiscovery { + $script:winPSModule = Resolve-Path -Path (Join-Path $PSScriptRoot '..' 'psDscAdapter' 'win_psDscAdapter.psm1') | Select-Object -ExpandProperty Path + Import-Module $winPSModule -Force -ErrorAction Stop + + # Mock the command to work on GitHub runners because Microsoft.PowerShell.Security is not available + Mock -CommandName ConvertTo-SecureString -MockWith { [System.Security.SecureString]::new() } } + + $jsonInput = @{ + resources = @{ + name = 'Service info' + type = 'PSDesiredStateConfiguration/Service' + properties = @{ + Name = 'Spooler' + Credential = @{ + UserName = 'User' + Password = 'Password' + } + } + } + } | ConvertTo-Json -Depth 10 + + # Instead of calling dsc.exe we call the cmdlet directly to be able to test the output and mocks + $resourceObject = Get-DscResourceObject -jsonInput $jsonInput + $cacheEntry = Invoke-DscCacheRefresh -Module PSDesiredStateConfiguration + + $out = Invoke-DscOperation -Operation Test -DesiredState $resourceObject -dscResourceCache $cacheEntry + $LASTEXITCODE | Should -Be 0 + $out.properties.InDesiredState.InDesiredState | Should -Be $false + + Should -Invoke -CommandName ConvertTo-SecureString -Exactly -Times 1 -Scope It + } + + It 'Config does not work when credential properties are missing required fields' -Skip:(!$IsWindows) { + $yaml = @" + `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + resources: + - name: Service info + type: PsDesiredStateConfiguration/Service + properties: + Name: Spooler + Credential: + UserName: 'User' + OtherProperty: 'Password' +"@ + # Compared to PowerShell we use test here as it filters out the properties + $out = dsc config test -i $yaml 2>&1 | Out-String + $LASTEXITCODE | Should -Be 2 + $out | Should -Not -BeNullOrEmpty + $out | Should -BeLike "*ERROR*Credential object 'Credential' requires both 'username' and 'password' properties*" + } } diff --git a/powershell-adapter/psDscAdapter/psDscAdapter.psm1 b/powershell-adapter/psDscAdapter/psDscAdapter.psm1 index 30c3500ea..52bb2a1fb 100644 --- a/powershell-adapter/psDscAdapter/psDscAdapter.psm1 +++ b/powershell-adapter/psDscAdapter/psDscAdapter.psm1 @@ -416,18 +416,23 @@ function Invoke-DscOperation { $ValidProperties = $cachedDscResourceInfo.Properties.Name + $ValidProperties | ConvertTo-Json | Write-DscTrace -Operation Trace + if ($DesiredState.properties) { # set each property of $dscResourceInstance to the value of the property in the $desiredState INPUT object $DesiredState.properties.psobject.properties | ForEach-Object -Process { # handle input objects by converting them to a hash table if ($_.Value -is [System.Management.Automation.PSCustomObject]) { - Write-DscTrace -Message "The object is a PSCustomObject" - $_.Value.psobject.properties | ForEach-Object -Begin { - $propertyHash = @{} - } -Process { - $propertyHash[$_.Name] = $_.Value - } -End { - $dscResourceInstance.$($_.Name) = $propertyHash + $validateProperty = $cachedDscResourceInfo.Properties | Where-Object -Property Name -EQ $_.Name + if ($validateProperty.PropertyType -eq 'PSCredential') { + if (-not $_.Value.Username -or -not $_.Value.Password) { + "Credential object '$($_.Name)' requires both 'username' and 'password' properties" | Write-DscTrace -Operation Error + exit 1 + } + $dscResourceInstance.$($_.Name) = [System.Management.Automation.PSCredential]::new($_.Value.Username, (ConvertTo-SecureString -AsPlainText $_.Value.Password -Force)) + } + else { + $dscResourceInstance.$($_.Name) = $_.Value.psobject.properties | ForEach-Object -Begin { $propertyHash = @{} } -Process { $propertyHash[$_.Name] = $_.Value } -End { $propertyHash } } } else { diff --git a/powershell-adapter/psDscAdapter/win_psDscAdapter.psm1 b/powershell-adapter/psDscAdapter/win_psDscAdapter.psm1 index f02605544..3ff1c6803 100644 --- a/powershell-adapter/psDscAdapter/win_psDscAdapter.psm1 +++ b/powershell-adapter/psDscAdapter/win_psDscAdapter.psm1 @@ -74,7 +74,7 @@ function Invoke-DscCacheRefresh { $cache = Get-Content -Raw $cacheFilePath | ConvertFrom-Json if ($cache.CacheSchemaVersion -ne $script:CurrentCacheSchemaVersion) { $refreshCache = $true - "Incompartible version of cache in file '" + $cache.CacheSchemaVersion + "' (expected '" + $script:CurrentCacheSchemaVersion + "')" | Write-DscTrace + "Incompatible version of cache in file '" + $cache.CacheSchemaVersion + "' (expected '" + $script:CurrentCacheSchemaVersion + "')" | Write-DscTrace } else { $dscResourceCacheEntries = $cache.ResourceCache @@ -281,14 +281,6 @@ function Get-DscResourceObject { $inputObj = $jsonInput | ConvertFrom-Json $desiredState = [System.Collections.Generic.List[Object]]::new() - # match adapter to version of powershell - if ($PSVersionTable.PSVersion.Major -le 5) { - $adapterName = 'Microsoft.Windows/WindowsPowerShell' - } - else { - $adapterName = 'Microsoft.DSC/PowerShell' - } - # change the type from pscustomobject to dscResourceObject $inputObj.resources | ForEach-Object -Process { $desiredState += [dscResourceObject]@{ @@ -364,7 +356,18 @@ function Invoke-DscOperation { # morph the INPUT object into a hashtable named "property" for the cmdlet Invoke-DscResource $DesiredState.properties.psobject.properties | ForEach-Object -Begin { $property = @{} } -Process { if ($_.Value -is [System.Management.Automation.PSCustomObject]) { - $property[$_.Name] = $_.Value.psobject.properties | ForEach-Object -Begin { $propertyHash = @{} } -Process { $propertyHash[$_.Name] = $_.Value } -End { $propertyHash } + $validateProperty = $cachedDscResourceInfo.Properties | Where-Object -Property Name -EQ $_.Name + $validateProperty | Write-DscTrace -Operation Debug + if ($validateProperty.PropertyType -eq '[PSCredential]') { + if (-not $_.Value.Username -or -not $_.Value.Password) { + "Credential object '$($_.Name)' requires both 'username' and 'password' properties" | Write-DscTrace -Operation Error + exit 1 + } + $property.$($_.Name) = [System.Management.Automation.PSCredential]::new($_.Value.Username, (ConvertTo-SecureString -AsPlainText $_.Value.Password -Force)) + } + else { + $property.$($_.Name) = $_.Value.psobject.properties | ForEach-Object -Begin { $propertyHash = @{} } -Process { $propertyHash[$_.Name] = $_.Value } -End { $propertyHash } + } } else { $property[$_.Name] = $_.Value @@ -373,7 +376,7 @@ function Invoke-DscOperation { # using the cmdlet the appropriate dsc module, and handle errors try { - Write-DscTrace -Operation Debug -Message "Module: $($cachedDscResourceInfo.ModuleName), Name: $($cachedDscResourceInfo.Name), Property: $($property)" + Write-DscTrace -Operation Debug -Message "Module: $($cachedDscResourceInfo.ModuleName), Name: $($cachedDscResourceInfo.Name), Property: $($property | ConvertTo-Json -Compress)" $invokeResult = Invoke-DscResource -Method $Operation -ModuleName $cachedDscResourceInfo.ModuleName -Name $cachedDscResourceInfo.Name -Property $property -ErrorAction Stop if ($invokeResult.GetType().Name -eq 'Hashtable') { @@ -402,7 +405,23 @@ function Invoke-DscOperation { if ($DesiredState.properties) { # set each property of $dscResourceInstance to the value of the property in the $desiredState INPUT object $DesiredState.properties.psobject.properties | ForEach-Object -Process { - $dscResourceInstance.$($_.Name) = $_.Value + # handle input objects by converting them to a hash table + if ($_.Value -is [System.Management.Automation.PSCustomObject]) { + $validateProperty = $cachedDscResourceInfo.Properties | Where-Object -Property Name -EQ $_.Name + if ($validateProperty.PropertyType -eq '[PSCredential]') { + if (-not $_.Value.Username -or -not $_.Value.Password) { + "Credential object '$($_.Name)' requires both 'username' and 'password' properties" | Write-DscTrace -Operation Error + exit 1 + } + $dscResourceInstance.$($_.Name) = [System.Management.Automation.PSCredential]::new($_.Value.Username, (ConvertTo-SecureString -AsPlainText $_.Value.Password -Force)) + } + else { + $dscResourceInstance.$($_.Name) = $_.Value.psobject.properties | ForEach-Object -Begin { $propertyHash = @{} } -Process { $propertyHash[$_.Name] = $_.Value } -End { $propertyHash } + } + } + else { + $dscResourceInstance.$($_.Name) = $_.Value + } } } @@ -444,9 +463,28 @@ function Invoke-DscOperation { } # morph the INPUT object into a hashtable named "property" for the cmdlet Invoke-DscResource - $DesiredState.properties.psobject.properties | ForEach-Object -Begin { $property = @{} } -Process { $property[$_.Name] = $_.Value } + $DesiredState.properties.psobject.properties | ForEach-Object -Begin { $property = @{} } -Process { + if ($_.Value -is [System.Management.Automation.PSCustomObject]) { + $validateProperty = $cachedDscResourceInfo.Properties | Where-Object -Property Name -EQ $_.Name + if ($validateProperty.PropertyType -eq '[PSCredential]') { + if (-not $_.Value.Username -or -not $_.Value.Password) { + "Credential object '$($_.Name)' requires both 'username' and 'password' properties" | Write-DscTrace -Operation Error + exit 1 + } + $property.$($_.Name) = [System.Management.Automation.PSCredential]::new($_.Value.Username, (ConvertTo-SecureString -AsPlainText $_.Value.Password -Force)) + } + else { + $property.$($_.Name) = $_.Value.psobject.properties | ForEach-Object -Begin { $propertyHash = @{} } -Process { $propertyHash[$_.Name] = $_.Value } -End { $propertyHash } + } + } + else { + $property[$_.Name] = $_.Value + } + } + # using the cmdlet from PSDesiredStateConfiguration module in Windows try { + Write-DscTrace -Operation Debug -Message "Module: $($cachedDscResourceInfo.ModuleName), Name: $($cachedDscResourceInfo.Name), Property: $($property | ConvertTo-Json -Compress)" $invokeResult = Invoke-DscResource -Method $Operation -ModuleName $cachedDscResourceInfo.ModuleName -Name $cachedDscResourceInfo.Name -Property $property if ($invokeResult.GetType().Name -eq 'Hashtable') { $invokeResult.keys | ForEach-Object -Begin { $ResultProperties = @{} } -Process { $ResultProperties[$_] = $invokeResult.$_ }