Add new functions for credential handling, node lookup, and context-aware parameter overrides; update module version to 1.2.0
This commit is contained in:
@@ -18,6 +18,10 @@ function Assert-ConfigurationDataParameterType {
|
|||||||
|
|
||||||
$TypeName = ([string](Get-ConfigurationDataMapValue -Map $Definition -Key "Type")).ToLowerInvariant()
|
$TypeName = ([string](Get-ConfigurationDataMapValue -Map $Definition -Key "Type")).ToLowerInvariant()
|
||||||
|
|
||||||
|
if(Test-ConfigurationDataExpression -Value $Value){
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
switch($TypeName){
|
switch($TypeName){
|
||||||
"string" {
|
"string" {
|
||||||
if(Test-ConfigurationDataSecretReference -Value $Value){
|
if(Test-ConfigurationDataSecretReference -Value $Value){
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
function ConvertTo-ConfigurationDataDomainCredential {
|
||||||
|
[CmdletBinding()]
|
||||||
|
Param(
|
||||||
|
[Parameter(Mandatory=$true)]
|
||||||
|
[System.Management.Automation.PSCredential]
|
||||||
|
$Credential,
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$true)]
|
||||||
|
[string]
|
||||||
|
$Domain
|
||||||
|
)
|
||||||
|
|
||||||
|
if([string]::IsNullOrWhiteSpace($Domain)){
|
||||||
|
throw "Credential domain must not be empty."
|
||||||
|
}
|
||||||
|
|
||||||
|
$UserName = [string]$Credential.UserName
|
||||||
|
if($UserName.Contains("\") -or $UserName.Contains("@")){
|
||||||
|
return $Credential
|
||||||
|
}
|
||||||
|
|
||||||
|
return [System.Management.Automation.PSCredential]::new(
|
||||||
|
"$Domain\$UserName",
|
||||||
|
$Credential.Password
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -35,6 +35,71 @@ function Invoke-ConfigurationDataExpressionFunction {
|
|||||||
|
|
||||||
throw "Function [$Name] expects 1 or 2 arguments, but received [$($Arguments.Count)]."
|
throw "Function [$Name] expects 1 or 2 arguments, but received [$($Arguments.Count)]."
|
||||||
}
|
}
|
||||||
|
"parentkey" {
|
||||||
|
Assert-ConfigurationDataExpressionArgumentCount -Name $Name -Arguments $Arguments -Count 0
|
||||||
|
|
||||||
|
$Path = @($Context.CurrentPath)
|
||||||
|
if($Path.Count -lt 2){
|
||||||
|
throw "Function [$Name] requires the expression to be nested inside a parent map."
|
||||||
|
}
|
||||||
|
|
||||||
|
return [string]$Path[$Path.Count - 2]
|
||||||
|
}
|
||||||
|
"credentialwithdomain" {
|
||||||
|
Assert-ConfigurationDataExpressionArgumentCount -Name $Name -Arguments $Arguments -Count 2
|
||||||
|
|
||||||
|
if(-not ($Arguments[0] -is [System.Management.Automation.PSCredential])){
|
||||||
|
throw "Function [$Name] expects a PSCredential as first argument."
|
||||||
|
}
|
||||||
|
|
||||||
|
return ConvertTo-ConfigurationDataDomainCredential -Credential ([System.Management.Automation.PSCredential]$Arguments[0]) -Domain ([string]$Arguments[1])
|
||||||
|
}
|
||||||
|
"firstnodenamewhere" {
|
||||||
|
if($Arguments.Count -ne 1 -and $Arguments.Count -ne 2){
|
||||||
|
throw "Function [$Name] expects 1 or 2 arguments, but received [$($Arguments.Count)]."
|
||||||
|
}
|
||||||
|
|
||||||
|
$PropertyName = [string]$Arguments[0]
|
||||||
|
if([string]::IsNullOrWhiteSpace($PropertyName)){
|
||||||
|
throw "Function [$Name] requires a non-empty node property name."
|
||||||
|
}
|
||||||
|
|
||||||
|
$ExpectedValue = $true
|
||||||
|
if($Arguments.Count -eq 2){
|
||||||
|
$ExpectedValue = $Arguments[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
$AllNodes = $null
|
||||||
|
if($Context.ConfigurationData.ContainsKey("AllNodes")){
|
||||||
|
$AllNodes = $Context.ConfigurationData.AllNodes
|
||||||
|
}elseif($Context.ConfigurationData.ContainsKey("Resources") -and
|
||||||
|
$Context.ConfigurationData.Resources -is [System.Collections.IDictionary] -and
|
||||||
|
$Context.ConfigurationData.Resources.ContainsKey("AllNodes")){
|
||||||
|
$AllNodes = $Context.ConfigurationData.Resources.AllNodes
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach($Node in @($AllNodes)){
|
||||||
|
if($null -eq $Node -or -not ($Node -is [System.Collections.IDictionary])){
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if(-not $Node.ContainsKey($PropertyName)){
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if($Node[$PropertyName] -ne $ExpectedValue){
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if(-not $Node.ContainsKey("NodeName") -or [string]::IsNullOrWhiteSpace([string]$Node.NodeName)){
|
||||||
|
throw "Function [$Name] matched a node by [$PropertyName], but the node does not define [NodeName]."
|
||||||
|
}
|
||||||
|
|
||||||
|
return [string]$Node.NodeName
|
||||||
|
}
|
||||||
|
|
||||||
|
return $null
|
||||||
|
}
|
||||||
"concat" {
|
"concat" {
|
||||||
return (@($Arguments) | ForEach-Object { [string]$_ }) -join ""
|
return (@($Arguments) | ForEach-Object { [string]$_ }) -join ""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,7 +40,11 @@ function New-ConfigurationDataDummySecretValue {
|
|||||||
$UserNameLeaf = "Credential"
|
$UserNameLeaf = "Credential"
|
||||||
}
|
}
|
||||||
|
|
||||||
$UserName = "DUMMY\$UserNameLeaf"
|
if($null -ne $Reference -and (Test-ConfigurationDataMapContainsKey -Map $Reference -Key "Domain")){
|
||||||
|
$UserName = $UserNameLeaf
|
||||||
|
}else{
|
||||||
|
$UserName = "DUMMY\$UserNameLeaf"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
switch($ExpectedType.ToLowerInvariant()){
|
switch($ExpectedType.ToLowerInvariant()){
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ function New-ConfigurationDataResolutionContext {
|
|||||||
ResolvingVariables = @{}
|
ResolvingVariables = @{}
|
||||||
ReferenceCache = @{}
|
ReferenceCache = @{}
|
||||||
ResolvingReferences = @{}
|
ResolvingReferences = @{}
|
||||||
|
CurrentPath = @()
|
||||||
}
|
}
|
||||||
|
|
||||||
if($ConfigurationData.ContainsKey("Parameters") -and $null -ne $ConfigurationData.Parameters){
|
if($ConfigurationData.ContainsKey("Parameters") -and $null -ne $ConfigurationData.Parameters){
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ function Resolve-ConfigurationDataParameterSecrets {
|
|||||||
)
|
)
|
||||||
|
|
||||||
$ResolvedConfigurationData = $ConfigurationData.Clone()
|
$ResolvedConfigurationData = $ConfigurationData.Clone()
|
||||||
|
$Context = New-ConfigurationDataResolutionContext -ConfigurationData $ResolvedConfigurationData
|
||||||
|
|
||||||
if($ResolvedConfigurationData.ContainsKey("Parameters")){
|
if($ResolvedConfigurationData.ContainsKey("Parameters")){
|
||||||
$ResolvedConfigurationData.Parameters = $ResolvedConfigurationData.Parameters.Clone()
|
$ResolvedConfigurationData.Parameters = $ResolvedConfigurationData.Parameters.Clone()
|
||||||
@@ -36,10 +37,19 @@ function Resolve-ConfigurationDataParameterSecrets {
|
|||||||
$ExpectedType = $TypeName.ToLowerInvariant()
|
$ExpectedType = $TypeName.ToLowerInvariant()
|
||||||
foreach($ValueKey in @("Value", "DefaultValue")){
|
foreach($ValueKey in @("Value", "DefaultValue")){
|
||||||
if((Test-ConfigurationDataMapContainsKey -Map $Definition -Key $ValueKey) -and (Test-ConfigurationDataSecretReference -Value $Definition[$ValueKey])){
|
if((Test-ConfigurationDataMapContainsKey -Map $Definition -Key $ValueKey) -and (Test-ConfigurationDataSecretReference -Value $Definition[$ValueKey])){
|
||||||
|
$SecretReference = $Definition[$ValueKey]
|
||||||
if($UseDummySecrets){
|
if($UseDummySecrets){
|
||||||
$Definition[$ValueKey] = New-ConfigurationDataDummySecretValue -ExpectedType $ExpectedType -Reference $Definition[$ValueKey] -ParameterName $Parameter.Name
|
$Definition[$ValueKey] = New-ConfigurationDataDummySecretValue -ExpectedType $ExpectedType -Reference $SecretReference -ParameterName $Parameter.Name
|
||||||
}else{
|
}else{
|
||||||
$Definition[$ValueKey] = Resolve-ConfigurationDataSecretReference -Reference $Definition[$ValueKey] -ExpectedType $ExpectedType -ProviderSettings $ProviderSettings
|
$Definition[$ValueKey] = Resolve-ConfigurationDataSecretReference -Reference $SecretReference -ExpectedType $ExpectedType -ProviderSettings $ProviderSettings -Context $Context
|
||||||
|
}
|
||||||
|
|
||||||
|
if($ExpectedType -eq "credential" -and
|
||||||
|
$UseDummySecrets -and
|
||||||
|
(Test-ConfigurationDataMapContainsKey -Map $SecretReference -Key "Domain")){
|
||||||
|
$Domain = Get-ConfigurationDataMapValue -Map $SecretReference -Key "Domain"
|
||||||
|
$Domain = Resolve-ConfigurationDataValue -Value $Domain -Context $Context
|
||||||
|
$Definition[$ValueKey] = ConvertTo-ConfigurationDataDomainCredential -Credential $Definition[$ValueKey] -Domain ([string]$Domain)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,11 @@ function Resolve-ConfigurationDataSecretReference {
|
|||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
[Parameter(Mandatory=$false)]
|
||||||
[hashtable]
|
[hashtable]
|
||||||
$ProviderSettings = @{}
|
$ProviderSettings = @{},
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[AllowNull()]
|
||||||
|
$Context = $null
|
||||||
)
|
)
|
||||||
|
|
||||||
$Provider = [string](Get-ConfigurationDataMapValue -Map $Reference -Key "Provider")
|
$Provider = [string](Get-ConfigurationDataMapValue -Map $Reference -Key "Provider")
|
||||||
@@ -31,5 +35,20 @@ function Resolve-ConfigurationDataSecretReference {
|
|||||||
$CurrentProviderSettings = $ProviderSettings[$ProviderDefinition.Name]
|
$CurrentProviderSettings = $ProviderSettings[$ProviderDefinition.Name]
|
||||||
}
|
}
|
||||||
|
|
||||||
return & $ProviderDefinition.Resolver -Reference $Reference -ExpectedType $ExpectedType -ProviderSettings $CurrentProviderSettings
|
$SecretValue = & $ProviderDefinition.Resolver -Reference $Reference -ExpectedType $ExpectedType -ProviderSettings $CurrentProviderSettings
|
||||||
|
|
||||||
|
if($ExpectedType.ToLowerInvariant() -eq "credential" -and (Test-ConfigurationDataMapContainsKey -Map $Reference -Key "Domain")){
|
||||||
|
if(-not ($SecretValue -is [System.Management.Automation.PSCredential])){
|
||||||
|
throw "Secret reference [$($Reference.Name)] defines [Domain], but the resolved value is not a PSCredential."
|
||||||
|
}
|
||||||
|
|
||||||
|
$Domain = Get-ConfigurationDataMapValue -Map $Reference -Key "Domain"
|
||||||
|
if($null -ne $Context){
|
||||||
|
$Domain = Resolve-ConfigurationDataValue -Value $Domain -Context $Context
|
||||||
|
}
|
||||||
|
|
||||||
|
$SecretValue = ConvertTo-ConfigurationDataDomainCredential -Credential $SecretValue -Domain ([string]$Domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
return $SecretValue
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,10 @@ function Resolve-ConfigurationDataValue {
|
|||||||
$Value,
|
$Value,
|
||||||
|
|
||||||
[Parameter(Mandatory=$true)]
|
[Parameter(Mandatory=$true)]
|
||||||
$Context
|
$Context,
|
||||||
|
|
||||||
|
[object[]]
|
||||||
|
$Path = @()
|
||||||
)
|
)
|
||||||
|
|
||||||
if($null -eq $Value){
|
if($null -eq $Value){
|
||||||
@@ -18,7 +21,7 @@ function Resolve-ConfigurationDataValue {
|
|||||||
if($Entry.Name -eq "Sealed"){
|
if($Entry.Name -eq "Sealed"){
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
$Resolved[$Entry.Name] = Resolve-ConfigurationDataValue -Value $Entry.Value -Context $Context
|
$Resolved[$Entry.Name] = Resolve-ConfigurationDataValue -Value $Entry.Value -Context $Context -Path (@($Path) + @($Entry.Name))
|
||||||
}
|
}
|
||||||
return $Resolved
|
return $Resolved
|
||||||
}
|
}
|
||||||
@@ -29,21 +32,27 @@ function Resolve-ConfigurationDataValue {
|
|||||||
if($Entry.Name -eq "Sealed"){
|
if($Entry.Name -eq "Sealed"){
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
$Resolved[$Entry.Name] = Resolve-ConfigurationDataValue -Value $Entry.Value -Context $Context
|
$Resolved[$Entry.Name] = Resolve-ConfigurationDataValue -Value $Entry.Value -Context $Context -Path (@($Path) + @($Entry.Name))
|
||||||
}
|
}
|
||||||
return $Resolved
|
return $Resolved
|
||||||
}
|
}
|
||||||
|
|
||||||
if($Value -is [System.Array] -and $Value -isnot [string]){
|
if($Value -is [System.Array] -and $Value -isnot [string]){
|
||||||
$Resolved = @()
|
$Resolved = @()
|
||||||
foreach($Item in $Value){
|
for($Index = 0; $Index -lt $Value.Count; $Index++){
|
||||||
$Resolved += ,(Resolve-ConfigurationDataValue -Value $Item -Context $Context)
|
$Resolved += ,(Resolve-ConfigurationDataValue -Value $Value[$Index] -Context $Context -Path (@($Path) + @($Index)))
|
||||||
}
|
}
|
||||||
return ,$Resolved
|
return ,$Resolved
|
||||||
}
|
}
|
||||||
|
|
||||||
if($Value -is [string] -and (Test-ConfigurationDataExpression -Value $Value)){
|
if($Value -is [string] -and (Test-ConfigurationDataExpression -Value $Value)){
|
||||||
return Resolve-ConfigurationDataExpression -Expression $Value -Context $Context
|
$PreviousPath = @($Context.CurrentPath)
|
||||||
|
$Context.CurrentPath = @($Path)
|
||||||
|
try {
|
||||||
|
return Resolve-ConfigurationDataExpression -Expression $Value -Context $Context
|
||||||
|
} finally {
|
||||||
|
$Context.CurrentPath = $PreviousPath
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return $Value
|
return $Value
|
||||||
|
|||||||
@@ -424,6 +424,102 @@ ConfigDbName : SharePoint_corp_TST_Farm_Config
|
|||||||
|
|
||||||
`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`.
|
`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`.
|
||||||
|
|
||||||
|
### Credentials
|
||||||
|
|
||||||
|
`credentialWithDomain(credential, domain)` returns a new `PSCredential` with the same password and a domain-qualified user name. If the user name already contains `DOMAIN\User` or `user@domain`, the original credential is returned.
|
||||||
|
|
||||||
|
This is useful when a secret backend such as KeePass stores the user name without a domain, but the DSC resource expects a domain-qualified credential:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Parameters = @{
|
||||||
|
DomainNetBIOS = @{
|
||||||
|
Type = 'string'
|
||||||
|
Value = 'CONTOSO'
|
||||||
|
}
|
||||||
|
|
||||||
|
FarmCredential = @{
|
||||||
|
Type = 'credential'
|
||||||
|
Value = @{
|
||||||
|
Provider = 'SecretManagement'
|
||||||
|
Vault = 'KeePass'
|
||||||
|
Name = 'SharePoint/FarmAccount'
|
||||||
|
Domain = "[parameters('DomainNetBIOS')]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The same transformation can also be used explicitly in expressions:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
FarmCredentialQualified = @{
|
||||||
|
Type = 'credential'
|
||||||
|
DefaultValue = "[credentialWithDomain(parameters('FarmCredential'), parameters('DomainNetBIOS'))]"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Node Lookup
|
||||||
|
|
||||||
|
`firstNodeNameWhere(propertyName)` returns the first `NodeName` from `AllNodes` where the named property is `$true`.
|
||||||
|
`firstNodeNameWhere(propertyName, expectedValue)` compares the property with the provided value.
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
AllNodes = @(
|
||||||
|
@{
|
||||||
|
NodeName = 'SP01'
|
||||||
|
RunCentralAdmin = $true
|
||||||
|
}
|
||||||
|
@{
|
||||||
|
NodeName = 'SP02'
|
||||||
|
RunCentralAdmin = $false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
CentralAdministrationHostName = @{
|
||||||
|
Type = 'string'
|
||||||
|
DefaultValue = "[firstNodeNameWhere('RunCentralAdmin')]"
|
||||||
|
}
|
||||||
|
|
||||||
|
CentralAdministrationUrl = @{
|
||||||
|
Type = 'string'
|
||||||
|
DefaultValue = "[format('https://{0}:{1}', parameters('CentralAdministrationHostName'), parameters('CentralAdministrationPort'))]"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If a fixed URL is required, set an explicit parameter value and the default expression is not used.
|
||||||
|
|
||||||
|
### Parent Map Key
|
||||||
|
|
||||||
|
`parentKey()` returns the key of the map that contains the current expression property. It can be used to derive resource-specific parameter names while retaining a shared default:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Parameters = @{
|
||||||
|
DefaultWebApplicationAuthenticationMethod = @{
|
||||||
|
Type = 'string'
|
||||||
|
DefaultValue = 'NTLM'
|
||||||
|
}
|
||||||
|
HNSCWebApplicationAuthenticationMethod = @{
|
||||||
|
Type = 'string'
|
||||||
|
Value = 'Kerberos'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Resources = @{
|
||||||
|
WebApplications = @{
|
||||||
|
HNSC = @{
|
||||||
|
AuthenticationMethod = "[coalesce(parameters(concat(parentKey(), 'WebApplicationAuthenticationMethod')), parameters('DefaultWebApplicationAuthenticationMethod'))]"
|
||||||
|
}
|
||||||
|
MySite = @{
|
||||||
|
AuthenticationMethod = "[coalesce(parameters(concat(parentKey(), 'WebApplicationAuthenticationMethod')), parameters('DefaultWebApplicationAuthenticationMethod'))]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `HNSC` resource resolves the optional `HNSCWebApplicationAuthenticationMethod` override. `MySite` falls back to `DefaultWebApplicationAuthenticationMethod` when no corresponding override parameter exists.
|
||||||
|
|
||||||
### String Composition
|
### String Composition
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
@@ -628,6 +724,10 @@ ConfigDbName : SharePoint_corp_TST_Farm_Config
|
|||||||
- `variables(name)`
|
- `variables(name)`
|
||||||
- `reference(path)`
|
- `reference(path)`
|
||||||
- `reference(path, property)`
|
- `reference(path, property)`
|
||||||
|
- `credentialWithDomain(credential, domain)`
|
||||||
|
- `firstNodeNameWhere(propertyName)`
|
||||||
|
- `firstNodeNameWhere(propertyName, expectedValue)`
|
||||||
|
- `parentKey()`
|
||||||
- `concat(value1, value2, ...)`
|
- `concat(value1, value2, ...)`
|
||||||
- `format(formatString, value1, value2, ...)`
|
- `format(formatString, value1, value2, ...)`
|
||||||
- `coalesce(value1, value2, ...)`
|
- `coalesce(value1, value2, ...)`
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
@{
|
@{
|
||||||
RootModule = "Resolve-DSCConfigurationData.psm1"
|
RootModule = "Resolve-DSCConfigurationData.psm1"
|
||||||
ModuleVersion = "1.1.0"
|
ModuleVersion = "1.2.0"
|
||||||
GUID = "1d6ba0d4-93d5-4b0f-94c7-bf10a30fe0e8"
|
GUID = "1d6ba0d4-93d5-4b0f-94c7-bf10a30fe0e8"
|
||||||
Author = "Torsten Brendgen"
|
Author = "Torsten Brendgen"
|
||||||
Copyright = "(c) Torsten Brendgen. All rights reserved."
|
Copyright = "(c) Torsten Brendgen. All rights reserved."
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
"DSC",
|
"DSC",
|
||||||
"ConfigurationData"
|
"ConfigurationData"
|
||||||
)
|
)
|
||||||
ReleaseNotes = "Initial module layout."
|
ReleaseNotes = "Adds parentKey() for context-aware parameter overrides, domain-qualified credential secret references, and node lookup expression functions."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user