Private/Test-SqlAdPermission.ps1
|
# ============================================================================= # Script : Test-SqlAdPermission.ps1 # Author : Keith Ramsey # ============================================================================= # Change Log # ----------------------------------------------------------------------------- # 2026-05-09 Keith Ramsey Phase 2 release polish - DR-202 standard header applied. # ============================================================================= function Test-SqlAdPermission { <# .SYNOPSIS Pre-flight check: does the calling user have rights to write SPNs on the target AD object? .DESCRIPTION Reads the nTSecurityDescriptor on the supplied DistinguishedName and walks the DACL looking for an access rule that grants the calling identity (or one of its group memberships) the rights needed to register an SPN. The operation that matters for setspn -S is the "Validated write to service principal name" extended right (objectGuid f3a64788-5306-11d1-a9c5-0000f80367c1). Older / privileged accounts may instead carry a generic WriteProperty allow on the servicePrincipalName attribute or the broader 'GenericAll' / 'WriteProperty' with InheritedObjectType matching the SPN attribute. Returns a structured result rather than throwing so the caller can decide whether to proceed (e.g., proceed-with-warning or hard-stop). Per BTRD �6. .PARAMETER DistinguishedName DN of the target object (the account that will own the SPN). .OUTPUTS PSCustomObject with Allowed (bool), Reason (string), and Method (string) describing how the determination was made. .NOTES This is a best-effort static check. Some AD environments use SACLs or delegation patterns that this function will not detect; treat a 'true' result as 'no obvious denial', not a guarantee of success. #> [CmdletBinding()] param([Parameter(Mandatory=$true)][string]$DistinguishedName) $validatedWriteSpnGuid = [guid]'f3a64788-5306-11d1-a9c5-0000f80367c1' try { $obj = Get-ADObject -Identity $DistinguishedName -Properties nTSecurityDescriptor -ErrorAction Stop } catch { return [PSCustomObject]@{ Allowed = $false Reason = "Cannot read nTSecurityDescriptor on [$DistinguishedName]: $($_.Exception.Message)" Method = 'sd-read-failed' } } $sd = $obj.nTSecurityDescriptor if (-not $sd) { return [PSCustomObject]@{ Allowed = $false Reason = 'Object returned no security descriptor.' Method = 'sd-empty' } } try { $currentIdentity = [System.Security.Principal.WindowsIdentity]::GetCurrent() $callerSids = @($currentIdentity.User) + @($currentIdentity.Groups) } catch { return [PSCustomObject]@{ Allowed = $false Reason = "Unable to resolve caller identity: $($_.Exception.Message)" Method = 'identity-failed' } } foreach ($ace in $sd.Access) { if ($ace.AccessControlType -ne 'Allow') { continue } $sid = try { ($ace.IdentityReference).Translate([System.Security.Principal.SecurityIdentifier]) } catch { $null } if (-not $sid) { continue } if (-not ($callerSids | Where-Object { $_.Value -eq $sid.Value })) { continue } $rights = [string]$ace.ActiveDirectoryRights if ($rights -match 'GenericAll|WriteProperty|Self') { if ($ace.ObjectType -eq $validatedWriteSpnGuid) { return [PSCustomObject]@{ Allowed = $true Reason = 'Caller has Validated-Write-To-Service-Principal-Name extended right.' Method = 'extended-right' } } if ($rights -match 'GenericAll') { return [PSCustomObject]@{ Allowed = $true Reason = 'Caller has GenericAll on the target object.' Method = 'generic-all' } } if ($rights -match 'WriteProperty' -and ($ace.ObjectType -eq [guid]::Empty -or $ace.ObjectType -eq $validatedWriteSpnGuid)) { return [PSCustomObject]@{ Allowed = $true Reason = 'Caller has WriteProperty rights covering the SPN attribute.' Method = 'write-property' } } } } return [PSCustomObject]@{ Allowed = $false Reason = "Caller has no Allow ACE granting Validated-Write-SPN, GenericAll, or WriteProperty on [$DistinguishedName]." Method = 'no-matching-ace' } } |