Public/New-SqlSpnPlan.ps1
|
# ============================================================================= # Script : New-SqlSpnPlan.ps1 # Author : Keith Ramsey # ============================================================================= # Change Log # ----------------------------------------------------------------------------- # 2026-05-09 Keith Ramsey Phase 2 release polish - DR-202 standard header applied. # 2026-05-13 Keith Ramsey Document v1.3.0 role + scenario coverage funnel in # .NOTES: planner ValidateSet is 12 (BTRD contract), # CFG exposes 5, wizard hardcodes Engine; MSDTC is # accepted but emits MSSQLSvc/ (Phase 0 gap). # 2026-05-14 Keith Ramsey Add AccountName (sAMAccountName) to the plan. setspn # cannot resolve an account by DistinguishedName; the # engine must pass the sAMAccountName. AccountDn is kept # for the permission preflight. Surfaced by lab testing. # 2026-05-21 Keith Ramsey DR-309 v1 surface narrowing: -Role ValidateSet # reduced to Engine,Agent. SSAS/SSRS/SSIS/FTS/PolyBase/ # Browser/ReplayClient/ReplayController/QueryStore/ # VSSWriter removed from the PUBLIC surface for v1 # (internal Get-SqlRoleMetadata table unchanged - they # are simply not user-reachable in v1). MSDTC removed # from the Scenario surface via Get-SqlSpnInfrastructure # (this function's Scenario comes from infrastructure). # ============================================================================= function New-SqlSpnPlan { <# .SYNOPSIS Builds an SPN registration plan from a verified account, resolved infrastructure, and a role. .DESCRIPTION Composes the canonical plan object that the rest of the module consumes: { PlanGuid, AccountDn, ProposedSpns, Role, Scenario, TargetDomain, CrossForest } The service class for each SPN comes from Get-SqlRoleMetadata. For FCI Engine registrations, if the supplied VerifiedAccount is not a computer object the function attempts to resolve the cluster Virtual Computer Object (VCO) from the infrastructure's virtual name; on success a warning is emitted and the plan targets the VCO. On lookup failure the caller is instructed to pass an explicit -VirtualComputerAccount. Returns $null for roles that do not require an SPN (e.g., Agent under most configurations). .PARAMETER VerifiedAccount Output of Get-SqlSpnAccount. Must carry DistinguishedName and ObjectClass. .PARAMETER Infrastructure Output of Get-SqlSpnInfrastructure. Must carry NetBIOS, FQDN, Port, Scenario, InstanceName, TargetDomain, CrossForest. .PARAMETER Role SQL service role being registered. .PARAMETER VirtualComputerAccount Optional explicit override for FCI: the verified Cluster Virtual Computer Account (output of Get-SqlSpnAccount) to register against. Skips auto-resolution. .EXAMPLE $acct = Get-SqlSpnAccount -SamAccountName 'svc_sql_prod' $infra = Get-SqlSpnInfrastructure -Scenario Standalone -TargetName SQLSRV01 $plan = New-SqlSpnPlan -VerifiedAccount $acct -Infrastructure $infra -Role Engine .OUTPUTS PSCustomObject with PlanGuid, AccountDn, ProposedSpns, Role, Scenario, TargetDomain, CrossForest. .NOTES Cross-forest detection comes from Get-SqlSpnInfrastructure; this function propagates TargetDomain so Invoke-SqlSpnExecutionEngine can pass setspn -T. Role coverage (v1.4.0, DR-309 + DR-311): Public surface narrowed to the proven Engine core. The ValidateSet exposes only Engine and Agent (Agent's RequireSpn=$false; returns $null as a documented no-op). SSAS, SSRS, PBIRS, Browser, and the remaining BTRD roles are deferred as named, demand-sequenced, prove-before-expose post-v1 expansions. The internal Get-SqlRoleMetadata table retains entries; they are simply not user-reachable in v1. Scenario coverage (v1.4.0, DR-309): Standalone, AlwaysOn, and FCI - all three lab-proven end-to-end on a real domain (Waves 1-3, 2026-05-17). MSDTC removed from the public Scenario surface; the prior MSDTC=>MSSQLSvc/ pinning test is inverted to assert MSDTC is NOT publicly accepted in v1. #> [CmdletBinding()] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'New-SqlSpnPlan is a pure builder - returns a PSCustomObject describing proposed registrations. State change happens later in Add-SqlSpn / Invoke-SqlSpnExecutionEngine.')] param( [Parameter(Mandatory=$true)][PSCustomObject]$VerifiedAccount, [Parameter(Mandatory=$true)][PSCustomObject]$Infrastructure, [Parameter(Mandatory=$true)][ValidateSet('Engine','Agent')][string]$Role, [PSCustomObject]$VirtualComputerAccount ) $meta = Get-SqlRoleMetadata -Role $Role if (-not $meta.RequireSpn) { Write-Verbose "Role $Role does not require SPN registration." return $null } $accountForPlan = $VerifiedAccount if ($Infrastructure.Scenario -eq 'FCI' -and $Role -eq 'Engine') { if ($VirtualComputerAccount) { if ($VirtualComputerAccount.ObjectClass -ne 'computer') { throw "VirtualComputerAccount [$($VirtualComputerAccount.Name)] must be a computer object (got ObjectClass: $($VirtualComputerAccount.ObjectClass))." } $accountForPlan = $VirtualComputerAccount } elseif ($VerifiedAccount.ObjectClass -ne 'computer') { $resolved = Resolve-SqlSpnFciCno -VirtualName $Infrastructure.NetBIOS if (-not $resolved.Found) { throw "FCI Engine SPNs must be assigned to the Cluster Virtual Computer Account. Auto-resolution of [$($Infrastructure.NetBIOS)] failed: $($resolved.Reason). Re-run with -VirtualComputerAccount (Get-SqlSpnAccount -SamAccountName <virtual-name>$)." } Write-Warning "Redirecting FCI Engine SPN to cluster computer account [$($resolved.SamAccountName)] (auto-resolved from virtual name)." $accountForPlan = [PSCustomObject]@{ Name = $resolved.SamAccountName DistinguishedName = $resolved.DistinguishedName ObjectClass = 'computer' } } } $spns = @() $spns += "$($meta.ServiceClass)/$($Infrastructure.NetBIOS):$($Infrastructure.Port)" $spns += "$($meta.ServiceClass)/$($Infrastructure.FQDN):$($Infrastructure.Port)" if ($Infrastructure.InstanceName -eq 'MSSQLSERVER') { $spns += "$($meta.ServiceClass)/$($Infrastructure.NetBIOS)" $spns += "$($meta.ServiceClass)/$($Infrastructure.FQDN)" } return [PSCustomObject]@{ PlanGuid = [guid]::NewGuid() AccountDn = $accountForPlan.DistinguishedName AccountName = $accountForPlan.Name ProposedSpns = $spns | Select-Object -Unique Role = $Role Scenario = $Infrastructure.Scenario TargetDomain = $Infrastructure.TargetDomain CrossForest = $Infrastructure.CrossForest } } |