Private/Invoke-AGTemplateScenario.ps1
|
function Invoke-AGTemplateScenario { <# .SYNOPSIS Runs the shared scenario deploy pipeline for one template scenario. .DESCRIPTION The pipeline every scenario deploy (issues #27-#30) shares: clone -> recipe -> rights -> publish -> record state 1. Clone. The source template is cloned via Copy-AGTemplate (issue #25 kernel), which applies the fresh OID, schema upgrade, whole- descriptor copy, ownership marker, and collision contract. 2. Recipe. The caller's -Recipe scriptblock runs against the resolved clone DirectoryEntry and commits any attribute overrides. ESC4 passes no recipe (zero overrides). 3. Rights. The caller's -AccessRules scriptblock returns the ActiveDirectoryAccessRule objects to layer on the copied source DACL. 4. Publish. The clone's cn is added to the selected CA's certificateTemplates. 5. State. The clone's actual cn, minted OID, companion OID object DN, and post-setup SDDL are appended to the deploy state file (#26). The source template is never written. .PARAMETER Scenario The scenario label recorded in the state file (e.g. 'ESC1'). .PARAMETER SourceName The cn of the built-in source template (e.g. 'WebServer'). .PARAMETER DestinationName The desired cn/displayName of the clone. .PARAMETER Recipe A scriptblock invoked as `& $Recipe $clone` that applies attribute overrides to the clone DirectoryEntry and commits them. Omit for scenarios with no attribute overrides. .PARAMETER AccessRules A scriptblock invoked as `& $AccessRules $principalSid` that returns the ActiveDirectoryAccessRule objects to grant on the clone. $principalSid is the SID of the principal named by -Principal. Omit for scenarios that grant no template rights. .PARAMETER Principal Which principal the -AccessRules scriptblock builds ACEs for: 'DomainUsers' (resolved from the forest) or 'AuthenticatedUsers' (the well-known SID S-1-5-11). Defaults to 'DomainUsers'. .PARAMETER AlsoPublishTemplate An additional built-in template cn to publish on the selected CA if not already published (e.g. the ESC3 chain's User template). Its ACL is never touched. .PARAMETER CAName The cn of the enterprise CA to publish to. Optional; autodetected when the forest has exactly one enterprise CA. .PARAMETER StatePath Where the deploy state file lives. .PARAMETER Server The domain controller to write to. Defaults to the logon server. .PARAMETER Force Replaces an ADCSGoat-owned existing clone without prompting. .OUTPUTS System.Management.Automation.PSCustomObject with CloneCn, Oid, and CompanionOidObjectDN. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'AD writes gated by the Copy-AGTemplate collision prompt / -Force contract per module precedent.')] [CmdletBinding()] param ( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$Scenario, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$SourceName, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$DestinationName, [Parameter()] [scriptblock]$Recipe, [Parameter()] [scriptblock]$AccessRules, # Which principal the -AccessRules scriptblock builds ACEs for. Domain # Users is resolved from the forest; Authenticated Users is the # well-known SID S-1-5-11. [Parameter()] [ValidateSet('DomainUsers', 'AuthenticatedUsers')] [string]$Principal = 'DomainUsers', # An additional built-in template cn to publish on the selected CA if # not already published (e.g. the ESC3 chain's User template). Its ACL # is never touched. [Parameter()] [ValidateNotNullOrEmpty()] [string]$AlsoPublishTemplate, [Parameter()] [ValidateNotNullOrEmpty()] [string]$CAName, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$StatePath, [Parameter()] [ValidateNotNullOrEmpty()] [string]$Server, [Parameter()] [switch]$Force ) begin { Add-Type -AssemblyName System.DirectoryServices # The prevailing repo idiom builds paths as "LDAP://$Server/DN", which # resolves correctly whether or not -Server was supplied. $serverPrefix = if ($PSBoundParameters.ContainsKey('Server')) { "LDAP://$Server/" } else { 'LDAP://' } $helperParams = @{} if ($PSBoundParameters.ContainsKey('Server')) { $helperParams['Server'] = $Server } } process { # Resolve the CA and load/create the state file. if ($PSBoundParameters.ContainsKey('CAName')) { $selectedCA = Get-AGEnrollmentService -CAName $CAName @helperParams } else { $selectedCA = Get-AGEnrollmentService @helperParams } if (Test-Path -Path $StatePath) { $state = Read-AGDeployState -Path $StatePath } else { $state = New-AGDeployState -SelectedCA $selectedCA @helperParams } # 1. Clone the source. $copyParams = @{ SourceName = $SourceName; DestinationName = $DestinationName } if ($Force.IsPresent) { $copyParams['Force'] = $true } if ($helperParams.ContainsKey('Server')) { $copyParams['Server'] = $helperParams['Server'] } $cloneResult = Copy-AGTemplate @copyParams $cloneCn = $cloneResult.DestinationName # Resolve the freshly created clone object. $rootDSE = New-Object System.DirectoryServices.DirectoryEntry("${serverPrefix}RootDSE") $configurationPartition = $rootDSE.configurationNamingContext $cloneDN = "CN=$cloneCn,CN=Certificate Templates,CN=Public Key Services,CN=Services,$configurationPartition" $clone = New-Object System.DirectoryServices.DirectoryEntry("$serverPrefix$cloneDN") $clone.RefreshCache() # 2. Recipe: apply the caller's attribute overrides, if any. if ($PSBoundParameters.ContainsKey('Recipe') -and $null -ne $Recipe) { & $Recipe $clone } # 3. Rights: layer the caller's ACEs on the copied DACL, if any. if ($PSBoundParameters.ContainsKey('AccessRules') -and $null -ne $AccessRules) { $principalSid = if ($Principal -eq 'AuthenticatedUsers') { New-Object System.Security.Principal.SecurityIdentifier('S-1-5-11') } else { (New-Object System.Security.Principal.NTAccount((Get-ADDomainNameFromNc -ConfigurationNC $configurationPartition @helperParams), 'Domain Users')).Translate([System.Security.Principal.SecurityIdentifier]) } $sd = $clone.ObjectSecurity foreach ($rule in @(& $AccessRules $principalSid)) { $sd.AddAccessRule($rule) } $clone.ObjectSecurity = $sd $clone.CommitChanges() } $postSetupSddl = $clone.ObjectSecurity.GetSecurityDescriptorSddlForm([System.Security.AccessControl.AccessControlSections]::All) $clone.Dispose() # 4. Publish the clone on the selected CA, plus any additional built-in # template the scenario depends on (e.g. the ESC3 chain's User # template). Publish is additive; an already-published template is # left as-is and its ACL is never touched. $caEntry = New-Object System.DirectoryServices.DirectoryEntry("$serverPrefix$($selectedCA.DistinguishedName)") $published = @($caEntry.Properties['certificateTemplates'] | ForEach-Object { "$_" }) if ($published -notcontains $cloneCn) { $caEntry.PutEx(3, 'certificateTemplates', @($cloneCn)) # ADS_PROPERTY_APPEND $caEntry.SetInfo() $caEntry.RefreshCache() $published = @($caEntry.Properties['certificateTemplates'] | ForEach-Object { "$_" }) } if ($PSBoundParameters.ContainsKey('AlsoPublishTemplate') -and $published -notcontains $AlsoPublishTemplate) { $caEntry.PutEx(3, 'certificateTemplates', @($AlsoPublishTemplate)) $caEntry.SetInfo() } $caEntry.Dispose() # 5. Record the clone in the state file. Add-AGDeployStateClone -State $state -Clone ([pscustomobject]@{ Scenario = $Scenario Cn = $cloneCn Oid = $cloneResult.Oid CompanionOidObjectDN = $cloneResult.CompanionOidObjectDN PostSetupSddl = $postSetupSddl }) Save-AGDeployState -State $state -Path $StatePath Write-Output ([pscustomobject]@{ CloneCn = $cloneCn Oid = $cloneResult.Oid CompanionOidObjectDN = $cloneResult.CompanionOidObjectDN }) } } |