diff --git a/build.ps1 b/build.ps1 index 78f71004a..fe49fbf9e 100755 --- a/build.ps1 +++ b/build.ps1 @@ -60,6 +60,8 @@ $filesForWindowsPackage = @( 'assertion.dsc.resource.json', 'group.dsc.resource.json', 'include.dsc.resource.json', + 'microsoft.powershell.dsc.extension.json', + 'microsoft.powershell.secret.ps1', 'NOTICE.txt', 'osinfo.exe', 'osinfo.dsc.resource.json', @@ -98,6 +100,8 @@ $filesForLinuxPackage = @( 'apt.dsc.resource.sh', 'group.dsc.resource.json', 'include.dsc.resource.json', + 'microsoft.powershell.dsc.extension.json', + 'microsoft.powershell.secret.ps1', 'NOTICE.txt', 'osinfo', 'osinfo.dsc.resource.json', @@ -123,6 +127,8 @@ $filesForMacPackage = @( 'brew.dsc.resource.sh', 'group.dsc.resource.json', 'include.dsc.resource.json', + 'microsoft.powershell.dsc.extension.json', + 'microsoft.powershell.secret.ps1', 'NOTICE.txt', 'osinfo', 'osinfo.dsc.resource.json', @@ -331,9 +337,9 @@ if (!$SkipBuild) { New-Item -ItemType Directory $target -ErrorAction Ignore > $null # make sure dependencies are built first so clippy runs correctly - $windows_projects = @("pal", "registry_lib", "registry", "reboot_pending", "wmi-adapter", "configurations/windows", 'extensions/appx') - $macOS_projects = @("resources/brew") - $linux_projects = @("resources/apt") + $windows_projects = @("pal", "registry_lib", "registry", "reboot_pending", "wmi-adapter", "configurations/windows", "extensions/appx", "extensions/powershell/secret") + $macOS_projects = @("resources/brew", "extensions/powershell/secret") + $linux_projects = @("resources/apt", "extensions/powershell/secret") # projects are in dependency order $projects = @( diff --git a/dsc/tests/dsc_extension_discover.tests.ps1 b/dsc/tests/dsc_extension_discover.tests.ps1 index d37455465..6c645c921 100644 --- a/dsc/tests/dsc_extension_discover.tests.ps1 +++ b/dsc/tests/dsc_extension_discover.tests.ps1 @@ -23,30 +23,30 @@ Describe 'Discover extension tests' { It 'Discover extensions' { $out = dsc extension list | ConvertFrom-Json $LASTEXITCODE | Should -Be 0 - if ($IsWindows) { - $out.Count | Should -Be 3 -Because ($out | Out-String) - $out[0].type | Should -Be 'Microsoft.DSC.Extension/Bicep' - $out[0].version | Should -Be '0.1.0' - $out[0].capabilities | Should -BeExactly @('import') - $out[0].manifest | Should -Not -BeNullOrEmpty - $out[1].type | Should -Be 'Microsoft.Windows.Appx/Discover' - $out[1].version | Should -Be '0.1.0' - $out[1].capabilities | Should -BeExactly @('discover') - $out[1].manifest | Should -Not -BeNullOrEmpty - $out[2].type | Should -BeExactly 'Test/Discover' - $out[2].version | Should -BeExactly '0.1.0' - $out[2].capabilities | Should -BeExactly @('discover') - $out[2].manifest | Should -Not -BeNullOrEmpty + $expectedExtensions = if ($IsWindows) { + @( + @{ type = 'Microsoft.DSC.Extension/Bicep'; version = '0.1.0'; capabilities = @('import') } + @{ type = 'Microsoft.DSC.Transitional/PSDesiredStateConfiguration'; version = '0.1.0'; capabilities = @('import') } + @{ type = 'Microsoft.Windows.Appx/Discover'; version = '0.1.0'; capabilities = @('discover') } + @{ type = 'Microsoft.PowerShell/SecretManagement'; version = '0.1.0'; capabilities = @('secret') } + @{ type = 'Test/Discover'; version = '0.1.0'; capabilities = @('discover') } + ) } else { - $out.Count | Should -Be 2 -Because ($out | Out-String) - $out[0].type | Should -Be 'Microsoft.DSC.Extension/Bicep' - $out[0].version | Should -Be '0.1.0' - $out[0].capabilities | Should -BeExactly @('import') - $out[0].manifest | Should -Not -BeNullOrEmpty - $out[1].type | Should -BeExactly 'Test/Discover' - $out[1].version | Should -BeExactly '0.1.0' - $out[1].capabilities | Should -BeExactly @('discover') - $out[1].manifest | Should -Not -BeNullOrEmpty + @( + @{ type = 'Microsoft.DSC.Extension/Bicep'; version = '0.1.0'; capabilities = @('import') } + @{ type = 'Microsoft.PowerShell/SecretManagement'; version = '0.1.0'; capabilities = @('secret') } + @{ type = 'Test/Discover'; version = '0.1.0'; capabilities = @('discover') } + ) + } + + $out.Count | Should -Be $expectedExtensions.Count -Because ($out | Out-String) + + foreach ($expected in $expectedExtensions) { + $extension = $out | Where-Object { $_.type -eq $expected.type } + $extension | Should -Not -BeNullOrEmpty -Because "Extension $($expected.type) should exist" + $extension.version | Should -BeExactly $expected.version + $extension.capabilities | Should -BeExactly $expected.capabilities + $extension.manifest | Should -Not -BeNullOrEmpty } } diff --git a/extensions/powershell/secret/copy_files.txt b/extensions/powershell/secret/copy_files.txt new file mode 100644 index 000000000..b9e7ea40d --- /dev/null +++ b/extensions/powershell/secret/copy_files.txt @@ -0,0 +1,2 @@ +microsoft.powershell.secret.ps1 +microsoft.powershell.dsc.extension.json diff --git a/extensions/powershell/secret/microsoft.powershell.dsc.extension.json b/extensions/powershell/secret/microsoft.powershell.dsc.extension.json new file mode 100644 index 000000000..4c81021da --- /dev/null +++ b/extensions/powershell/secret/microsoft.powershell.dsc.extension.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://aka.ms/dsc/schemas/v3/bundled/extension/manifest.json", + "type": "Microsoft.PowerShell/SecretManagement", + "version": "0.1.0", + "description": "Retrieve secrets using the Microsoft.PowerShell.SecretManagement module", + "secret": { + "executable": "pwsh", + "args": [ + "-NoLogo", + "-NonInteractive", + "-NoProfile", + "-Command", + "./microsoft.powershell.secret.ps1", + { + "nameArg": "-Name" + }, + { + "vaultArg": "-Vault" + } + ] + } +} diff --git a/extensions/powershell/secret/microsoft.powershell.secret.ps1 b/extensions/powershell/secret/microsoft.powershell.secret.ps1 new file mode 100644 index 000000000..928b47b0c --- /dev/null +++ b/extensions/powershell/secret/microsoft.powershell.secret.ps1 @@ -0,0 +1,25 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)] + [string]$Name, + [Parameter()] + [string]$Vault +) + +if (Get-Command Get-Secret -ErrorAction Ignore) { + $secretParams = @{ + Name = $Name + AsPlainText = $true + } + + if (-not ([string]::IsNullOrEmpty($Vault))) { + $secretParams['Vault'] = $Vault + } + + $secret = Get-Secret @secretParams -ErrorAction Ignore + + Write-Output $secret +} \ No newline at end of file diff --git a/extensions/powershell/secret/microsoft.powershell.secret.tests.ps1 b/extensions/powershell/secret/microsoft.powershell.secret.tests.ps1 new file mode 100644 index 000000000..285eaf123 --- /dev/null +++ b/extensions/powershell/secret/microsoft.powershell.secret.tests.ps1 @@ -0,0 +1,41 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +BeforeDiscovery { + $runningInCI = $false +} + +BeforeAll { + $FullyQualifiedName = @() + $FullyQualifiedName += @{ModuleName="Microsoft.PowerShell.SecretManagement";ModuleVersion="1.1.2"} + $FullyQualifiedName += @{ModuleName="Microsoft.PowerShell.SecretStore";ModuleVersion="1.0.6"} + foreach ($module in $FullyQualifiedName) { + if (-not (Get-Module -ListAvailable -FullyQualifiedName $module)) { + Save-PSResource -Name $module.ModuleName -Version $module.ModuleVersion -Path $TestDrive -Repository PSGallery -TrustRepository + } + } + + $env:PSModulePath += [System.IO.Path]::PathSeparator + $TestDrive +} + +Describe 'Tests for PowerShell Secret Management' -Skip:($runningInCI) { + It 'Should get secret from default store' { + # Instead of doing it in the BeforeAll block, reset the store here as we know we are running in the CI + Reset-SecretStore -Password (ConvertTo-SecureString -AsPlainText -String 'P@ssw0rd' -Force) -Force + Register-SecretVault -Name 'VaultA' -ModuleName 'Microsoft.PowerShell.SecretStore' -DefaultVault + Set-Secret -Name TestSecret -Secret "Super@SecretPassword" + + $configYaml = @' + $schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + resources: + - name: Echo + type: Microsoft.DSC.Debug/Echo + properties: + output: "[secret('TestSecret')]" +'@ + $out = dsc -l trace config get -i $configYaml 2> $TestDrive/error.log | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 -Because (Get-Content -Raw -Path $TestDrive/error.log) + $out.results.Count | Should -Be 1 + $out.results[0].result.actualState.Output | Should -BeExactly 'Super@SecretPassword' + } +} \ No newline at end of file