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:
Torsten Brendgen
2026-08-18 22:45:05 +02:00
parent 4b67c74ac0
commit 5a07cded4e
10 changed files with 251 additions and 13 deletions
@@ -18,6 +18,10 @@ function Assert-ConfigurationDataParameterType {
$TypeName = ([string](Get-ConfigurationDataMapValue -Map $Definition -Key "Type")).ToLowerInvariant()
if(Test-ConfigurationDataExpression -Value $Value){
return
}
switch($TypeName){
"string" {
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)]."
}
"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" {
return (@($Arguments) | ForEach-Object { [string]$_ }) -join ""
}
@@ -40,7 +40,11 @@ function New-ConfigurationDataDummySecretValue {
$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()){
@@ -14,6 +14,7 @@ function New-ConfigurationDataResolutionContext {
ResolvingVariables = @{}
ReferenceCache = @{}
ResolvingReferences = @{}
CurrentPath = @()
}
if($ConfigurationData.ContainsKey("Parameters") -and $null -ne $ConfigurationData.Parameters){
@@ -15,6 +15,7 @@ function Resolve-ConfigurationDataParameterSecrets {
)
$ResolvedConfigurationData = $ConfigurationData.Clone()
$Context = New-ConfigurationDataResolutionContext -ConfigurationData $ResolvedConfigurationData
if($ResolvedConfigurationData.ContainsKey("Parameters")){
$ResolvedConfigurationData.Parameters = $ResolvedConfigurationData.Parameters.Clone()
@@ -36,10 +37,19 @@ function Resolve-ConfigurationDataParameterSecrets {
$ExpectedType = $TypeName.ToLowerInvariant()
foreach($ValueKey in @("Value", "DefaultValue")){
if((Test-ConfigurationDataMapContainsKey -Map $Definition -Key $ValueKey) -and (Test-ConfigurationDataSecretReference -Value $Definition[$ValueKey])){
$SecretReference = $Definition[$ValueKey]
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{
$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)]
[hashtable]
$ProviderSettings = @{}
$ProviderSettings = @{},
[Parameter(Mandatory=$false)]
[AllowNull()]
$Context = $null
)
$Provider = [string](Get-ConfigurationDataMapValue -Map $Reference -Key "Provider")
@@ -31,5 +35,20 @@ function Resolve-ConfigurationDataSecretReference {
$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
}
+15 -6
View File
@@ -5,7 +5,10 @@ function Resolve-ConfigurationDataValue {
$Value,
[Parameter(Mandatory=$true)]
$Context
$Context,
[object[]]
$Path = @()
)
if($null -eq $Value){
@@ -18,7 +21,7 @@ function Resolve-ConfigurationDataValue {
if($Entry.Name -eq "Sealed"){
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
}
@@ -29,21 +32,27 @@ function Resolve-ConfigurationDataValue {
if($Entry.Name -eq "Sealed"){
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
}
if($Value -is [System.Array] -and $Value -isnot [string]){
$Resolved = @()
foreach($Item in $Value){
$Resolved += ,(Resolve-ConfigurationDataValue -Value $Item -Context $Context)
for($Index = 0; $Index -lt $Value.Count; $Index++){
$Resolved += ,(Resolve-ConfigurationDataValue -Value $Value[$Index] -Context $Context -Path (@($Path) + @($Index)))
}
return ,$Resolved
}
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
+100
View File
@@ -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`.
### 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
```powershell
@@ -628,6 +724,10 @@ ConfigDbName : SharePoint_corp_TST_Farm_Config
- `variables(name)`
- `reference(path)`
- `reference(path, property)`
- `credentialWithDomain(credential, domain)`
- `firstNodeNameWhere(propertyName)`
- `firstNodeNameWhere(propertyName, expectedValue)`
- `parentKey()`
- `concat(value1, value2, ...)`
- `format(formatString, value1, value2, ...)`
- `coalesce(value1, value2, ...)`
+2 -2
View File
@@ -1,6 +1,6 @@
@{
RootModule = "Resolve-DSCConfigurationData.psm1"
ModuleVersion = "1.1.0"
ModuleVersion = "1.2.0"
GUID = "1d6ba0d4-93d5-4b0f-94c7-bf10a30fe0e8"
Author = "Torsten Brendgen"
Copyright = "(c) Torsten Brendgen. All rights reserved."
@@ -22,7 +22,7 @@
"DSC",
"ConfigurationData"
)
ReleaseNotes = "Initial module layout."
ReleaseNotes = "Adds parentKey() for context-aware parameter overrides, domain-qualified credential secret references, and node lookup expression functions."
}
}
}