Public/Assert-SqlAccountStandard.ps1

# =============================================================================
# Script : Assert-SqlAccountStandard.ps1
# Author : Keith Ramsey
# =============================================================================
# Change Log
# -----------------------------------------------------------------------------
# 2026-05-09 Keith Ramsey Phase 2 release polish - DR-202 standard header applied.
# =============================================================================
function Assert-SqlAccountStandard {
    <#
    .SYNOPSIS
        Validates an Active Directory account against a named compliance policy.
    .DESCRIPTION
        Looks up the account in AD and asserts three policy invariants:
          - ObjectClass matches the policy's Type (user, computer, gMSA).
          - SamAccountName starts with the policy's Prefix when one is defined.
          - DistinguishedName lives under the policy's OU.

        Throws on first failure with all violations joined into the exception message.
        Returns $true on full compliance. Designed to be called as a pre-flight
        check inside Start-SqlSpnConfiguration; can also be called standalone for
        ad-hoc validation.
    .PARAMETER SamAccountName
        SAM account name to validate (e.g., svc_sql_prod or SQLFCI01$).
    .PARAMETER PolicyName
        Policy key from Get-SqlAccountPolicy (e.g., Std_Engine, Std_FCI_VNN, Std_SSAS, Std_Engine_gMSA).
    .EXAMPLE
        Assert-SqlAccountStandard -SamAccountName svc_sql_prod -PolicyName Std_Engine
    .EXAMPLE
        Resolve-SqlPolicyFromContext -Scenario FCI -Role Engine | ForEach-Object {
            Assert-SqlAccountStandard -SamAccountName 'SQLFCI01$' -PolicyName $_
        }
    .OUTPUTS
        [bool]. Returns $true on success. Throws a terminating error on any compliance violation.
    .NOTES
        The policy table lives in Get-SqlAccountPolicy. Adding a new role/scenario
        requires extending that table. Phase 0 ships with Std_Engine, Std_Engine_gMSA,
        Std_FCI_VNN, and Std_SSAS; other policies are Phase 1 work.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)][string]$SamAccountName,
        [Parameter(Mandatory=$true)][string]$PolicyName
    )

    $policy   = Get-SqlAccountPolicy -PolicyName $PolicyName
    if (-not $policy) { throw "Unknown policy [$PolicyName]. See Get-SqlAccountPolicy for valid keys." }

    $adObject = Get-ADObject -Filter "SamAccountName -eq '$SamAccountName'" -Properties DistinguishedName, ObjectClass
    if (-not $adObject) { throw "Account [$SamAccountName] not found in Active Directory." }

    $failures = @()
    if ($adObject.ObjectClass -ne $policy.Type) {
        $failures += "TYPE: Expected $($policy.Type), got $($adObject.ObjectClass)"
    }
    if ($policy.Prefix -and -not $SamAccountName.StartsWith($policy.Prefix)) {
        $failures += "PREFIX: Missing required prefix '$($policy.Prefix)'"
    }
    if ($adObject.DistinguishedName -notmatch [regex]::Escape($policy.OU)) {
        $failures += "OU: Account is not located under '$($policy.OU)'"
    }

    if ($failures.Count -gt 0) {
        throw "Compliance Failure for [$SamAccountName] under policy [$PolicyName]: $($failures -join ' | ')"
    }
    return $true
}