diff --git a/Private/Assert-ConfigurationDataParameter.ps1 b/Private/Assert-ConfigurationDataParameter.ps1 index d8ce651..4d06bed 100644 --- a/Private/Assert-ConfigurationDataParameter.ps1 +++ b/Private/Assert-ConfigurationDataParameter.ps1 @@ -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 diff --git a/Private/Assert-ConfigurationDataParameterType.ps1 b/Private/Assert-ConfigurationDataParameterType.ps1 index 7532956..5dff7bf 100644 --- a/Private/Assert-ConfigurationDataParameterType.ps1 +++ b/Private/Assert-ConfigurationDataParameterType.ps1 @@ -48,11 +48,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]." diff --git a/Private/Assert-ConfigurationDataSecretReference.ps1 b/Private/Assert-ConfigurationDataSecretReference.ps1 new file mode 100644 index 0000000..96cc72b --- /dev/null +++ b/Private/Assert-ConfigurationDataSecretReference.ps1 @@ -0,0 +1,45 @@ +function Assert-ConfigurationDataSecretReference { + [CmdletBinding()] + Param( + [Parameter(Mandatory=$true)] + [string] + $Name, + + [Parameter(Mandatory=$true)] + [ValidateSet("credential", "securestring")] + [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." + } + + switch($Provider.ToLowerInvariant()){ + "keepass" { return } + default { + throw "Parameter [$Name] uses unsupported secret provider [$Provider]." + } + } +} diff --git a/Private/Invoke-ConfigurationDataExpression.ps1 b/Private/Invoke-ConfigurationDataExpression.ps1 index 0f5b4ca..0dbe4f2 100644 --- a/Private/Invoke-ConfigurationDataExpression.ps1 +++ b/Private/Invoke-ConfigurationDataExpression.ps1 @@ -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) diff --git a/Private/Test-ConfigurationDataExpressionParentheses.ps1 b/Private/Test-ConfigurationDataExpressionParentheses.ps1 new file mode 100644 index 0000000..8fe51de --- /dev/null +++ b/Private/Test-ConfigurationDataExpressionParentheses.ps1 @@ -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 +} diff --git a/Private/Test-ConfigurationDataMissingReferenceError.ps1 b/Private/Test-ConfigurationDataMissingReferenceError.ps1 new file mode 100644 index 0000000..a9f4d50 --- /dev/null +++ b/Private/Test-ConfigurationDataMissingReferenceError.ps1 @@ -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\.$" +} diff --git a/Readme.md b/Readme.md index 8868ed1..ed11d7f 100644 --- a/Readme.md +++ b/Readme.md @@ -103,7 +103,9 @@ FarmPassphrase = @{ Required = $true Sensitive = $true Value = @{ - SecretName = 'SharePointFarmPassphrase' + Provider = 'KeePass' + Vault = 'BGW' + Name = 'SharePoint/FarmPassphrase' } } @@ -112,11 +114,21 @@ SetupCredential = @{ Required = $true Sensitive = $true Value = @{ - CredentialName = 'SharePointSetup' + Provider = 'KeePass' + Vault = 'BGW' + Name = 'SharePoint/SetupAccount' + UserName = 'BGW\SVC_SHP_SETUP' } } ``` +Secret references are validated by `Resolve-DSCConfigurationData`, but they are resolved by `Resolve-DSCConfigurationSecrets` before the normal data resolve step: + +```powershell +$withSecrets = Resolve-DSCConfigurationSecrets -ConfigurationData $merged +$resolved = Resolve-DSCConfigurationData -ConfigurationData $withSecrets +``` + Array values can be restricted item by item: ```powershell