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'
    }
}