Compare commits

...

9 Commits

Author SHA1 Message Date
Torsten Brendgen
4b67c74ac0 Enhance configuration data handling with new reference resolution functions, dummy secret support, and update module version to 1.1.0 2026-07-07 21:50:50 +02:00
Torsten Brendgen
45e71c093c Refactor secret management providers and introduce unified handling
- Removed KeePass and SecretStore provider implementations.
- Integrated KeePass and SecretStore as vaults under the SecretManagement provider.
- Added new functions: Get-DSCConfigurationDataCredentialProvider and Set-DSCConfigurationDataCredentialProvider for managing credential providers.
- Implemented Unlock-ConfigurationDataSecretManagementVault to handle vault unlocking with master passwords.
- Updated README to reflect changes in provider usage and examples.
- Enhanced error handling and validation for vault registration and settings.
2026-07-06 22:50:39 +02:00
Torsten Brendgen
3fa52d7ada Remove obsolete test file for credential providers 2026-07-03 12:52:45 +02:00
Torsten Brendgen
4b7ebe59ef Bump module version to 1.0.1 in the module manifest 2026-07-03 11:59:16 +02:00
Torsten Brendgen
943894b8fe Add Azure Key Vault support and enhance credential handling in configuration data 2026-07-03 10:34:25 +02:00
Torsten Brendgen
9b36693444 Add support for SecretManagement and SecretStore providers, enhance configuration data handling, and introduce new utility functions 2026-07-02 23:57:14 +02:00
Torsten Brendgen
115b8be385 Add secret provider functionality and enhance configuration data resolution 2026-07-02 14:36:04 +02:00
Torsten Brendgen
1cd225c7a0 Enhance configuration data validation and add new utility functions
- Added validation for empty values in Assert-ConfigurationDataParameter.
- Introduced Assert-ConfigurationDataSecretReference for secret reference validation.
- Implemented Test-ConfigurationDataExpressionParentheses to validate expression syntax.
- Added Test-ConfigurationDataMissingReferenceError to check for missing parameter or variable definitions.
- Updated Invoke-ConfigurationDataExpression to handle new validation logic.
- Revised Readme.md to reflect changes in secret reference handling.
2026-07-01 20:34:59 +02:00
88b842c8e9 .tests/Resolve-DSCConfigurationData.Tests.ps1 gelöscht 2026-06-27 12:30:18 +00:00
38 changed files with 1970 additions and 523 deletions

View File

@@ -1,512 +0,0 @@
$script:ModuleRoot = Split-Path -Path $PSScriptRoot -Parent
Import-Module (Join-Path -Path $script:ModuleRoot -ChildPath "Resolve-DSCConfigurationData.psd1") -Force
Describe "Resolve-DSCConfigurationData" {
It "resolves parameters using Value before DefaultValue" {
$ConfigurationData = @{
Parameters = @{
Name = @{
DefaultValue = "Default"
Value = "Deployment"
}
}
Resources = @{
Value = "[parameters('Name')]"
}
}
$Result = Resolve-DSCConfigurationData -ConfigurationData $ConfigurationData
$Result.Resources.Value | Should Be "Deployment"
}
It "allows parameter values listed in AllowedValues" {
$ConfigurationData = @{
Parameters = @{
Stage = @{
Value = "Test"
AllowedValues = @(
"Install",
"Test",
"Release"
)
}
}
Resources = @{
Stage = "[parameters('Stage')]"
}
}
$Result = Resolve-DSCConfigurationData -ConfigurationData $ConfigurationData
$Result.Resources.Stage | Should Be "Test"
}
It "validates DefaultValue against AllowedValues when Value is not set" {
$ConfigurationData = @{
Parameters = @{
Stage = @{
DefaultValue = "Release"
AllowedValues = @(
"Install",
"Test",
"Release"
)
}
}
Resources = @{
Stage = "[parameters('Stage')]"
}
}
$Result = Resolve-DSCConfigurationData -ConfigurationData $ConfigurationData
$Result.Resources.Stage | Should Be "Release"
}
It "throws when a parameter value is not listed in AllowedValues" {
$ConfigurationData = @{
Parameters = @{
Stage = @{
Value = "Production"
AllowedValues = @(
"Install",
"Test",
"Release"
)
}
}
Resources = @{
Stage = "[parameters('Stage')]"
}
}
{ Resolve-DSCConfigurationData -ConfigurationData $ConfigurationData } | Should Throw
}
It "validates array parameter items against AllowedValues" {
$ConfigurationData = @{
Parameters = @{
ServerRoles = @{
Type = "array"
Value = @(
"WebFrontEnd",
"Application"
)
AllowedValues = @(
"WebFrontEnd",
"Application",
"Search"
)
}
}
Resources = @{
Roles = "[parameters('ServerRoles')]"
}
}
$Result = Resolve-DSCConfigurationData -ConfigurationData $ConfigurationData
$Result.Resources.Roles[0] | Should Be "WebFrontEnd"
$Result.Resources.Roles[1] | Should Be "Application"
}
It "throws when an array parameter item is not listed in AllowedValues" {
$ConfigurationData = @{
Parameters = @{
ServerRoles = @{
Type = "array"
Value = @(
"WebFrontEnd",
"InvalidRole"
)
AllowedValues = @(
"WebFrontEnd",
"Application",
"Search"
)
}
}
}
{ Resolve-DSCConfigurationData -ConfigurationData $ConfigurationData } | Should Throw
}
It "validates required parameters" {
$ConfigurationData = @{
Parameters = @{
DatabasePrefix = @{
Type = "string"
Required = $true
}
}
}
{ Resolve-DSCConfigurationData -ConfigurationData $ConfigurationData } | Should Throw
}
It "validates parameter length constraints" {
$ConfigurationData = @{
Parameters = @{
DatabasePrefix = @{
Value = "S"
MinLength = 2
MaxLength = 10
}
}
}
{ Resolve-DSCConfigurationData -ConfigurationData $ConfigurationData } | Should Throw
}
It "validates parameter patterns" {
$ConfigurationData = @{
Parameters = @{
DatabasePrefix = @{
Value = "Share Point"
Pattern = "^[A-Za-z][A-Za-z0-9_-]*$"
}
}
}
{ Resolve-DSCConfigurationData -ConfigurationData $ConfigurationData } | Should Throw
}
It "validates supported parameter types" {
$ConfigurationData = @{
Parameters = @{
Name = @{
Type = "string"
Value = "SharePoint"
}
Port = @{
Type = "int"
Value = 1433
}
Enabled = @{
Type = "bool"
Value = $true
}
Roles = @{
Type = "array"
Value = @(
"WebFrontEnd",
"Application"
)
}
SecretReference = @{
Type = "secureString"
Value = @{
SecretName = "SharePointFarmPassphrase"
}
Sensitive = $true
}
CredentialReference = @{
Type = "credential"
Value = @{
CredentialName = "SharePointSetup"
}
Sensitive = $true
}
}
Resources = @{
Name = "[parameters('Name')]"
Port = "[parameters('Port')]"
Enabled = "[parameters('Enabled')]"
RoleCount = "[length(parameters('Roles'))]"
}
}
$Result = Resolve-DSCConfigurationData -ConfigurationData $ConfigurationData
$Result.Resources.Name | Should Be "SharePoint"
$Result.Resources.Port | Should Be 1433
$Result.Resources.Enabled | Should Be $true
$Result.Resources.RoleCount | Should Be 2
}
It "throws when parameter type validation fails" {
$ConfigurationData = @{
Parameters = @{
Port = @{
Type = "int"
Value = "1433"
}
}
}
{ Resolve-DSCConfigurationData -ConfigurationData $ConfigurationData } | Should Throw
}
It "validates numeric ranges" {
$ConfigurationData = @{
Parameters = @{
Port = @{
Type = "int"
Value = 1433
MinValue = 1
MaxValue = 65535
}
}
Resources = @{
Port = "[parameters('Port')]"
}
}
$Result = Resolve-DSCConfigurationData -ConfigurationData $ConfigurationData
$Result.Resources.Port | Should Be 1433
}
It "throws when numeric range validation fails" {
$ConfigurationData = @{
Parameters = @{
Port = @{
Type = "int"
Value = 70000
MinValue = 1
MaxValue = 65535
}
}
}
{ Resolve-DSCConfigurationData -ConfigurationData $ConfigurationData } | Should Throw
}
It "supports localized deprecated parameter messages" {
$ConfigurationData = @{
Parameters = @{
OldDatabasePrefix = @{
Value = "SharePoint"
Deprecated = @{
Message = @{
"de-DE" = "Der Parameter [OldDatabasePrefix] ist veraltet. Verwende [DatabasePrefix]."
"en-US" = "Parameter [OldDatabasePrefix] is deprecated. Use [DatabasePrefix]."
}
}
}
}
Resources = @{
Value = "[parameters('OldDatabasePrefix')]"
}
}
$Warnings = @()
$Result = Resolve-DSCConfigurationData -ConfigurationData $ConfigurationData -WarningVariable Warnings 3>$null
$Result.Resources.Value | Should Be "SharePoint"
$Warnings.Count | Should Be 1
([string]$Warnings[0]) | Should Match "OldDatabasePrefix"
}
It "resolves variables that reference parameters" {
$ConfigurationData = @{
Parameters = @{
Prefix = @{
Value = "SharePoint"
}
}
Variables = @{
ConfigDbName = "[concat(parameters('Prefix'), '_Farm_Config')]"
}
Resources = @{
DatabaseName = "[variables('ConfigDbName')]"
}
}
$Result = Resolve-DSCConfigurationData -ConfigurationData $ConfigurationData
$Result.Variables.ConfigDbName | Should Be "SharePoint_Farm_Config"
$Result.Resources.DatabaseName | Should Be "SharePoint_Farm_Config"
}
It "resolves nested functions" {
$ConfigurationData = @{
Parameters = @{
Prefix = @{
Value = "sharepoint"
}
}
Resources = @{
Name = "[concat(toUpper(parameters('Prefix')), '_Services')]"
}
}
$Result = Resolve-DSCConfigurationData -ConfigurationData $ConfigurationData
$Result.Resources.Name | Should Be "SHAREPOINT_Services"
}
It "resolves string helper functions" {
$ConfigurationData = @{
Resources = @{
Lower = "[toLower('BGW-LAN')]"
Upper = "[toUpper('bgw-lan')]"
First = "[firstIndexOf('a', 'b', 'c')]"
Last = "[lastIndexOf('a', 'b', 'c')]"
Index = "[indexOf('BGW', 1)]"
Substring = "[substring('SharePoint', 5, 5)]"
Replace = "[replace('BGW-LAN', '-', '_')]"
}
}
$Result = Resolve-DSCConfigurationData -ConfigurationData $ConfigurationData
$Result.Resources.Lower | Should Be "bgw-lan"
$Result.Resources.Upper | Should Be "BGW-LAN"
$Result.Resources.First | Should Be "a"
$Result.Resources.Last | Should Be "c"
$Result.Resources.Index | Should Be "G"
$Result.Resources.Substring | Should Be "Point"
$Result.Resources.Replace | Should Be "BGW_LAN"
}
It "resolves formatting, fallback, and conditional functions" {
$ConfigurationData = @{
Parameters = @{
DatabasePrefix = @{
Value = "SharePoint"
}
DomainLabel = @{
Value = "LAN"
}
Stage = @{
Value = "Test"
}
OptionalValue = @{
Value = ""
}
}
Resources = @{
Formatted = "[format('{0}_{1}_{2}', parameters('DatabasePrefix'), parameters('DomainLabel'), parameters('Stage'))]"
Fallback = "[coalesce(parameters('OptionalValue'), 'DefaultValue')]"
Conditional = "[if(equals(parameters('Stage'), 'Test'), 'TST', 'PRD')]"
NotEquals = "[notEquals(parameters('Stage'), 'Prod')]"
AndValue = "[and(equals(parameters('Stage'), 'Test'), not(empty(parameters('DatabasePrefix'))))]"
OrValue = "[or(equals(parameters('Stage'), 'Prod'), equals(parameters('Stage'), 'Test'))]"
}
}
$Result = Resolve-DSCConfigurationData -ConfigurationData $ConfigurationData
$Result.Resources.Formatted | Should Be "SharePoint_LAN_Test"
$Result.Resources.Fallback | Should Be "DefaultValue"
$Result.Resources.Conditional | Should Be "TST"
$Result.Resources.NotEquals | Should Be $true
$Result.Resources.AndValue | Should Be $true
$Result.Resources.OrValue | Should Be $true
}
It "resolves collection and string inspection functions" {
$ConfigurationData = @{
Parameters = @{
DomainFQDN = @{
Value = "bgw-online.de"
}
}
Resources = @{
Split = "[split(parameters('DomainFQDN'), '.')]"
Joined = "[join(split(parameters('DomainFQDN'), '.'), '_')]"
JoinedNotEmpty = "[joinNotEmpty('_', 'SharePoint', '', 'LAN', 'Test', '', 'Services')]"
Take = "[join(take(split(parameters('DomainFQDN'), '.'), 1), '_')]"
Skip = "[join(skip(split(parameters('DomainFQDN'), '.'), 1), '_')]"
First = "[first(split(parameters('DomainFQDN'), '.'))]"
Last = "[last(split(parameters('DomainFQDN'), '.'))]"
Unique = "[join(unique(split('SP.SP.SQL', '.')), '_')]"
Sort = "[join(sort(split('SQL.SP.APP', '.')), '_')]"
ContainsString = "[contains(parameters('DomainFQDN'), 'online')]"
ContainsArray = "[contains(split(parameters('DomainFQDN'), '.'), 'de')]"
StartsWith = "[startsWith(parameters('DomainFQDN'), 'bgw')]"
EndsWith = "[endsWith(parameters('DomainFQDN'), 'DE')]"
Length = "[length(split(parameters('DomainFQDN'), '.'))]"
Empty = "[empty('')]"
}
}
$Result = Resolve-DSCConfigurationData -ConfigurationData $ConfigurationData
$Result.Resources.Split[0] | Should Be "bgw-online"
$Result.Resources.Split[1] | Should Be "de"
$Result.Resources.Joined | Should Be "bgw-online_de"
$Result.Resources.JoinedNotEmpty | Should Be "SharePoint_LAN_Test_Services"
$Result.Resources.Take | Should Be "bgw-online"
$Result.Resources.Skip | Should Be "de"
$Result.Resources.First | Should Be "bgw-online"
$Result.Resources.Last | Should Be "de"
$Result.Resources.Unique | Should Be "SP_SQL"
$Result.Resources.Sort | Should Be "APP_SP_SQL"
$Result.Resources.ContainsString | Should Be $true
$Result.Resources.ContainsArray | Should Be $true
$Result.Resources.StartsWith | Should Be $true
$Result.Resources.EndsWith | Should Be $true
$Result.Resources.Length | Should Be 2
$Result.Resources.Empty | Should Be $true
}
It "resolves trim and padding functions" {
$ConfigurationData = @{
Resources = @{
Trim = "[trim(' SharePoint ')]"
TrimStart = "[trimStart(' SharePoint')]"
TrimEnd = "[trimEnd('SharePoint ')]"
PadLeft = "[padLeft('1', 2, '0')]"
PadRight = "[padRight('SP', 4, '0')]"
DefaultIfEmpty = "[defaultIfEmpty('', 'SharePoint')]"
Sanitized = "[sanitizeName(' SharePoint LAN/Test DB ')]"
Normalized = "[normalizeSeparator('__SharePoint___LAN_Test__', '_')]"
Prefix = "[prefixIfNotEmpty('LAN', 'BGW-')]"
EmptyPrefix = "[prefixIfNotEmpty('', 'BGW-')]"
Suffix = "[suffixIfNotEmpty('SharePoint', '_DB')]"
EmptySuffix = "[suffixIfNotEmpty('', '_DB')]"
}
}
$Result = Resolve-DSCConfigurationData -ConfigurationData $ConfigurationData
$Result.Resources.Trim | Should Be "SharePoint"
$Result.Resources.TrimStart | Should Be "SharePoint"
$Result.Resources.TrimEnd | Should Be "SharePoint"
$Result.Resources.PadLeft | Should Be "01"
$Result.Resources.PadRight | Should Be "SP00"
$Result.Resources.DefaultIfEmpty | Should Be "SharePoint"
$Result.Resources.Sanitized | Should Be "SharePoint_LAN_Test_DB"
$Result.Resources.Normalized | Should Be "SharePoint_LAN_Test"
$Result.Resources.Prefix | Should Be "BGW-LAN"
$Result.Resources.EmptyPrefix | Should Be ""
$Result.Resources.Suffix | Should Be "SharePoint_DB"
$Result.Resources.EmptySuffix | Should Be ""
}
It "resolves values inside arrays recursively" {
$ConfigurationData = @{
Parameters = @{
NodeName = @{
Value = "SP01"
}
}
AllNodes = @(
@{
NodeName = "[parameters('NodeName')]"
}
)
}
$Result = Resolve-DSCConfigurationData -ConfigurationData $ConfigurationData
$Result.AllNodes[0].NodeName | Should Be "SP01"
}
It "throws for circular variables" {
$ConfigurationData = @{
Variables = @{
A = "[variables('B')]"
B = "[variables('A')]"
}
Resources = @{
Value = "[variables('A')]"
}
}
{ Resolve-DSCConfigurationData -ConfigurationData $ConfigurationData } | Should Throw
}
}

View File

@@ -42,6 +42,10 @@ function Assert-ConfigurationDataParameter {
return
}
if((Test-ConfigurationDataValueIsEmpty -Value $Value) -and (-not $Required)){
return
}
Assert-ConfigurationDataParameterType -Name $Name -Definition $Definition -Value $Value
Assert-ConfigurationDataParameterAllowedValue -Name $Name -Definition $Definition -Value $Value

View File

@@ -20,6 +20,11 @@ function Assert-ConfigurationDataParameterType {
switch($TypeName){
"string" {
if(Test-ConfigurationDataSecretReference -Value $Value){
Assert-ConfigurationDataSecretReference -Name $Name -TypeName "string" -Value $Value
return
}
if($Value -isnot [string]){
throw "Parameter [$Name] expects type [string], but received [$($Value.GetType().Name)]."
}
@@ -48,11 +53,15 @@ function Assert-ConfigurationDataParameterType {
if(($Value -isnot [string]) -and ($Value -isnot [System.Security.SecureString]) -and (-not (Test-ConfigurationDataMap -Value $Value))){
throw "Parameter [$Name] expects type [secureString], but received [$($Value.GetType().Name)]."
}
Assert-ConfigurationDataSecretReference -Name $Name -TypeName "securestring" -Value $Value
}
"credential" {
if(($Value -isnot [System.Management.Automation.PSCredential]) -and (-not (Test-ConfigurationDataMap -Value $Value))){
throw "Parameter [$Name] expects type [credential], but received [$($Value.GetType().Name)]."
}
Assert-ConfigurationDataSecretReference -Name $Name -TypeName "credential" -Value $Value
}
default {
throw "Parameter [$Name] uses unsupported type [$TypeName]."

View File

@@ -0,0 +1,47 @@
function Assert-ConfigurationDataSecretReference {
[CmdletBinding()]
Param(
[Parameter(Mandatory=$true)]
[string]
$Name,
[Parameter(Mandatory=$true)]
[ValidateSet("credential", "securestring", "string")]
[string]
$TypeName,
[Parameter(Mandatory=$true)]
$Value
)
if(-not (Test-ConfigurationDataMap -Value $Value)){
return
}
if(-not (Test-ConfigurationDataMapContainsKey -Map $Value -Key "Provider")){
throw "Parameter [$Name] uses type [$TypeName] and a secret reference, but [Provider] is not defined."
}
if(-not (Test-ConfigurationDataMapContainsKey -Map $Value -Key "Name")){
throw "Parameter [$Name] uses type [$TypeName] and a secret reference, but [Name] is not defined."
}
$Provider = [string](Get-ConfigurationDataMapValue -Map $Value -Key "Provider")
if([string]::IsNullOrWhiteSpace($Provider)){
throw "Parameter [$Name] secret reference [Provider] must not be empty."
}
$SecretName = [string](Get-ConfigurationDataMapValue -Map $Value -Key "Name")
if([string]::IsNullOrWhiteSpace($SecretName)){
throw "Parameter [$Name] secret reference [Name] must not be empty."
}
$ProviderDefinition = Get-ConfigurationDataSecretProvider -Name $Provider
if($null -eq $ProviderDefinition){
throw "Parameter [$Name] uses unsupported secret provider [$Provider]."
}
if($ProviderDefinition.SupportedTypes -notcontains $TypeName.ToLowerInvariant()){
throw "Parameter [$Name] uses provider [$Provider], but it does not support type [$TypeName]."
}
}

View File

@@ -0,0 +1,101 @@
function ConvertFrom-ConfigurationDataSecretValue {
[CmdletBinding()]
Param(
[Parameter(Mandatory=$true)]
[AllowNull()]
$Secret,
[Parameter(Mandatory=$true)]
[ValidateSet("credential", "securestring", "string")]
[string]
$ExpectedType,
[Parameter(Mandatory=$false)]
[string]
$Name = "",
[Parameter(Mandatory=$false)]
[string]
$UserName = ""
)
if($null -eq $Secret){
throw "Secret [$Name] was not found."
}
if($ExpectedType -eq "credential"){
if($Secret -is [System.Management.Automation.PSCredential]){
return $Secret
}
if($Secret -is [System.Collections.IDictionary]){
if([string]::IsNullOrWhiteSpace($UserName) -and $Secret.Contains("UserName")){
$UserName = [string]$Secret["UserName"]
}
if($Secret.Contains("Password")){
return ConvertTo-ConfigurationDataCredential -UserName $UserName -Password $Secret["Password"]
}
}
if($Secret.PSObject.Properties["UserName"] -and [string]::IsNullOrWhiteSpace($UserName)){
$UserName = [string]$Secret.UserName
}
if($Secret.PSObject.Properties["Password"]){
return ConvertTo-ConfigurationDataCredential -UserName $UserName -Password $Secret.Password
}
if([string]::IsNullOrWhiteSpace($UserName)){
throw "Secret [$Name] cannot be converted to [PSCredential] because no username was provided."
}
return ConvertTo-ConfigurationDataCredential -UserName $UserName -Password $Secret
}
if($ExpectedType -eq "securestring"){
if($Secret -is [System.Security.SecureString]){
return $Secret
}
if($Secret -is [System.Management.Automation.PSCredential]){
return $Secret.Password
}
if($Secret -is [System.Collections.IDictionary] -and $Secret.Contains("Password")){
$Secret = $Secret["Password"]
}elseif($Secret.PSObject.Properties["Password"]){
$Secret = $Secret.Password
}
if($Secret -is [System.Security.SecureString]){
return $Secret
}
return ConvertTo-SecureString -String ([string]$Secret) -AsPlainText -Force
}
if($Secret -is [System.Management.Automation.PSCredential]){
return $Secret.GetNetworkCredential().Password
}
if($Secret -is [System.Security.SecureString]){
$Pointer = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($Secret)
try {
return [Runtime.InteropServices.Marshal]::PtrToStringBSTR($Pointer)
}
finally {
[Runtime.InteropServices.Marshal]::ZeroFreeBSTR($Pointer)
}
}
if($Secret -is [System.Collections.IDictionary] -and $Secret.Contains("Password")){
return [string]$Secret["Password"]
}
if($Secret.PSObject.Properties["Password"]){
return [string]$Secret.Password
}
return [string]$Secret
}

View File

@@ -0,0 +1,19 @@
function ConvertTo-ConfigurationDataCredential {
[CmdletBinding()]
Param(
[Parameter(Mandatory=$true)]
[string]
$UserName,
[Parameter(Mandatory=$true)]
$Password
)
if($Password -is [System.Security.SecureString]){
$SecurePassword = $Password
}else{
$SecurePassword = ConvertTo-SecureString -String ([string]$Password) -AsPlainText -Force
}
return [System.Management.Automation.PSCredential]::new($UserName, $SecurePassword)
}

View File

@@ -0,0 +1,63 @@
function ConvertTo-PowerShellDataFileText {
[CmdletBinding()]
Param(
[Parameter(Mandatory=$true)]
[AllowNull()]
$InputObject,
[Parameter(Mandatory=$false)]
[int]
$Indent = 0
)
$IndentText = " " * $Indent
$ChildIndent = $Indent + 4
$ChildIndentText = " " * $ChildIndent
if($null -eq $InputObject){
return '$null'
}
if($InputObject -is [bool]){
if($InputObject){
return '$true'
}
return '$false'
}
if($InputObject -is [int] -or $InputObject -is [long] -or $InputObject -is [decimal] -or $InputObject -is [double]){
return ([string]$InputObject)
}
if($InputObject -is [string]){
return "'$($InputObject.Replace("'", "''"))'"
}
if($InputObject -is [System.Collections.IDictionary]){
$Lines = @("@{")
foreach($Key in $InputObject.Keys){
$ValueText = ConvertTo-PowerShellDataFileText -InputObject $InputObject[$Key] -Indent $ChildIndent
$Lines += "$ChildIndentText$Key = $ValueText"
}
$Lines += "$IndentText}"
return ($Lines -join [Environment]::NewLine)
}
if($InputObject -is [System.Collections.IEnumerable] -and $InputObject -isnot [string]){
$Items = @($InputObject)
if($Items.Count -eq 0){
return '@()'
}
$Lines = @("@(")
foreach($Item in $Items){
$ValueText = ConvertTo-PowerShellDataFileText -InputObject $Item -Indent $ChildIndent
$Lines += "$ChildIndentText$ValueText"
}
$Lines += "$IndentText)"
return ($Lines -join [Environment]::NewLine)
}
return "'$(([string]$InputObject).Replace("'", "''"))'"
}

View File

@@ -0,0 +1,28 @@
function Export-PowerShellDataFile {
[CmdletBinding()]
Param(
[Parameter(Mandatory=$true)]
[System.Collections.IDictionary]
$InputObject,
[Parameter(Mandatory=$true)]
[string]
$Path,
[Parameter(Mandatory=$false)]
[switch]
$Force
)
if((Test-Path -Path $Path -PathType Leaf) -and (-not $Force)){
throw "File [$Path] already exists. Use -Force to overwrite it."
}
$Parent = Split-Path -Path $Path -Parent
if(-not [string]::IsNullOrWhiteSpace($Parent) -and -not (Test-Path -Path $Parent)){
New-Item -Path $Parent -ItemType Directory -Force | Out-Null
}
$Text = ConvertTo-PowerShellDataFileText -InputObject $InputObject
Set-Content -Path $Path -Value $Text -Encoding UTF8
}

View File

@@ -6,8 +6,15 @@ function Get-ConfigurationDataMapValue {
[Parameter(Mandatory=$true)]
[string]
$Key
$Key,
[AllowNull()]
$DefaultValue = $null
)
if(-not (Test-ConfigurationDataMapContainsKey -Map $Map -Key $Key)){
return $DefaultValue
}
return $Map[$Key]
}

View File

@@ -0,0 +1,39 @@
function Get-ConfigurationDataObjectPropertyValue {
[CmdletBinding()]
Param(
[Parameter(Mandatory=$true)]
[AllowNull()]
$Value,
[Parameter(Mandatory=$true)]
[string]
$Property,
[Parameter(Mandatory=$false)]
[string]
$Path = ""
)
if([string]::IsNullOrWhiteSpace($Property)){
throw "Configuration data reference property must not be empty."
}
if($null -eq $Value){
throw "Configuration data reference property [$Property] was requested from a null value at path [$Path]."
}
if($Value -is [System.Collections.IDictionary]){
if(-not $Value.Contains($Property)){
throw "Configuration data reference property [$Property] was not found at path [$Path]."
}
return $Value[$Property]
}
$ObjectProperty = $Value.PSObject.Properties[$Property]
if($null -eq $ObjectProperty){
throw "Configuration data reference property [$Property] was not found at path [$Path]."
}
return $ObjectProperty.Value
}

View File

@@ -0,0 +1,61 @@
function Get-ConfigurationDataPathValue {
[CmdletBinding()]
Param(
[Parameter(Mandatory=$true)]
[AllowNull()]
$Value,
[Parameter(Mandatory=$true)]
[string]
$Path
)
if([string]::IsNullOrWhiteSpace($Path)){
throw "Configuration data reference path must not be empty."
}
$Current = $Value
$Segments = @($Path -split '\.')
foreach($Segment in $Segments){
if([string]::IsNullOrWhiteSpace($Segment)){
throw "Configuration data reference path [$Path] contains an empty segment."
}
if($null -eq $Current){
throw "Configuration data reference path [$Path] was not found. Segment [$Segment] resolved from a null value."
}
if($Current -is [System.Collections.IDictionary]){
if(-not $Current.Contains($Segment)){
throw "Configuration data reference path [$Path] was not found. Segment [$Segment] is missing."
}
$Current = $Current[$Segment]
continue
}
if($Current -is [System.Array] -and $Current -isnot [string]){
$Index = 0
if(-not [int]::TryParse($Segment, [ref]$Index)){
throw "Configuration data reference path [$Path] expected a numeric array index at segment [$Segment]."
}
if($Index -lt 0 -or $Index -ge @($Current).Count){
throw "Configuration data reference path [$Path] array index [$Index] is out of range."
}
$Current = @($Current)[$Index]
continue
}
$Property = $Current.PSObject.Properties[$Segment]
if($null -eq $Property){
throw "Configuration data reference path [$Path] was not found. Property [$Segment] is missing."
}
$Current = $Property.Value
}
return $Current
}

View File

@@ -0,0 +1,19 @@
function Get-ConfigurationDataSecretProvider {
[CmdletBinding()]
Param(
[Parameter(Mandatory=$true)]
[string]
$Name
)
if($null -eq $script:ConfigurationDataSecretProviders){
$script:ConfigurationDataSecretProviders = @{}
}
$ProviderName = $Name.ToLowerInvariant()
if(-not $script:ConfigurationDataSecretProviders.ContainsKey($ProviderName)){
return $null
}
return $script:ConfigurationDataSecretProviders[$ProviderName]
}

View File

@@ -0,0 +1,14 @@
function Get-ConfigurationDataSecretProviderName {
[CmdletBinding()]
Param()
if($null -eq $script:ConfigurationDataSecretProviders){
$script:ConfigurationDataSecretProviders = @{}
}
return @(
$script:ConfigurationDataSecretProviders.Values |
Sort-Object -Property Name |
ForEach-Object { $_.Name }
)
}

View File

@@ -23,12 +23,40 @@ function Invoke-ConfigurationDataExpression {
return [bool]::Parse($Expression)
}
if(-not (Test-ConfigurationDataExpressionParentheses -Expression $Expression)){
throw "Invalid configuration data expression [$Expression]."
}
if($Expression -notmatch "^([A-Za-z][A-Za-z0-9]*)\((.*)\)$"){
throw "Invalid configuration data expression [$Expression]."
}
$FunctionName = $Matches[1]
$ArgumentText = $Matches[2]
if($FunctionName -ieq "coalesce"){
$RawArguments = @(Split-ConfigurationDataExpressionArguments -ArgumentText $ArgumentText)
Assert-ConfigurationDataExpressionMinimumArgumentCount -Name $FunctionName -Arguments $RawArguments -Count 1
foreach($Argument in $RawArguments){
try {
$Value = Invoke-ConfigurationDataExpressionArgument -Argument $Argument -Context $Context
if(-not (Test-ConfigurationDataValueIsEmpty -Value $Value)){
return $Value
}
}
catch {
if(Test-ConfigurationDataMissingReferenceError -ErrorRecord $_){
continue
}
throw
}
}
return $null
}
$Arguments = @()
foreach($Argument in @(Split-ConfigurationDataExpressionArguments -ArgumentText $ArgumentText)){
$Arguments += ,(Invoke-ConfigurationDataExpressionArgument -Argument $Argument -Context $Context)

View File

@@ -24,6 +24,17 @@ function Invoke-ConfigurationDataExpressionFunction {
Assert-ConfigurationDataExpressionArgumentCount -Name $Name -Arguments $Arguments -Count 1
return Resolve-ConfigurationDataVariable -Name ([string]$Arguments[0]) -Context $Context
}
"reference" {
if($Arguments.Count -eq 1){
return Resolve-ConfigurationDataReference -Path ([string]$Arguments[0]) -Context $Context
}
if($Arguments.Count -eq 2){
return Resolve-ConfigurationDataReference -Path ([string]$Arguments[0]) -Property ([string]$Arguments[1]) -Context $Context
}
throw "Function [$Name] expects 1 or 2 arguments, but received [$($Arguments.Count)]."
}
"concat" {
return (@($Arguments) | ForEach-Object { [string]$_ }) -join ""
}

View File

@@ -0,0 +1,31 @@
function New-ConfigurationDataAesKeyFile {
[CmdletBinding()]
Param(
[Parameter(Mandatory=$true)]
[string]
$Path,
[Parameter(Mandatory=$false)]
[switch]
$Force
)
if((Test-Path -Path $Path -PathType Leaf) -and (-not $Force)){
$Key = [Convert]::FromBase64String((Get-Content -Path $Path -Raw).Trim())
if($Key.Length -notin @(16, 24, 32)){
throw "Existing key file [$Path] does not contain a valid AES key length."
}
return $Key
}
$Parent = Split-Path -Path $Path -Parent
if(-not [string]::IsNullOrWhiteSpace($Parent) -and -not (Test-Path -Path $Parent)){
New-Item -Path $Parent -ItemType Directory -Force | Out-Null
}
$Key = New-Object byte[] 32
[System.Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($Key)
Set-Content -Path $Path -Value ([Convert]::ToBase64String($Key)) -Encoding ASCII
return $Key
}

View File

@@ -0,0 +1,68 @@
function New-ConfigurationDataDummySecretValue {
[CmdletBinding()]
Param(
[Parameter(Mandatory=$true)]
[ValidateSet("credential", "securestring", "string")]
[string]
$ExpectedType,
[Parameter(Mandatory=$false)]
[System.Collections.IDictionary]
$Reference,
[Parameter(Mandatory=$false)]
[string]
$ParameterName = ""
)
$Name = ""
if($null -ne $Reference -and (Test-ConfigurationDataMapContainsKey -Map $Reference -Key "Name")){
$Name = [string](Get-ConfigurationDataMapValue -Map $Reference -Key "Name" -DefaultValue "")
}
$UserName = ""
if($null -ne $Reference -and (Test-ConfigurationDataMapContainsKey -Map $Reference -Key "UserName")){
$UserName = [string](Get-ConfigurationDataMapValue -Map $Reference -Key "UserName" -DefaultValue "")
}
if([string]::IsNullOrWhiteSpace($UserName)){
$UserNameSource = $ParameterName
if([string]::IsNullOrWhiteSpace($UserNameSource)){
$UserNameSource = $Name
}
if([string]::IsNullOrWhiteSpace($UserNameSource)){
$UserNameSource = "Credential"
}
$UserNameLeaf = @($UserNameSource -split '[\\/]+')[-1]
if([string]::IsNullOrWhiteSpace($UserNameLeaf)){
$UserNameLeaf = "Credential"
}
$UserName = "DUMMY\$UserNameLeaf"
}
switch($ExpectedType.ToLowerInvariant()){
"credential" {
return [pscredential]::new(
$UserName,
(ConvertTo-SecureString -String "DummyPassword!" -AsPlainText -Force)
)
}
"securestring" {
return ConvertTo-SecureString -String "DummySecret!" -AsPlainText -Force
}
"string" {
if([string]::IsNullOrWhiteSpace($Name)){
$Name = $ParameterName
}
if([string]::IsNullOrWhiteSpace($Name)){
return "DummySecret"
}
return "DummySecret:$Name"
}
}
}

View File

@@ -7,10 +7,13 @@ function New-ConfigurationDataResolutionContext {
)
$Context = [PSCustomObject]@{
ConfigurationData = $ConfigurationData
Parameters = @{}
Variables = @{}
VariableDefinitions = @{}
ResolvingVariables = @{}
ReferenceCache = @{}
ResolvingReferences = @{}
}
if($ConfigurationData.ContainsKey("Parameters") -and $null -ne $ConfigurationData.Parameters){

View File

@@ -0,0 +1,40 @@
function Register-ConfigurationDataSecretProvider {
[CmdletBinding()]
Param(
[Parameter(Mandatory=$true)]
[string]
$Name,
[Parameter(Mandatory=$true)]
[string[]]
$SupportedTypes,
[Parameter(Mandatory=$true)]
[scriptblock]
$Resolver
)
if([string]::IsNullOrWhiteSpace($Name)){
throw "Secret provider [Name] must not be empty."
}
if($SupportedTypes.Count -eq 0){
throw "Secret provider [$Name] must define at least one supported type."
}
foreach($Type in $SupportedTypes){
if($Type -notin @("credential", "securestring", "string")){
throw "Secret provider [$Name] uses unsupported type [$Type]."
}
}
if($null -eq $script:ConfigurationDataSecretProviders){
$script:ConfigurationDataSecretProviders = @{}
}
$script:ConfigurationDataSecretProviders[$Name.ToLowerInvariant()] = [PSCustomObject]@{
Name = $Name
SupportedTypes = @($SupportedTypes | ForEach-Object { $_.ToLowerInvariant() })
Resolver = $Resolver
}
}

View File

@@ -0,0 +1,50 @@
function Resolve-ConfigurationDataParameterSecrets {
[CmdletBinding()]
Param(
[Parameter(Mandatory=$true)]
[System.Collections.Hashtable]
$ConfigurationData,
[Parameter(Mandatory=$false)]
[hashtable]
$ProviderSettings = @{},
[Parameter(Mandatory=$false)]
[switch]
$UseDummySecrets
)
$ResolvedConfigurationData = $ConfigurationData.Clone()
if($ResolvedConfigurationData.ContainsKey("Parameters")){
$ResolvedConfigurationData.Parameters = $ResolvedConfigurationData.Parameters.Clone()
foreach($Parameter in @($ResolvedConfigurationData.Parameters.GetEnumerator())){
$Definition = $Parameter.Value
if(-not (Test-ConfigurationDataMap -Value $Definition)){
continue
}
$Definition = $Definition.Clone()
$ResolvedConfigurationData.Parameters[$Parameter.Name] = $Definition
$TypeName = [string](Get-ConfigurationDataMapValue -Map $Definition -Key "Type" -DefaultValue "")
if($TypeName -notin @("credential", "secureString", "string")){
continue
}
$ExpectedType = $TypeName.ToLowerInvariant()
foreach($ValueKey in @("Value", "DefaultValue")){
if((Test-ConfigurationDataMapContainsKey -Map $Definition -Key $ValueKey) -and (Test-ConfigurationDataSecretReference -Value $Definition[$ValueKey])){
if($UseDummySecrets){
$Definition[$ValueKey] = New-ConfigurationDataDummySecretValue -ExpectedType $ExpectedType -Reference $Definition[$ValueKey] -ParameterName $Parameter.Name
}else{
$Definition[$ValueKey] = Resolve-ConfigurationDataSecretReference -Reference $Definition[$ValueKey] -ExpectedType $ExpectedType -ProviderSettings $ProviderSettings
}
}
}
}
}
return $ResolvedConfigurationData
}

View File

@@ -0,0 +1,51 @@
function Resolve-ConfigurationDataProviderSecureString {
[CmdletBinding()]
Param(
[Parameter(Mandatory=$true)]
$Value
)
if($Value -is [System.Security.SecureString]){
return $Value
}
if($Value -is [string]){
return ConvertTo-SecureString -String $Value -AsPlainText -Force
}
if(-not (Test-ConfigurationDataMap -Value $Value)){
throw "Provider secure string value must be a SecureString, string, or hashtable."
}
if(Test-ConfigurationDataMapContainsKey -Map $Value -Key "EnvironmentVariable"){
$VariableName = [string](Get-ConfigurationDataMapValue -Map $Value -Key "EnvironmentVariable")
$EnvironmentValue = [Environment]::GetEnvironmentVariable($VariableName)
if([string]::IsNullOrEmpty($EnvironmentValue)){
throw "Environment variable [$VariableName] is not defined or empty."
}
return ConvertTo-SecureString -String $EnvironmentValue -AsPlainText -Force
}
if(Test-ConfigurationDataMapContainsKey -Map $Value -Key "ProtectedValue"){
$ProtectedValue = [string](Get-ConfigurationDataMapValue -Map $Value -Key "ProtectedValue")
$Key = $null
if(Test-ConfigurationDataMapContainsKey -Map $Value -Key "Key"){
$Key = [Convert]::FromBase64String([string](Get-ConfigurationDataMapValue -Map $Value -Key "Key"))
}elseif(Test-ConfigurationDataMapContainsKey -Map $Value -Key "KeyPath"){
$KeyPath = [string](Get-ConfigurationDataMapValue -Map $Value -Key "KeyPath")
if(-not (Test-Path -Path $KeyPath -PathType Leaf)){
throw "Secure string key file [$KeyPath] was not found."
}
$Key = [Convert]::FromBase64String((Get-Content -Path $KeyPath -Raw).Trim())
}else{
throw "Protected provider secure string requires [Key] or [KeyPath]."
}
return ConvertTo-SecureString -String $ProtectedValue -Key $Key
}
throw "Unsupported provider secure string reference."
}

View File

@@ -0,0 +1,28 @@
function Resolve-ConfigurationDataProviderSettingsPath {
[CmdletBinding()]
Param(
[Parameter(Mandatory=$true)]
[string]
$Provider,
[Parameter(Mandatory=$false)]
[AllowEmptyString()]
[string]
$SettingsPath
)
$FileName = "ProviderSettings.$Provider.psd1"
if([string]::IsNullOrWhiteSpace($SettingsPath)){
return (Join-Path -Path (Get-Location).Path -ChildPath $FileName)
}
if((Test-Path -Path $SettingsPath -PathType Container) -or
$SettingsPath.EndsWith("\") -or
$SettingsPath.EndsWith("/") -or
[string]::IsNullOrWhiteSpace([System.IO.Path]::GetExtension($SettingsPath))){
return (Join-Path -Path $SettingsPath -ChildPath $FileName)
}
return $SettingsPath
}

View File

@@ -0,0 +1,46 @@
function Resolve-ConfigurationDataReference {
[CmdletBinding()]
Param(
[Parameter(Mandatory=$true)]
[string]
$Path,
[Parameter(Mandatory=$false)]
[AllowEmptyString()]
[string]
$Property = "",
[Parameter(Mandatory=$true)]
$Context
)
$ReferenceKey = $Path
if(-not [string]::IsNullOrWhiteSpace($Property)){
$ReferenceKey = "$Path::$Property"
}
if($Context.ReferenceCache.ContainsKey($ReferenceKey)){
return $Context.ReferenceCache[$ReferenceKey]
}
if($Context.ResolvingReferences.ContainsKey($ReferenceKey)){
$Stack = @($Context.ResolvingReferences.Keys) + $ReferenceKey
throw "Circular configuration data reference detected: $($Stack -join ' -> ')."
}
$Context.ResolvingReferences[$ReferenceKey] = $true
try {
$RawValue = Get-ConfigurationDataPathValue -Value $Context.ConfigurationData -Path $Path
$ResolvedValue = Resolve-ConfigurationDataValue -Value $RawValue -Context $Context
if(-not [string]::IsNullOrWhiteSpace($Property)){
$ResolvedValue = Get-ConfigurationDataObjectPropertyValue -Value $ResolvedValue -Property $Property -Path $Path
}
$Context.ReferenceCache[$ReferenceKey] = $ResolvedValue
return $ResolvedValue
}
finally {
$Context.ResolvingReferences.Remove($ReferenceKey)
}
}

View File

@@ -0,0 +1,35 @@
function Resolve-ConfigurationDataSecretReference {
[CmdletBinding()]
Param(
[Parameter(Mandatory=$true)]
[System.Collections.IDictionary]
$Reference,
[Parameter(Mandatory=$true)]
[ValidateSet("credential", "securestring", "string")]
[string]
$ExpectedType,
[Parameter(Mandatory=$false)]
[hashtable]
$ProviderSettings = @{}
)
$Provider = [string](Get-ConfigurationDataMapValue -Map $Reference -Key "Provider")
$ProviderDefinition = Get-ConfigurationDataSecretProvider -Name $Provider
if($null -eq $ProviderDefinition){
throw "Unsupported secret provider [$Provider]."
}
if($ProviderDefinition.SupportedTypes -notcontains $ExpectedType.ToLowerInvariant()){
throw "Secret provider [$($ProviderDefinition.Name)] does not support type [$ExpectedType]."
}
$CurrentProviderSettings = @{}
if($ProviderSettings.ContainsKey($ProviderDefinition.Name)){
$CurrentProviderSettings = $ProviderSettings[$ProviderDefinition.Name]
}
return & $ProviderDefinition.Resolver -Reference $Reference -ExpectedType $ExpectedType -ProviderSettings $CurrentProviderSettings
}

View File

@@ -15,6 +15,9 @@ function Resolve-ConfigurationDataValue {
if($Value -is [System.Collections.Specialized.OrderedDictionary]){
$Resolved = [ordered]@{}
foreach($Entry in $Value.GetEnumerator()){
if($Entry.Name -eq "Sealed"){
continue
}
$Resolved[$Entry.Name] = Resolve-ConfigurationDataValue -Value $Entry.Value -Context $Context
}
return $Resolved
@@ -23,6 +26,9 @@ function Resolve-ConfigurationDataValue {
if($Value -is [System.Collections.Hashtable]){
$Resolved = @{}
foreach($Entry in $Value.GetEnumerator()){
if($Entry.Name -eq "Sealed"){
continue
}
$Resolved[$Entry.Name] = Resolve-ConfigurationDataValue -Value $Entry.Value -Context $Context
}
return $Resolved

View File

@@ -0,0 +1,39 @@
function Test-ConfigurationDataExpressionParentheses {
[CmdletBinding()]
Param(
[AllowNull()]
[string]
$Expression
)
if([string]::IsNullOrWhiteSpace($Expression)){
return $true
}
$Depth = 0
$InString = $false
for($Index = 0; $Index -lt $Expression.Length; $Index++){
$Character = $Expression[$Index]
if($Character -eq "'"){
$InString = -not $InString
continue
}
if($InString){
continue
}
if($Character -eq "("){
$Depth++
}elseif($Character -eq ")"){
$Depth--
if($Depth -lt 0){
return $false
}
}
}
return $Depth -eq 0 -and -not $InString
}

View File

@@ -0,0 +1,11 @@
function Test-ConfigurationDataMissingReferenceError {
[CmdletBinding()]
Param(
[Parameter(Mandatory=$true)]
[System.Management.Automation.ErrorRecord]
$ErrorRecord
)
$Message = $ErrorRecord.Exception.Message
return $Message -match "^(Parameter|Variable) \[.+\] is not defined\.$"
}

View File

@@ -0,0 +1,14 @@
function Test-ConfigurationDataSecretReference {
[CmdletBinding()]
Param(
[AllowNull()]
$Value
)
if(-not (Test-ConfigurationDataMap -Value $Value)){
return $false
}
return (Test-ConfigurationDataMapContainsKey -Map $Value -Key "Provider") -and
(Test-ConfigurationDataMapContainsKey -Map $Value -Key "Name")
}

View File

@@ -0,0 +1,49 @@
function Unlock-ConfigurationDataSecretManagementVault {
[CmdletBinding()]
Param(
[Parameter(Mandatory=$false)]
[hashtable]
$ProviderSettings = @{}
)
if(-not $ProviderSettings.ContainsKey("SecretManagement")){
return
}
$SecretManagementSettings = $ProviderSettings.SecretManagement
if(-not ($SecretManagementSettings -is [System.Collections.IDictionary])){
return
}
if(-not (Test-ConfigurationDataMapContainsKey -Map $SecretManagementSettings -Key "MasterPassword")){
return
}
$Vault = [string](Get-ConfigurationDataMapValue -Map $SecretManagementSettings -Key "DefaultVault" -DefaultValue "")
if([string]::IsNullOrWhiteSpace($Vault)){
throw "SecretManagement provider settings define [MasterPassword], but [DefaultVault] is not defined."
}
$UnlockSecretVaultCommand = Get-Command -Name Unlock-SecretVault -ErrorAction SilentlyContinue
if($null -eq $UnlockSecretVaultCommand){
throw "Command [Unlock-SecretVault] was not found. Install module [Microsoft.PowerShell.SecretManagement]."
}
$MasterPassword = Resolve-ConfigurationDataProviderSecureString -Value $SecretManagementSettings.MasterPassword
& $UnlockSecretVaultCommand -Name $Vault -Password $MasterPassword
$TestSecretVaultCommand = Get-Command -Name Test-SecretVault -ErrorAction SilentlyContinue
if($null -ne $TestSecretVaultCommand){
$IsUnlocked = $false
try {
$IsUnlocked = [bool](& $TestSecretVaultCommand -Name $Vault -ErrorAction Stop)
}
catch {
throw "SecretManagement vault [$Vault] could not be unlocked. $($_.Exception.Message)"
}
if(-not $IsUnlocked){
throw "SecretManagement vault [$Vault] could not be unlocked. Verify the KeePass database path, KeePass key file, and protected master password in the provider settings."
}
}
}

View File

@@ -0,0 +1,57 @@
$SecretManagementProvider = @{
Name = "SecretManagement"
SupportedTypes = @(
"credential",
"securestring",
"string"
)
Resolver = {
Param(
[Parameter(Mandatory=$true)]
[System.Collections.IDictionary]
$Reference,
[Parameter(Mandatory=$true)]
[ValidateSet("credential", "securestring", "string")]
[string]
$ExpectedType,
[Parameter(Mandatory=$false)]
[hashtable]
$ProviderSettings = @{}
)
$GetSecretCommand = Get-Command -Name Get-Secret -ErrorAction SilentlyContinue
if($null -eq $GetSecretCommand){
throw "SecretManagement provider requires the module [Microsoft.PowerShell.SecretManagement] and command [Get-Secret]."
}
$Vault = [string](Get-ConfigurationDataMapValue -Map $Reference -Key "Vault" -DefaultValue "")
$Name = [string](Get-ConfigurationDataMapValue -Map $Reference -Key "Name")
$UserName = [string](Get-ConfigurationDataMapValue -Map $Reference -Key "UserName" -DefaultValue "")
$Options = Get-ConfigurationDataMapValue -Map $Reference -Key "Options" -DefaultValue @{}
if([string]::IsNullOrWhiteSpace($Vault) -and $ProviderSettings.ContainsKey("DefaultVault")){
$Vault = [string]$ProviderSettings.DefaultVault
}
$CommandParameters = @{
Name = $Name
}
if(-not [string]::IsNullOrWhiteSpace($Vault)){
$CommandParameters["Vault"] = $Vault
}
if($Options -is [System.Collections.IDictionary]){
foreach($Key in $Options.Keys){
$CommandParameters[$Key] = $Options[$Key]
}
}
$Secret = & $GetSecretCommand @CommandParameters
return ConvertFrom-ConfigurationDataSecretValue -Secret $Secret -ExpectedType $ExpectedType -Name $Name -UserName $UserName
}
}
Register-ConfigurationDataSecretProvider @SecretManagementProvider

View File

@@ -0,0 +1,119 @@
function Get-DSCConfigurationDataCredentialProvider {
[CmdletBinding()]
Param(
[Parameter(Mandatory=$false)]
[string]
$Vault,
[Parameter(Mandatory=$false)]
[string]
$SettingsPath,
[Parameter(Mandatory=$false)]
[switch]
$TestVault
)
DynamicParam {
$ProviderNames = @(Get-ConfigurationDataSecretProviderName)
if($ProviderNames.Count -eq 0){
$ProviderNames = @("__NoProvidersRegistered__")
}
$ParameterAttribute = [System.Management.Automation.ParameterAttribute]::new()
$ParameterAttribute.Mandatory = $false
$ParameterAttribute.Position = 0
$ValidateSetAttribute = New-Object System.Management.Automation.ValidateSetAttribute -ArgumentList (,[string[]]$ProviderNames)
$Attributes = [System.Collections.ObjectModel.Collection[System.Attribute]]::new()
$Attributes.Add($ParameterAttribute)
$Attributes.Add($ValidateSetAttribute)
$RuntimeParameter = [System.Management.Automation.RuntimeDefinedParameter]::new(
"Provider",
[string],
$Attributes
)
$Dictionary = [System.Management.Automation.RuntimeDefinedParameterDictionary]::new()
$Dictionary.Add("Provider", $RuntimeParameter)
return $Dictionary
}
begin {
$Provider = [string]$PSBoundParameters["Provider"]
if([string]::IsNullOrWhiteSpace($Provider)){
$ProviderNames = @(Get-ConfigurationDataSecretProviderName)
if($ProviderNames.Count -eq 1){
$Provider = $ProviderNames[0]
}elseif($ProviderNames.Count -eq 0){
throw "No configuration data secret providers are registered."
}else{
throw "Provider is required. Registered providers: $($ProviderNames -join ', ')."
}
}
$ResolvedSettingsPath = Resolve-ConfigurationDataProviderSettingsPath -Provider $Provider -SettingsPath $SettingsPath
}
process {
$Settings = @{}
$SettingsExists = Test-Path -LiteralPath $ResolvedSettingsPath -PathType Leaf
if($SettingsExists){
$Settings = Import-PowerShellDataFile -LiteralPath $ResolvedSettingsPath
}
$ProviderSettings = @{}
if($Settings -is [System.Collections.IDictionary] -and $Settings.ContainsKey($Provider)){
$ProviderSettings = $Settings[$Provider]
}
$DefaultVault = ""
if($ProviderSettings -is [System.Collections.IDictionary] -and $ProviderSettings.ContainsKey("DefaultVault")){
$DefaultVault = [string]$ProviderSettings.DefaultVault
}
if([string]::IsNullOrWhiteSpace($Vault)){
$Vault = $DefaultVault
}
$Vaults = @()
$GetSecretVaultCommand = Get-Command -Name Get-SecretVault -ErrorAction SilentlyContinue
if($null -ne $GetSecretVaultCommand){
if([string]::IsNullOrWhiteSpace($Vault)){
$Vaults = @(& $GetSecretVaultCommand)
}else{
$Vaults = @(& $GetSecretVaultCommand -Name $Vault -ErrorAction SilentlyContinue)
}
}
$TestResult = $null
$TestError = $null
if($TestVault -and -not [string]::IsNullOrWhiteSpace($Vault)){
$TestSecretVaultCommand = Get-Command -Name Test-SecretVault -ErrorAction SilentlyContinue
if($null -eq $TestSecretVaultCommand){
$TestError = "Command [Test-SecretVault] was not found."
}else{
try {
$TestResult = [bool](& $TestSecretVaultCommand -Name $Vault -ErrorAction Stop)
}
catch {
$TestResult = $false
$TestError = $_.Exception.Message
}
}
}
[PSCustomObject]@{
Provider = $Provider
SettingsPath = $ResolvedSettingsPath
SettingsExists = $SettingsExists
DefaultVault = $DefaultVault
RequestedVault = $Vault
ProviderSettings = $ProviderSettings
RegisteredVaults = $Vaults
TestVault = $TestResult
TestError = $TestError
}
}
}

View File

@@ -0,0 +1,243 @@
function Register-DSCConfigurationDataCredentialProvider {
[CmdletBinding(SupportsShouldProcess=$true)]
Param(
[Parameter(Mandatory=$true, Position=1)]
[string]
$Vault,
[Parameter(Mandatory=$false)]
[ValidateSet("Generic", "KeePass")]
[string]
$VaultType = "Generic",
[Parameter(Mandatory=$false)]
[string]
$DatabasePath,
[Parameter(Mandatory=$false)]
[string]
$KeyPath,
[Parameter(Mandatory=$false)]
[string]
$SettingsPath,
[Parameter(Mandatory=$false)]
[string]
$ModuleName,
[Parameter(Mandatory=$false)]
[hashtable]
$VaultParameters = @{},
[Parameter(Mandatory=$false)]
[switch]
$RegisterVault,
[Parameter(Mandatory=$false)]
[switch]
$DefaultVault,
[Parameter(Mandatory=$false)]
[switch]
$AllowClobber,
[Parameter(Mandatory=$false)]
[switch]
$UseMasterPassword,
[Parameter(Mandatory=$false)]
[Alias("MasterKey")]
[System.Security.SecureString]
$MasterPassword,
[Parameter(Mandatory=$false)]
[Alias("MasterKeyPath")]
[string]
$MasterPasswordKeyPath,
[Parameter(Mandatory=$false)]
[switch]
$Force,
[Parameter(Mandatory=$false)]
[switch]
$PassThru
)
DynamicParam {
$ProviderNames = @(Get-ConfigurationDataSecretProviderName)
if($ProviderNames.Count -eq 0){
$ProviderNames = @("__NoProvidersRegistered__")
}
$ParameterAttribute = [System.Management.Automation.ParameterAttribute]::new()
$ParameterAttribute.Mandatory = $false
$ParameterAttribute.Position = 0
$ValidateSetAttribute = New-Object System.Management.Automation.ValidateSetAttribute -ArgumentList (,[string[]]$ProviderNames)
$Attributes = [System.Collections.ObjectModel.Collection[System.Attribute]]::new()
$Attributes.Add($ParameterAttribute)
$Attributes.Add($ValidateSetAttribute)
$RuntimeParameter = [System.Management.Automation.RuntimeDefinedParameter]::new(
"Provider",
[string],
$Attributes
)
$Dictionary = [System.Management.Automation.RuntimeDefinedParameterDictionary]::new()
$Dictionary.Add("Provider", $RuntimeParameter)
return $Dictionary
}
begin {
$Provider = [string]$PSBoundParameters["Provider"]
if([string]::IsNullOrWhiteSpace($Provider)){
$ProviderNames = @(Get-ConfigurationDataSecretProviderName)
if($ProviderNames.Count -eq 1){
$Provider = $ProviderNames[0]
}elseif($ProviderNames.Count -eq 0){
throw "No configuration data secret providers are registered."
}else{
throw "Provider is required. Registered providers: $($ProviderNames -join ', ')."
}
}
$SettingsPath = Resolve-ConfigurationDataProviderSettingsPath -Provider $Provider -SettingsPath $SettingsPath
}
process {
switch($Provider){
"SecretManagement" {
if([string]::IsNullOrWhiteSpace($ModuleName)){
if($VaultType -eq "KeePass"){
$ModuleName = "SecretManagement.KeePass"
}else{
$ModuleName = "Microsoft.PowerShell.SecretStore"
}
}
$ShouldRegisterVault = $RegisterVault -or $PSBoundParameters.ContainsKey("ModuleName")
if($VaultType -eq "KeePass" -and $ShouldRegisterVault){
if([string]::IsNullOrWhiteSpace($DatabasePath) -and -not $VaultParameters.ContainsKey("Path")){
throw "SecretManagement KeePass vault registration requires [DatabasePath] or VaultParameters['Path']."
}
if(-not [string]::IsNullOrWhiteSpace($DatabasePath)){
$VaultParameters["Path"] = $DatabasePath
}
if(-not [string]::IsNullOrWhiteSpace($KeyPath)){
$VaultParameters["KeyPath"] = $KeyPath
}
if($UseMasterPassword){
$VaultParameters["UseMasterPassword"] = $true
}
}
if($ShouldRegisterVault){
if($VaultType -eq "KeePass"){
$KeePassModule = Get-Module -Name SecretManagement.KeePass -ListAvailable | Sort-Object -Property Version -Descending | Select-Object -First 1
if($null -eq $KeePassModule){
throw "SecretManagement KeePass vault registration requires module [SecretManagement.KeePass]."
}
$ModuleReference = $KeePassModule.Name
if(-not [string]::IsNullOrWhiteSpace($KeePassModule.Path)){
$ModuleReference = $KeePassModule.Path
}
$CommandParameters = @{
Name = $Vault
ModuleName = $ModuleReference
VaultParameters = @{
Path = $VaultParameters["Path"]
UseMasterPassword = $false
UseWindowsAccount = $false
KeyPath = $null
ShowFullTitle = $false
ShowRecycleBin = $false
}
}
foreach($Key in @("KeyPath", "UseMasterPassword", "UseWindowsAccount", "ShowFullTitle", "ShowRecycleBin", "SkipValidate")){
if($VaultParameters.ContainsKey($Key)){
$CommandParameters.VaultParameters[$Key] = $VaultParameters[$Key]
}
}
if($DefaultVault){
$CommandParameters["DefaultVault"] = $true
}
if($AllowClobber){
$CommandParameters["AllowClobber"] = $true
}
if($PSCmdlet.ShouldProcess($Vault, "Register KeePass SecretManagement vault [$ModuleReference]")){
Register-SecretVault @CommandParameters
}
}else{
$CommandParameters = @{
Name = $Vault
ModuleName = $ModuleName
}
if($VaultParameters.Count -gt 0){
$CommandParameters["VaultParameters"] = $VaultParameters
}
if($DefaultVault){
$CommandParameters["DefaultVault"] = $true
}
if($AllowClobber){
$CommandParameters["AllowClobber"] = $true
}
if($PSCmdlet.ShouldProcess($Vault, "Register SecretManagement vault [$ModuleName]")){
Register-SecretVault @CommandParameters
}
}
}
$ProviderSettings = [ordered]@{
SecretManagement = [ordered]@{
DefaultVault = $Vault
}
}
if($VaultType -ne "Generic"){
$ProviderSettings.SecretManagement["VaultType"] = $VaultType
}
if($null -ne $MasterPassword){
if([string]::IsNullOrWhiteSpace($MasterPasswordKeyPath)){
throw "MasterPassword requires [MasterPasswordKeyPath]."
}
$Key = New-ConfigurationDataAesKeyFile -Path $MasterPasswordKeyPath -Force:$Force
$ProtectedValue = $MasterPassword | ConvertFrom-SecureString -Key $Key
$ProviderSettings.SecretManagement["MasterPassword"] = [ordered]@{
ProtectedValue = $ProtectedValue
KeyPath = $MasterPasswordKeyPath
}
}
if($PSCmdlet.ShouldProcess($SettingsPath, "Create provider settings for [$Provider]")){
Export-PowerShellDataFile -InputObject $ProviderSettings -Path $SettingsPath -Force:$Force
}
if($PassThru){
return $ProviderSettings
}
}
default {
throw "Provider [$Provider] does not provide an initialization implementation."
}
}
}
}

View File

@@ -3,10 +3,37 @@ function Resolve-DSCConfigurationData {
Param(
[Parameter(Mandatory=$true, ValueFromPipeline=$true)]
[System.Collections.Hashtable]
$ConfigurationData
$ConfigurationData,
[Parameter(Mandatory=$false)]
[hashtable]
$ProviderSettings = @{},
[Parameter(Mandatory=$false)]
[string]
$ProviderSettingsPath,
[Parameter(Mandatory=$false)]
[switch]
$SkipSecrets
)
process {
if(-not [string]::IsNullOrWhiteSpace($ProviderSettingsPath)){
if(-not (Test-Path -Path $ProviderSettingsPath -PathType Leaf)){
throw "Provider settings file [$ProviderSettingsPath] was not found."
}
$ProviderSettings = Import-PowerShellDataFile -Path $ProviderSettingsPath
}
if($SkipSecrets){
$ConfigurationData = Resolve-ConfigurationDataParameterSecrets -ConfigurationData $ConfigurationData -ProviderSettings $ProviderSettings -UseDummySecrets
}else{
Unlock-ConfigurationDataSecretManagementVault -ProviderSettings $ProviderSettings
$ConfigurationData = Resolve-ConfigurationDataParameterSecrets -ConfigurationData $ConfigurationData -ProviderSettings $ProviderSettings
}
$Context = New-ConfigurationDataResolutionContext -ConfigurationData $ConfigurationData
return Resolve-ConfigurationDataValue -Value $ConfigurationData -Context $Context
}

View File

@@ -0,0 +1,258 @@
function Set-DSCConfigurationDataCredentialProvider {
[CmdletBinding(SupportsShouldProcess=$true)]
Param(
[Parameter(Mandatory=$false)]
[string]
$Vault,
[Parameter(Mandatory=$false)]
[AllowEmptyString()]
[ValidateSet("", "Generic", "KeePass")]
[string]
$VaultType = "",
[Parameter(Mandatory=$false)]
[string]
$DatabasePath,
[Parameter(Mandatory=$false)]
[string]
$KeyPath,
[Parameter(Mandatory=$false)]
[switch]
$DisableKeePassKeyFile,
[Parameter(Mandatory=$false)]
[string]
$ModuleName,
[Parameter(Mandatory=$false)]
[hashtable]
$VaultParameters = @{},
[Parameter(Mandatory=$false)]
[switch]
$RegisterVault,
[Parameter(Mandatory=$false)]
[switch]
$DefaultVault,
[Parameter(Mandatory=$false)]
[switch]
$AllowClobber,
[Parameter(Mandatory=$false)]
[Alias("UseMasterKey")]
[switch]
$UseMasterPassword,
[Parameter(Mandatory=$false)]
[Alias("NoMasterKey", "NoMasterPassword")]
[switch]
$DisableMasterPassword,
[Parameter(Mandatory=$false)]
[Alias("MasterKey")]
[System.Security.SecureString]
$MasterPassword,
[Parameter(Mandatory=$false)]
[Alias("MasterKeyPath")]
[string]
$MasterPasswordKeyPath,
[Parameter(Mandatory=$false)]
[switch]
$UseWindowsAccount,
[Parameter(Mandatory=$false)]
[switch]
$ShowFullTitle,
[Parameter(Mandatory=$false)]
[switch]
$ShowRecycleBin,
[Parameter(Mandatory=$false)]
[switch]
$SkipValidate,
[Parameter(Mandatory=$false)]
[string]
$SettingsPath,
[Parameter(Mandatory=$false)]
[switch]
$Force,
[Parameter(Mandatory=$false)]
[switch]
$PassThru
)
DynamicParam {
$ProviderNames = @(Get-ConfigurationDataSecretProviderName)
if($ProviderNames.Count -eq 0){
$ProviderNames = @("__NoProvidersRegistered__")
}
$ParameterAttribute = [System.Management.Automation.ParameterAttribute]::new()
$ParameterAttribute.Mandatory = $false
$ParameterAttribute.Position = 0
$ValidateSetAttribute = New-Object System.Management.Automation.ValidateSetAttribute -ArgumentList (,[string[]]$ProviderNames)
$Attributes = [System.Collections.ObjectModel.Collection[System.Attribute]]::new()
$Attributes.Add($ParameterAttribute)
$Attributes.Add($ValidateSetAttribute)
$RuntimeParameter = [System.Management.Automation.RuntimeDefinedParameter]::new(
"Provider",
[string],
$Attributes
)
$Dictionary = [System.Management.Automation.RuntimeDefinedParameterDictionary]::new()
$Dictionary.Add("Provider", $RuntimeParameter)
return $Dictionary
}
begin {
$Provider = [string]$PSBoundParameters["Provider"]
if([string]::IsNullOrWhiteSpace($Provider)){
$ProviderNames = @(Get-ConfigurationDataSecretProviderName)
if($ProviderNames.Count -eq 1){
$Provider = $ProviderNames[0]
}elseif($ProviderNames.Count -eq 0){
throw "No configuration data secret providers are registered."
}else{
throw "Provider is required. Registered providers: $($ProviderNames -join ', ')."
}
}
$ProviderInfo = Get-DSCConfigurationDataCredentialProvider -Provider $Provider -Vault $Vault -SettingsPath $SettingsPath
if([string]::IsNullOrWhiteSpace($Vault)){
$Vault = [string]$ProviderInfo.DefaultVault
}
if([string]::IsNullOrWhiteSpace($Vault)){
throw "Vault is required. Pass [Vault] or define [DefaultVault] in the provider settings file."
}
}
process {
$ExistingVault = @($ProviderInfo.RegisteredVaults | Select-Object -First 1)
$ExistingVaultParameters = @{}
if($ExistingVault.Count -gt 0 -and $ExistingVault[0].PSObject.Properties["VaultParameters"] -and $ExistingVault[0].VaultParameters -is [System.Collections.IDictionary]){
foreach($Key in $ExistingVault[0].VaultParameters.Keys){
$ExistingVaultParameters[$Key] = $ExistingVault[0].VaultParameters[$Key]
}
}
$EffectiveVaultType = $VaultType
if([string]::IsNullOrWhiteSpace($EffectiveVaultType) -and $ProviderInfo.ProviderSettings -is [System.Collections.IDictionary] -and $ProviderInfo.ProviderSettings.ContainsKey("VaultType")){
$EffectiveVaultType = [string]$ProviderInfo.ProviderSettings.VaultType
}
if([string]::IsNullOrWhiteSpace($EffectiveVaultType) -and $ExistingVault.Count -gt 0 -and $ExistingVault[0].PSObject.Properties["ModuleName"]){
if([string]$ExistingVault[0].ModuleName -match "SecretManagement\.KeePass"){
$EffectiveVaultType = "KeePass"
}
}
if([string]::IsNullOrWhiteSpace($EffectiveVaultType)){
$EffectiveVaultType = "Generic"
}
$EffectiveVaultParameters = @{}
foreach($Key in $ExistingVaultParameters.Keys){
$EffectiveVaultParameters[$Key] = $ExistingVaultParameters[$Key]
}
foreach($Key in $VaultParameters.Keys){
$EffectiveVaultParameters[$Key] = $VaultParameters[$Key]
}
if(-not [string]::IsNullOrWhiteSpace($DatabasePath)){
$EffectiveVaultParameters["Path"] = $DatabasePath
}
if(-not [string]::IsNullOrWhiteSpace($KeyPath)){
$EffectiveVaultParameters["KeyPath"] = $KeyPath
}
if($DisableKeePassKeyFile){
$EffectiveVaultParameters["KeyPath"] = $null
}
if($UseMasterPassword -and $DisableMasterPassword){
throw "Use either [UseMasterPassword] or [DisableMasterPassword], not both."
}
foreach($SwitchName in @("UseMasterPassword", "UseWindowsAccount", "ShowFullTitle", "ShowRecycleBin", "SkipValidate")){
if($PSBoundParameters.ContainsKey($SwitchName)){
$EffectiveVaultParameters[$SwitchName] = [bool]$PSBoundParameters[$SwitchName]
}
}
if($DisableMasterPassword){
$EffectiveVaultParameters["UseMasterPassword"] = $false
}
if($PSCmdlet.ShouldProcess($Vault, "Update DSC configuration data credential provider settings")){
$EffectiveMasterPasswordKeyPath = $MasterPasswordKeyPath
if([string]::IsNullOrWhiteSpace($EffectiveMasterPasswordKeyPath) -and
$ProviderInfo.ProviderSettings -is [System.Collections.IDictionary] -and
$ProviderInfo.ProviderSettings.ContainsKey("MasterPassword") -and
$ProviderInfo.ProviderSettings.MasterPassword -is [System.Collections.IDictionary] -and
$ProviderInfo.ProviderSettings.MasterPassword.ContainsKey("KeyPath")){
$EffectiveMasterPasswordKeyPath = [string]$ProviderInfo.ProviderSettings.MasterPassword.KeyPath
}
$CommandParameters = @{
Provider = $Provider
Vault = $Vault
VaultType = $EffectiveVaultType
SettingsPath = $SettingsPath
VaultParameters = $EffectiveVaultParameters
Force = $Force
PassThru = $true
}
if(-not [string]::IsNullOrWhiteSpace($ModuleName)){
$CommandParameters["ModuleName"] = $ModuleName
}
if($RegisterVault){
$CommandParameters["RegisterVault"] = $true
}
if($DefaultVault){
$CommandParameters["DefaultVault"] = $true
}
if($AllowClobber){
$CommandParameters["AllowClobber"] = $true
}
if($null -ne $MasterPassword){
$CommandParameters["MasterPassword"] = $MasterPassword
}
if(-not [string]::IsNullOrWhiteSpace($EffectiveMasterPasswordKeyPath)){
$CommandParameters["MasterPasswordKeyPath"] = $EffectiveMasterPasswordKeyPath
}
$Result = Register-DSCConfigurationDataCredentialProvider @CommandParameters
}
if($PassThru){
if($null -ne $Result){
return $Result
}
return (Get-DSCConfigurationDataCredentialProvider -Provider $Provider -Vault $Vault -SettingsPath $SettingsPath)
}
}
}

View File

@@ -0,0 +1,100 @@
function Unregister-DSCConfigurationDataCredentialProvider {
[CmdletBinding(SupportsShouldProcess=$true, ConfirmImpact="High")]
Param(
[Parameter(Mandatory=$true)]
[string]
$Vault,
[Parameter(Mandatory=$false)]
[string]
$SettingsPath,
[Parameter(Mandatory=$false)]
[switch]
$RemoveSettings,
[Parameter(Mandatory=$false)]
[switch]
$UnregisterVault,
[Parameter(Mandatory=$false)]
[switch]
$Force,
[Parameter(Mandatory=$false)]
[switch]
$PassThru
)
DynamicParam {
$ProviderNames = @(Get-ConfigurationDataSecretProviderName)
if($ProviderNames.Count -eq 0){
$ProviderNames = @("__NoProvidersRegistered__")
}
$ParameterAttribute = [System.Management.Automation.ParameterAttribute]::new()
$ParameterAttribute.Mandatory = $false
$ParameterAttribute.Position = 0
$ValidateSetAttribute = New-Object System.Management.Automation.ValidateSetAttribute -ArgumentList (,[string[]]$ProviderNames)
$Attributes = [System.Collections.ObjectModel.Collection[System.Attribute]]::new()
$Attributes.Add($ParameterAttribute)
$Attributes.Add($ValidateSetAttribute)
$RuntimeParameter = [System.Management.Automation.RuntimeDefinedParameter]::new(
"Provider",
[string],
$Attributes
)
$Dictionary = [System.Management.Automation.RuntimeDefinedParameterDictionary]::new()
$Dictionary.Add("Provider", $RuntimeParameter)
return $Dictionary
}
begin {
$Provider = [string]$PSBoundParameters["Provider"]
if([string]::IsNullOrWhiteSpace($Provider)){
$ProviderNames = @(Get-ConfigurationDataSecretProviderName)
if($ProviderNames.Count -eq 1){
$Provider = $ProviderNames[0]
}elseif($ProviderNames.Count -eq 0){
throw "No configuration data secret providers are registered."
}else{
throw "Provider is required. Registered providers: $($ProviderNames -join ', ')."
}
}
$SettingsPath = Resolve-ConfigurationDataProviderSettingsPath -Provider $Provider -SettingsPath $SettingsPath
}
process {
$Result = [ordered]@{
Provider = $Provider
Vault = $Vault
SettingsPath = $SettingsPath
RemovedSettings = $false
UnregisteredVault = $false
}
if($RemoveSettings -or (-not $PSBoundParameters.ContainsKey("RemoveSettings"))){
if(Test-Path -Path $SettingsPath -PathType Leaf){
if($PSCmdlet.ShouldProcess($SettingsPath, "Remove provider settings file")){
Remove-Item -Path $SettingsPath -Force:$Force
$Result.RemovedSettings = $true
}
}
}
if($UnregisterVault){
if($PSCmdlet.ShouldProcess($Vault, "Unregister SecretManagement vault")){
Unregister-SecretVault -Name $Vault
$Result.UnregisteredVault = $true
}
}
if($PassThru){
return [PSCustomObject]$Result
}
}
}

229
Readme.md
View File

@@ -9,6 +9,13 @@ $merged = Merge-DSCConfigurationData -Template $service -Deployment $environment
$resolved = Resolve-DSCConfigurationData -ConfigurationData $merged
```
Pipeline flow:
```powershell
$resolved = Merge-DSCConfigurationData -Template $service -Deployment $environment |
Resolve-DSCConfigurationData
```
Expressions use an ARM-like syntax:
```powershell
@@ -57,6 +64,9 @@ This block shows all currently supported parameter properties.
# Metadata for later reporting/output tooling. The resolver does not mask values yet.
Sensitive = $false
# Prevents child templates from changing this parameter definition during merge.
Sealed = $false
# Emits a warning when the parameter is present.
Deprecated = @{
Message = @{
@@ -82,6 +92,7 @@ Notes:
- `Type`, `Required`, `AllowedValues`, `MinLength`, `MaxLength`, `MinValue`, `MaxValue`, and `Pattern` are validated by the resolver.
- `AllowedValues` validates scalar values directly and array values item by item.
- `Sensitive` is currently metadata only.
- `Sealed = $true` on a parameter seals the whole parameter definition during merge. Child templates cannot change any property of that parameter.
- `Deprecated.Message` can be a string or a localized hashtable.
- Optional string parameters can allow empty values with a pattern like `'^$|^[A-Za-z][A-Za-z0-9_-]*$'`.
@@ -101,18 +112,222 @@ Supported parameter types:
FarmPassphrase = @{
Type = 'secureString'
Required = $true
Sensitive = $true
Value = @{
SecretName = 'SharePointFarmPassphrase'
Sensitive = $true
Value = @{
Provider = 'SecretManagement'
Vault = 'Contoso'
Name = 'SharePoint/FarmPassphrase'
}
}
SetupCredential = @{
Type = 'credential'
Required = $true
Sensitive = $true
Value = @{
Provider = 'SecretManagement'
Vault = 'Contoso'
Name = 'Application/SetupAccount'
UserName = 'CONTOSO\svc-app-setup'
}
}
```
Secret references are validated and resolved by `Resolve-DSCConfigurationData` before the normal parameter and variable resolve step:
```powershell
$resolved = Resolve-DSCConfigurationData -ConfigurationData $merged -ProviderSettings @{
SecretManagement = @{
DefaultVault = 'Contoso'
}
}
```
Use `-SkipSecrets` when you only want structural validation/resolution without loading provider secrets. Secret references are replaced with typed dummy values, so references such as `reference(..., 'UserName')` keep working in tests and previews.
```powershell
$preview = Resolve-DSCConfigurationData -ConfigurationData $merged -SkipSecrets
```
SecretManagement is the only built-in secret resolver provider. KeePass, SecretStore, Azure Key Vault, and other backends should be registered as SecretManagement vaults.
KeePass can be registered through the `SecretManagement.KeePass` vault extension:
```powershell
Register-DSCConfigurationDataCredentialProvider `
-Provider SecretManagement `
-Vault Contoso `
-VaultType KeePass `
-DatabasePath 'C:\DSC\Contoso\Secrets.kdbx' `
-KeyPath 'C:\DSC\Contoso\Secrets.key' `
-UseMasterPassword `
-RegisterVault `
-DefaultVault `
-SettingsPath 'C:\DSC\Contoso'
```
For unattended KeePass vaults that still require a master password, store the master password as a protected SecureString in the provider settings. The password is encrypted with an AES key file and the vault is unlocked automatically during `Resolve-DSCConfigurationData`:
```powershell
$masterPassword = Read-Host -Prompt 'KeePass Master Password' -AsSecureString
Register-DSCConfigurationDataCredentialProvider `
-Provider SecretManagement `
-Vault Contoso `
-VaultType KeePass `
-DatabasePath 'C:\DSC\Contoso\Secrets.kdbx' `
-KeyPath 'C:\DSC\Contoso\Secrets.key' `
-UseMasterPassword `
-MasterPassword $masterPassword `
-MasterPasswordKeyPath 'C:\DSC\Contoso\KeePass-MasterPassword.key' `
-RegisterVault `
-DefaultVault `
-SettingsPath 'C:\DSC\Contoso'
```
Generic SecretManagement vaults can be registered through the same entry point:
```powershell
Register-DSCConfigurationDataCredentialProvider `
-Provider SecretManagement `
-Vault LocalStore `
-ModuleName Microsoft.PowerShell.SecretStore `
-RegisterVault `
-DefaultVault `
-SettingsPath 'C:\DSC\Contoso'
```
Provider setup can be removed again:
```powershell
Unregister-DSCConfigurationDataCredentialProvider `
-Provider SecretManagement `
-Vault Contoso `
-SettingsPath 'C:\DSC\Contoso' `
-UnregisterVault
```
Then use it directly:
```powershell
$resolved = Resolve-DSCConfigurationData `
-ConfigurationData $merged `
-ProviderSettingsPath 'C:\DSC\Contoso\ProviderSettings.SecretManagement.psd1'
```
## Sealed Template Blocks
`Sealed = $true` can also be placed on any hashtable block in the configuration data. During merge, child templates cannot add or overwrite anything at that node or below it.
```powershell
Resources = @{
NonNodeData = @{
Services = @{
SharePoint = @{
Farm = @{
ManagedAccounts = @{
Sealed = $true
FarmAccount = "[parameters('FarmCredential')]"
}
}
}
}
}
}
```
The `Sealed` marker is kept during merge so later merge steps can enforce it. `Resolve-DSCConfigurationData` removes the marker from the final resolved data so DSC resource loops do not see it as a normal configuration item.
Inspect the configured provider and registered vault:
```powershell
Get-DSCConfigurationDataCredentialProvider `
-Provider SecretManagement `
-SettingsPath 'C:\DSC\Contoso' `
-TestVault
```
Update an existing KeePass-backed SecretManagement provider registration, for example to enable master-password based unlocks:
```powershell
$masterPassword = Read-Host -Prompt 'KeePass Master Password' -AsSecureString
Set-DSCConfigurationDataCredentialProvider `
-Provider SecretManagement `
-SettingsPath 'C:\DSC\Contoso' `
-UseMasterPassword `
-MasterPassword $masterPassword `
-MasterPasswordKeyPath 'C:\DSC\Contoso\KeePass-MasterPassword.key' `
-VaultParameters @{
ShowFullTitle = $true
} `
-RegisterVault `
-AllowClobber `
-Force
```
### Secret Provider
Only one built-in provider is registered:
- `SecretManagement`: uses `Microsoft.PowerShell.SecretManagement` / `Get-Secret`
Backend-specific behavior belongs to the registered SecretManagement vault extension. For example, KeePass is handled by `SecretManagement.KeePass`, SecretStore by `Microsoft.PowerShell.SecretStore`, and Azure Key Vault by the matching SecretManagement vault extension.
SecretManagement example:
```powershell
SetupCredential = @{
Type = 'credential'
Required = $true
Sensitive = $true
Value = @{
CredentialName = 'SharePointSetup'
Provider = 'SecretManagement'
Vault = 'LocalStore'
Name = 'SharePointSetupCredential'
}
}
```
SecretStore example through SecretManagement:
```powershell
FarmPassphrase = @{
Type = 'secureString'
Required = $true
Sensitive = $true
Value = @{
Provider = 'SecretManagement'
Vault = 'LocalStore'
Name = 'SharePointFarmPassphrase'
}
}
```
Azure Key Vault example through SecretManagement:
```powershell
SetupCredential = @{
Type = 'credential'
Required = $true
Sensitive = $true
Value = @{
Provider = 'SecretManagement'
Vault = 'contoso-kv'
Name = 'app-setup-password'
UserName = 'CONTOSO\svc-app-setup'
}
}
FarmPassphrase = @{
Type = 'secureString'
Required = $true
Sensitive = $true
Value = @{
Provider = 'SecretManagement'
Vault = 'contoso-kv'
Name = 'farm-passphrase'
}
}
```
@@ -199,12 +414,16 @@ ConfigDbName : SharePoint_corp_TST_Farm_Config
```powershell
"[parameters('DatabasePrefix')]"
"[variables('ServiceDbPrefix')]"
"[reference('Resources.NonNodeData.Services.SharePoint.Farm.ManagedAccounts.FarmAccount')]"
"[reference('Resources.NonNodeData.Services.SharePoint.Farm.ManagedAccounts.FarmAccount', 'UserName')]"
```
`parameters(name)` returns the effective parameter value. `Value` is used before `DefaultValue`.
`variables(name)` resolves another variable. Variables may reference other variables.
`reference(path)` resolves another value from the configuration data by path. `reference(path, property)` resolves the value and then returns a property from it, such as `UserName` from a `PSCredential`.
### String Composition
```powershell
@@ -407,6 +626,8 @@ ConfigDbName : SharePoint_corp_TST_Farm_Config
- `parameters(name)`
- `variables(name)`
- `reference(path)`
- `reference(path, property)`
- `concat(value1, value2, ...)`
- `format(formatString, value1, value2, ...)`
- `coalesce(value1, value2, ...)`

View File

@@ -1,13 +1,17 @@
@{
@{
RootModule = "Resolve-DSCConfigurationData.psm1"
ModuleVersion = "1.0.0"
ModuleVersion = "1.1.0"
GUID = "1d6ba0d4-93d5-4b0f-94c7-bf10a30fe0e8"
Author = "Torsten Brendgen"
Copyright = "(c) Torsten Brendgen. All rights reserved."
Description = "Resolves parameters, variables, and expressions in DSC configuration data."
PowerShellVersion = "5.1"
FunctionsToExport = @(
"Resolve-DSCConfigurationData"
"Get-DSCConfigurationDataCredentialProvider",
"Resolve-DSCConfigurationData",
"Register-DSCConfigurationDataCredentialProvider",
"Set-DSCConfigurationDataCredentialProvider",
"Unregister-DSCConfigurationDataCredentialProvider"
)
CmdletsToExport = @()
VariablesToExport = @()

View File

@@ -1,13 +1,22 @@
$PrivatePath = Join-Path -Path $PSScriptRoot -ChildPath "Private"
$ProviderPath = Join-Path -Path $PSScriptRoot -ChildPath "Providers"
$PublicPath = Join-Path -Path $PSScriptRoot -ChildPath "Public"
$Private = @(Get-ChildItem -Path $PrivatePath -Filter "*.ps1" -File -ErrorAction Stop | Sort-Object -Property FullName)
$Providers = @()
if(Test-Path -Path $ProviderPath){
$Providers = @(Get-ChildItem -Path $ProviderPath -Filter "Provider.*.ps1" -File -ErrorAction Stop | Sort-Object -Property FullName)
}
$Public = @(Get-ChildItem -Path $PublicPath -Filter "*.ps1" -File -ErrorAction Stop | Sort-Object -Property FullName)
foreach($File in @($Private + $Public)){
foreach($File in @($Private + $Providers + $Public)){
. $File.FullName
}
Export-ModuleMember -Function @(
"Resolve-DSCConfigurationData"
"Get-DSCConfigurationDataCredentialProvider",
"Resolve-DSCConfigurationData",
"Register-DSCConfigurationDataCredentialProvider",
"Set-DSCConfigurationDataCredentialProvider",
"Unregister-DSCConfigurationDataCredentialProvider"
)