Skip to content

Add credential support on PowerShell adapters #758

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 32 commits into from
Apr 26, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
6b2062c
Add credential support on PowerShell adapters
Gijsreyn Apr 21, 2025
f8e8323
Test the test
Gijsreyn Apr 21, 2025
a7c289e
Fix up test
Gijsreyn Apr 21, 2025
ec95eed
Fix up test
Gijsreyn Apr 21, 2025
a80a87e
Add debug line
Gijsreyn Apr 21, 2025
3d1d120
Fix test
Gijsreyn Apr 21, 2025
9132a2a
Runner hides length
Gijsreyn Apr 21, 2025
fa91d5f
Debug Windows PowerShell
Gijsreyn Apr 21, 2025
426b7ef
Rewrite to validate property to PSCredential
Gijsreyn Apr 21, 2025
597a4e1
Equals name
Gijsreyn Apr 21, 2025
d46f132
Add missing tests
Gijsreyn Apr 25, 2025
a4b4a86
Test PowerShell
Gijsreyn Apr 25, 2025
8da5b0b
Add tracing level for Windows PowerShell
Gijsreyn Apr 25, 2025
7b7f7fd
Add credential support on PowerShell adapters
Gijsreyn Apr 21, 2025
27b81f9
Test the test
Gijsreyn Apr 21, 2025
3717af6
Fix up test
Gijsreyn Apr 21, 2025
c6a21e8
Fix up test
Gijsreyn Apr 21, 2025
2abd9dd
Add debug line
Gijsreyn Apr 21, 2025
fc4d712
Fix test
Gijsreyn Apr 21, 2025
666862a
Runner hides length
Gijsreyn Apr 21, 2025
eb06406
Debug Windows PowerShell
Gijsreyn Apr 21, 2025
985b5ca
Rewrite to validate property to PSCredential
Gijsreyn Apr 21, 2025
dfad8a0
Equals name
Gijsreyn Apr 21, 2025
d03480b
Add missing tests
Gijsreyn Apr 25, 2025
628bc7f
Test PowerShell
Gijsreyn Apr 25, 2025
1c69a6f
Add tracing level for Windows PowerShell
Gijsreyn Apr 25, 2025
e2e85fd
Fix failing test
Gijsreyn Apr 25, 2025
fbb3a94
Merge branch 'add-credential-support' of https://github.com/Gijsreyn/…
Gijsreyn Apr 25, 2025
23b4d92
Add mock example on Windows Powershell
Gijsreyn Apr 25, 2025
7b0a3cb
Forgot skip
Gijsreyn Apr 25, 2025
abb7f26
Remove leftover code from pr #720
Gijsreyn Apr 25, 2025
4041e8a
Update resolve path
Gijsreyn Apr 26, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ class TestClassResource : BaseTestClass
[DscProperty()]
[string] $EnumProp

[DscProperty()]
[PSCredential] $Credential

[string] $NonDscProperty # This property shouldn't be in results data

hidden
Expand Down
36 changes: 36 additions & 0 deletions powershell-adapter/Tests/powershellgroup.config.tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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*"
}
}
277 changes: 167 additions & 110 deletions powershell-adapter/Tests/win_powershellgroup.tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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
Expand Down Expand Up @@ -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: <Context>' -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: <Context>' -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
Expand All @@ -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*"
}
}
Loading
Loading