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()
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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, ...)`
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user