Add secret provider functionality and enhance configuration data resolution
This commit is contained in:
@@ -36,10 +36,12 @@ function Assert-ConfigurationDataSecretReference {
|
|||||||
throw "Parameter [$Name] secret reference [Name] must not be empty."
|
throw "Parameter [$Name] secret reference [Name] must not be empty."
|
||||||
}
|
}
|
||||||
|
|
||||||
switch($Provider.ToLowerInvariant()){
|
$ProviderDefinition = Get-ConfigurationDataSecretProvider -Name $Provider
|
||||||
"keepass" { return }
|
if($null -eq $ProviderDefinition){
|
||||||
default {
|
throw "Parameter [$Name] uses unsupported secret provider [$Provider]."
|
||||||
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]."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
19
Private/ConvertTo-ConfigurationDataCredential.ps1
Normal file
19
Private/ConvertTo-ConfigurationDataCredential.ps1
Normal 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)
|
||||||
|
}
|
||||||
@@ -6,8 +6,15 @@ function Get-ConfigurationDataMapValue {
|
|||||||
|
|
||||||
[Parameter(Mandatory=$true)]
|
[Parameter(Mandatory=$true)]
|
||||||
[string]
|
[string]
|
||||||
$Key
|
$Key,
|
||||||
|
|
||||||
|
[AllowNull()]
|
||||||
|
$DefaultValue = $null
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if(-not (Test-ConfigurationDataMapContainsKey -Map $Map -Key $Key)){
|
||||||
|
return $DefaultValue
|
||||||
|
}
|
||||||
|
|
||||||
return $Map[$Key]
|
return $Map[$Key]
|
||||||
}
|
}
|
||||||
|
|||||||
19
Private/Get-ConfigurationDataSecretProvider.ps1
Normal file
19
Private/Get-ConfigurationDataSecretProvider.ps1
Normal 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]
|
||||||
|
}
|
||||||
40
Private/Register-ConfigurationDataSecretProvider.ps1
Normal file
40
Private/Register-ConfigurationDataSecretProvider.ps1
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
42
Private/Resolve-ConfigurationDataParameterSecrets.ps1
Normal file
42
Private/Resolve-ConfigurationDataParameterSecrets.ps1
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
function Resolve-ConfigurationDataParameterSecrets {
|
||||||
|
[CmdletBinding()]
|
||||||
|
Param(
|
||||||
|
[Parameter(Mandatory=$true)]
|
||||||
|
[System.Collections.Hashtable]
|
||||||
|
$ConfigurationData,
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[hashtable]
|
||||||
|
$ProviderSettings = @{}
|
||||||
|
)
|
||||||
|
|
||||||
|
$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")){
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$ExpectedType = $TypeName.ToLowerInvariant()
|
||||||
|
foreach($ValueKey in @("Value", "DefaultValue")){
|
||||||
|
if((Test-ConfigurationDataMapContainsKey -Map $Definition -Key $ValueKey) -and (Test-ConfigurationDataSecretReference -Value $Definition[$ValueKey])){
|
||||||
|
$Definition[$ValueKey] = Resolve-ConfigurationDataSecretReference -Reference $Definition[$ValueKey] -ExpectedType $ExpectedType -ProviderSettings $ProviderSettings
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $ResolvedConfigurationData
|
||||||
|
}
|
||||||
35
Private/Resolve-ConfigurationDataSecretReference.ps1
Normal file
35
Private/Resolve-ConfigurationDataSecretReference.ps1
Normal 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
|
||||||
|
}
|
||||||
14
Private/Test-ConfigurationDataSecretReference.ps1
Normal file
14
Private/Test-ConfigurationDataSecretReference.ps1
Normal 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")
|
||||||
|
}
|
||||||
84
Providers/Provider.KeePass.ps1
Normal file
84
Providers/Provider.KeePass.ps1
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
$KeePassProvider = @{
|
||||||
|
Name = "KeePass"
|
||||||
|
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 = @{}
|
||||||
|
)
|
||||||
|
|
||||||
|
$KeePassCommand = Get-Command -Name Get-KeePassEntry -ErrorAction SilentlyContinue
|
||||||
|
if($null -eq $KeePassCommand){
|
||||||
|
throw "KeePass provider requires the [PoShKeePass] module command [Get-KeePassEntry]."
|
||||||
|
}
|
||||||
|
|
||||||
|
$Vault = [string](Get-ConfigurationDataMapValue -Map $Reference -Key "Vault" -DefaultValue "")
|
||||||
|
$Name = [string](Get-ConfigurationDataMapValue -Map $Reference -Key "Name")
|
||||||
|
$Options = Get-ConfigurationDataMapValue -Map $Reference -Key "Options" -DefaultValue @{}
|
||||||
|
|
||||||
|
if([string]::IsNullOrWhiteSpace($Vault) -and $ProviderSettings.ContainsKey("DefaultVault")){
|
||||||
|
$Vault = [string]$ProviderSettings.DefaultVault
|
||||||
|
}
|
||||||
|
|
||||||
|
$CommandParameters = @{}
|
||||||
|
if(-not [string]::IsNullOrWhiteSpace($Vault)){
|
||||||
|
$CommandParameters["DatabaseProfileName"] = $Vault
|
||||||
|
}
|
||||||
|
|
||||||
|
if($Options -is [System.Collections.IDictionary]){
|
||||||
|
foreach($Key in $Options.Keys){
|
||||||
|
$CommandParameters[$Key] = $Options[$Key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(-not $CommandParameters.ContainsKey("Title") -and -not $CommandParameters.ContainsKey("Path")){
|
||||||
|
$CommandParameters["Title"] = $Name
|
||||||
|
}
|
||||||
|
|
||||||
|
$Entry = & $KeePassCommand @CommandParameters
|
||||||
|
if($null -eq $Entry){
|
||||||
|
throw "KeePass entry [$Name] was not found."
|
||||||
|
}
|
||||||
|
|
||||||
|
$Entry = @($Entry)[0]
|
||||||
|
$UserName = [string](Get-ConfigurationDataMapValue -Map $Reference -Key "UserName" -DefaultValue "")
|
||||||
|
if([string]::IsNullOrWhiteSpace($UserName)){
|
||||||
|
$UserName = [string]$Entry.UserName
|
||||||
|
}
|
||||||
|
|
||||||
|
$Password = $Entry.Password
|
||||||
|
if($ExpectedType -eq "credential"){
|
||||||
|
if([string]::IsNullOrWhiteSpace($UserName)){
|
||||||
|
throw "KeePass entry [$Name] does not provide a username and no [UserName] override was defined."
|
||||||
|
}
|
||||||
|
|
||||||
|
return ConvertTo-ConfigurationDataCredential -UserName $UserName -Password $Password
|
||||||
|
}
|
||||||
|
|
||||||
|
if($Password -is [System.Security.SecureString]){
|
||||||
|
return $Password
|
||||||
|
}
|
||||||
|
|
||||||
|
if($ExpectedType -eq "securestring"){
|
||||||
|
return ConvertTo-SecureString -String ([string]$Password) -AsPlainText -Force
|
||||||
|
}
|
||||||
|
|
||||||
|
return [string]$Password
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Register-ConfigurationDataSecretProvider @KeePassProvider
|
||||||
@@ -3,10 +3,22 @@ function Resolve-DSCConfigurationData {
|
|||||||
Param(
|
Param(
|
||||||
[Parameter(Mandatory=$true, ValueFromPipeline=$true)]
|
[Parameter(Mandatory=$true, ValueFromPipeline=$true)]
|
||||||
[System.Collections.Hashtable]
|
[System.Collections.Hashtable]
|
||||||
$ConfigurationData
|
$ConfigurationData,
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[hashtable]
|
||||||
|
$ProviderSettings = @{},
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[switch]
|
||||||
|
$SkipSecrets
|
||||||
)
|
)
|
||||||
|
|
||||||
process {
|
process {
|
||||||
|
if(-not $SkipSecrets){
|
||||||
|
$ConfigurationData = Resolve-ConfigurationDataParameterSecrets -ConfigurationData $ConfigurationData -ProviderSettings $ProviderSettings
|
||||||
|
}
|
||||||
|
|
||||||
$Context = New-ConfigurationDataResolutionContext -ConfigurationData $ConfigurationData
|
$Context = New-ConfigurationDataResolutionContext -ConfigurationData $ConfigurationData
|
||||||
return Resolve-ConfigurationDataValue -Value $ConfigurationData -Context $Context
|
return Resolve-ConfigurationDataValue -Value $ConfigurationData -Context $Context
|
||||||
}
|
}
|
||||||
|
|||||||
59
Readme.md
59
Readme.md
@@ -9,6 +9,13 @@ $merged = Merge-DSCConfigurationData -Template $service -Deployment $environment
|
|||||||
$resolved = Resolve-DSCConfigurationData -ConfigurationData $merged
|
$resolved = Resolve-DSCConfigurationData -ConfigurationData $merged
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Pipeline flow:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
$resolved = Merge-DSCConfigurationData -Template $service -Deployment $environment |
|
||||||
|
Resolve-DSCConfigurationData
|
||||||
|
```
|
||||||
|
|
||||||
Expressions use an ARM-like syntax:
|
Expressions use an ARM-like syntax:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
@@ -122,11 +129,57 @@ SetupCredential = @{
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Secret references are validated by `Resolve-DSCConfigurationData`, but they are resolved by `Resolve-DSCConfigurationSecrets` before the normal data resolve step:
|
Secret references are validated and resolved by `Resolve-DSCConfigurationData` before the normal parameter and variable resolve step:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
$withSecrets = Resolve-DSCConfigurationSecrets -ConfigurationData $merged
|
$resolved = Resolve-DSCConfigurationData -ConfigurationData $merged -ProviderSettings @{
|
||||||
$resolved = Resolve-DSCConfigurationData -ConfigurationData $withSecrets
|
KeePass = @{
|
||||||
|
DefaultVault = 'BGW'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `-SkipSecrets` when you only want structural validation/resolution without loading provider secrets.
|
||||||
|
|
||||||
|
### Secret Provider Files
|
||||||
|
|
||||||
|
Secret providers are loaded automatically from the module folder `Providers`.
|
||||||
|
The naming convention is:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Providers\Provider.<Name>.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
Each provider registers itself with the same schema:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
$MyProvider = @{
|
||||||
|
Name = 'MyProvider'
|
||||||
|
SupportedTypes = @(
|
||||||
|
'credential',
|
||||||
|
'securestring',
|
||||||
|
'string'
|
||||||
|
)
|
||||||
|
Resolver = {
|
||||||
|
param(
|
||||||
|
[System.Collections.IDictionary] $Reference,
|
||||||
|
[string] $ExpectedType,
|
||||||
|
[hashtable] $ProviderSettings
|
||||||
|
)
|
||||||
|
|
||||||
|
# Return a PSCredential for ExpectedType = credential,
|
||||||
|
# a SecureString for ExpectedType = securestring,
|
||||||
|
# or a string for ExpectedType = string.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Register-ConfigurationDataSecretProvider @MyProvider
|
||||||
|
```
|
||||||
|
|
||||||
|
The KeePass provider is implemented in:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Providers\Provider.KeePass.ps1
|
||||||
```
|
```
|
||||||
|
|
||||||
Array values can be restricted item by item:
|
Array values can be restricted item by item:
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
$PrivatePath = Join-Path -Path $PSScriptRoot -ChildPath "Private"
|
$PrivatePath = Join-Path -Path $PSScriptRoot -ChildPath "Private"
|
||||||
|
$ProviderPath = Join-Path -Path $PSScriptRoot -ChildPath "Providers"
|
||||||
$PublicPath = Join-Path -Path $PSScriptRoot -ChildPath "Public"
|
$PublicPath = Join-Path -Path $PSScriptRoot -ChildPath "Public"
|
||||||
|
|
||||||
$Private = @(Get-ChildItem -Path $PrivatePath -Filter "*.ps1" -File -ErrorAction Stop | Sort-Object -Property FullName)
|
$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)
|
$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
|
. $File.FullName
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user