DSCResources/COMMUNITY_ADCSTemplate/COMMUNITY_ADCSTemplate.psm1
#requires -Version 5.0 -Modules PSADCSToolkit Set-StrictMode -Version 5.0 $modulePath = Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent $propertyMapPath = Join-Path -Path $modulePath -ChildPath "PropertyMaps" $ADCSTemplatePropertyMapPath = Join-Path -Path $propertyMapPath -ChildPath "ADCSTemplate.PropertyMap.ps1" $script:ADCSTemplatePropertyMap = (. $ADCSTemplatePropertyMapPath).Properties $script:ADProperties = $script:ADCSTemplatePropertyMap | Where-Object { $_.Import -eq $true } | Select-Object -ExpandProperty ADProperty $script:AclProperties = @( "Group" "Owner" "DACLs" "SACLs" "AreAccessRulesProtected" "AreAuditRulesProtected" ) $script:DaclAceProperties = @( "IdentityReference" "ActiveDirectoryRights" "AccessControlType" "ObjectType" "InheritanceType" "InheritedObjectType" ) $script:SaclAceProperties = @( "IdentityReference" "ActiveDirectoryRights" "AuditFlags" "ObjectType" "InheritanceType" "InheritedObjectType" ) function Get-TargetResource { [CmdletBinding()] [OutputType([System.Collections.Hashtable])] param ( [parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String]$Name, [Parameter(Mandatory = $false)] [System.Boolean]$ImportAcl = $false, [Parameter(Mandatory = $false)] [System.Boolean]$ImportEnrollmentServices = $false, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [System.String]$Server ) $common = @{} if ($PSBoundParameters.ContainsKey('Server')) { $common.Server = $server } Write-Verbose -Message "Retrieving template '$Name'." try { $template = Get-ADCSTemplate @common -Name $Name if ($ImportAcl) { $templateAcl = Get-ADCSTemplateAcl @common -Name $Name -ExcludeInheritedAce } if ($ImportEnrollmentServices) { $enrollmentServices = Get-ADCSEnrollmentService @common | Where-Object -FilterScript { $_.certificateTemplates -contains $Name } | Select-Object -ExpandProperty Name } } catch [ADCSTemplateNotFoundException] { Write-Verbose -Message "Template '$Name' is not present." $template = $null } catch { throw [InvalidOperationException]::new("Error retrieving '$Name'.") } if ($template) { Write-Verbose -Message "Template '$Name' is present." $targetResource = @{ Ensure = 'Present' } # Retrieve each property from the ADCSTemplatePropertyMap and add to the hashtable foreach ($propertyName in $script:ADProperties) { if ($propertyName -in $template.PSobject.Properties.name) { $value = $template.$propertyName $targetResource.Add($propertyName, $value) } else { $targetResource.Add($propertyName, $null) } } if ($ImportAcl) { $targetAcl = @{} foreach ($propertyName in $script:AclProperties) { if ($propertyName -in $templateAcl.PSobject.Properties.name) { $value = $templateAcl.$propertyName $targetAcl.Add($propertyName, $value) } else { $targetAcl.Add($propertyName, $null) } } $targetResource.Add('Acl', $targetAcl) } if ($ImportEnrollmentServices) { $targetResource.Add('EnrollmentServices', $enrollmentServices) } } else { $targetResource = @{ Ensure = 'Absent' } foreach ($propertyName in $script:ADProperties) { $targetResource.Add($propertyName, $null) } if ($ImportAcl) { $targetResource.Add('Acl', $null) } if ($ImportEnrollmentServices) { $targetResource.Add('EnrollmentServices', $null) } } return $targetResource } function Test-TargetResource { [CmdletBinding()] [OutputType([System.Boolean])] param ( [parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $Name, [parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $Json, [Parameter(Mandatory = $false)] [System.Boolean]$ImportAcl = $false, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [System.String[]]$EnrollmentServices, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [System.String]$Server, [Parameter(Mandatory = $true)] [ValidateSet("Present", "Absent")] [System.String] $Ensure ) $common = @{} if ($PSBoundParameters.ContainsKey('Server')) { $common.Server = $server } $importEnrollmentServices = $false if ($PSBoundParameters.ContainsKey('EnrollmentServices')) { $importEnrollmentServices = $true # Ensure unique $EnrollmentServices = [System.String[]][System.Linq.Enumerable]::Distinct($EnrollmentServices, [System.StringComparer]::OrdinalIgnoreCase) } $targetResource = Get-TargetResource @common -Name $Name -ImportAcl $ImportAcl -ImportEnrollmentServices $importEnrollmentServices $inDesiredState = $true if ($targetResource.Ensure -eq 'Present') { if ($Ensure -eq 'Present') { $inputObject = $Json | ConvertFrom-Json if ("Name" -in $inputObject.PSObject.Properties.Name) { $inputObject.Name = $Name } else { $inputObject | Add-Member -Type NoteProperty -Name Name -Value $Name } foreach ($property in ($script:ADCSTemplatePropertyMap | Where-Object { $_.Import -eq $true })) { $propertyName = $property.ADProperty $propertyType = $property.ADType if ($propertyName -in $inputObject.PSObject.Properties.Name) { $inputValue = ($inputObject.$propertyName -as $PropertyType) } else { $inputValue = $null } # Issuance policy post processing if ($inputValue -and ($propertyName -in @('msPKI-Certificate-Policy', 'msPKI-RA-Policies'))) { $updatedPolicyOids = [System.Collections.ArrayList]@() $policyOids = $inputValue foreach ($oid in $policyOids) { $backupIssuancePolicy = $inputObject.IssuancePolicies | Where-Object -Property msPKI-Cert-Template-OID -EQ -Value $Oid if ($backupIssuancePolicy) { # Custom policies try { $issuancePolicy = Get-ADCSIssuancePolicy @common -DisplayName $backupIssuancePolicy.DisplayName $updatedPolicyOids.Add($issuancePolicy.'msPKI-Cert-Template-OID') | Out-Null } catch [ADCSIssuancePolicyNotFoundException] { Write-Error -Message "Unable to find issuance policy '$($backupIssuancePolicy.DisplayName)'." # Keep original as fallback $updatedPolicyOids.Add($oid) | Out-Null } } else { # Builtin policies, not existing in AD $updatedPolicyOids.Add($oid) | Out-Null } } $inputValue = ($updatedPolicyOids -as $PropertyType) } if (($null -ne $inputValue -and $null -eq $targetResource.$propertyName) -or ($null -eq $inputValue -and $null -ne $targetResource.$propertyName) -or (Compare-Object -ReferenceObject $inputValue -DifferenceObject $targetResource.$propertyName)) { Write-Verbose -Message ("'{0}' property is NOT in the desired state. Expected '{1}', actual '{2}'." -f $propertyName, ($inputValue -join '; '), ($targetResource.$propertyName -join '; ')) $inDesiredState = $false } } if ($ImportAcl) { if ("Acl" -in $inputObject.PSObject.Properties.Name) { $targetAcl = $targetResource.Acl $inputAcl = $inputObject.Acl foreach ($propertyName in $script:AclProperties) { if ($propertyName -in $inputAcl.PSObject.Properties.Name) { $inputValue = $inputAcl.$propertyName } else { $inputValue = $null } if ($propertyName -in @('DACLs', 'SACLs')) { if ($propertyName -eq 'DACLs') { $aceProperties = $script:DaclAceProperties } else { $aceProperties = $script:SaclAceProperties } # Exclude inherited ACLs $inputValue = ($inputvalue | Where-Object -FilterScript { $_.IsInherited -eq $false }) # Ensure input is not $null before Compare-Object if ($null -eq $inputValue) { $inputValue = @() } # Ensure target is not $null before Compare-Object $targetValue = $targetAcl.$propertyName if ($null -eq $targetValue) { $targetValue = @() } if ((Compare-Object -ReferenceObject $inputValue -DifferenceObject $targetValue -Property $aceProperties)) { Write-Verbose -Message ("'{0}' ACL property is NOT in the desired state. `nExpected: {1}`n`nActual: {2}" -f $propertyName, ($inputValue | Format-List | Out-String), ($targetValue | Format-List | Out-String)) $inDesiredState = $false } } else { if (($null -ne $inputValue -and $null -eq $targetAcl.$propertyName) -or ($null -eq $inputValue -and $null -ne $targetAcl.$propertyName) -or (Compare-Object -ReferenceObject $inputValue -DifferenceObject $targetAcl.$propertyName)) { Write-Verbose -Message ("'{0}' ACL property is NOT in the desired state. Expected '{1}', actual '{2}'." -f $propertyName, ($inputValue -join '; '), ($targetAcl.$propertyName -join '; ')) $inDesiredState = $false } } } } else { Write-Error -Message "Json does not contain ACLs." } } if ($importEnrollmentServices) { # Ensure target is not $null before Compare-Object $targetValue = $targetResource.EnrollmentServices if ($null -eq $targetValue) { $targetValue = @() } if ((Compare-Object -ReferenceObject $EnrollmentServices -DifferenceObject $targetValue)) { Write-Verbose -Message ("'{0}' are NOT in the desired state. Expected '{1}', actual '{2}'." -f 'EnrollmentServices', ($EnrollmentServices -join '; '), ($targetValue -join '; ')) $inDesiredState = $false } } } else { # Resource should be Absent Write-Verbose -Message "Template '$Name' is present but should be absent." $inDesiredState = $false } } else { # Resource is Absent if ($Ensure -eq 'Present') { # Resource should be Present Write-Verbose -Message "Template '$Name' is absent but should be present." $inDesiredState = $false } else { # Resource should be Absent Write-Verbose "Template '$Name' is in the desired state." $inDesiredState = $true } } return $inDesiredState } function Set-TargetResource { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] [CmdletBinding()] param ( [parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $Name, [parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $Json, [Parameter(Mandatory = $false)] [System.Boolean]$ImportAcl = $false, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [System.String[]]$EnrollmentServices, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [System.String]$Server, [ValidateSet("Present", "Absent")] [System.String] $Ensure ) $common = @{} if ($PSBoundParameters.ContainsKey('Server')) { $common.Server = $server } $importEnrollmentServices = $false if ($PSBoundParameters.ContainsKey('EnrollmentServices')) { $importEnrollmentServices = $true # Ensure unique values $EnrollmentServices = [System.String[]][System.Linq.Enumerable]::Distinct($EnrollmentServices, [System.StringComparer]::OrdinalIgnoreCase) } $targetResource = Get-TargetResource @common -Name $Name -ImportEnrollmentServices $importEnrollmentServices If ($Ensure -eq 'Present') { if ($targetResource.Ensure -eq 'Absent') { Write-Verbose -Message "Creating template '$Name'." Write-Debug -Message ("New-ADCSTemplate Parameters: Name: '$Name', Properties: " + ($Json | Out-String)) New-ADCSTemplate @common -Name $Name -Json $Json -ImportAcl:$ImportAcl if ($PSBoundParameters.ContainsKey('EnrollmentServices')) { Publish-ADCSTemplate @common -Name $Name -EnrollmentServices $EnrollmentServices } } if ($targetResource.Ensure -eq 'Present') { Write-Verbose -Message "Updating template '$Name'." Set-ADCSTemplate @common -Name $Name -Json $Json if ($ImportAcl) { $inputObject = $Json | ConvertFrom-Json if ("Acl" -in $inputObject.PSObject.Properties.Name) { Write-Verbose -Message "Updating template ACL for '$Name'." Set-ADCSTemplateAcl @common -Name $Name -InputObject $InputObject.Acl } else { Write-Error -Message "Json does not contain ACLs." } } if ($importEnrollmentServices) { # Ensure target is not $null before Compare-Object $targetValue = $targetResource.EnrollmentServices if ($null -eq $targetValue) { $targetValue = @() } $enrollmentServiceDiff = Compare-Object -ReferenceObject $EnrollmentServices -DifferenceObject $targetValue $unpublishServices = $enrollmentServiceDiff | Where-Object { $_.SideIndicator -eq '=>' } | Select-Object -ExpandProperty InputObject if ($unpublishServices) { Write-Verbose -Message ("Unpublishing template to '{0}'." -f ($unpublishServices -join ';')) Unpublish-ADCSTemplate @common -Name $Name -EnrollmentServices $unpublishServices } $publishServices = $enrollmentServiceDiff | Where-Object { $_.SideIndicator -eq '<=' } | Select-Object -ExpandProperty InputObject if ($publishServices) { Write-Verbose -Message ("Publishing template to '{0}'." -f ($publishServices -join ';')) Publish-ADCSTemplate @common -Name $Name -EnrollmentServices $publishServices } } } } elseif (($Ensure -eq 'Absent') -and ($targetResource.Ensure -eq 'Present')) { Write-Verbose "Removing template '$Name'." Remove-ADCSTemplate @common -Name $Name -Confirm:$false | Out-Null } } Export-ModuleMember -Function *-TargetResource |