Private/Assert-HDTBootstrapRuleDocument.ps1

function Assert-HDTBootstrapRuleDocument {
    <#
        .SYNOPSIS
            Holds a bootstrap rules document to what can be decided before there
            is a share.
 
        .DESCRIPTION
            THE VOCABULARY, AND NOTHING ELSE. Import-HDTRuleDocument has already
            proved the grammar; this proves that every rule sets something a
            machine with no share, no workspace and no task sequence can act on.
 
            WHY AN ALLOW-LIST AND NOT A WARNING. A set: this file cannot act on
            does nothing, silently, on a machine at three in the morning with
            nobody watching - and the administrator's evidence is a deployment
            that used the wrong computer name. Refusing at author time, naming
            the file that CAN set it, is the whole difference.
 
            THE FOUR, AND THEY ARE THE SHARE AND THE WAY INTO IT:
 
              HDTDeployRoot the reason this file exists
              HDTUserId the account to open it with - MDT's UserID,
              HDTUserDomain UserDomain and UserPassword, which
              HDTUserPassword Bootstrap.ini has always carried
 
            THREE MORE USED TO BE HERE AND THE ORDER OF Start-HDTDeployment
            SAYS THEY NEVER BELONGED:
 
              step 6 the address loop, the gather, AND the Welcome screen
              step 6b this file
              step 10a the technician wizard
 
            HDTKeyboardLocale and HDTUILanguage were justified as "the wizard
            needs both before it can draw" - but the Welcome screen has ALREADY
            been drawn by the time this file is read, so a locale set here
            reaches nothing it was meant to.
 
            HDTSkipWizard is the TECHNICIAN wizard, which runs at 10a with the
            share connected and rules.yaml readable - so rules.yaml is its home,
            exactly as SkipWizard lives in CustomSettings.ini rather than in
            Bootstrap.ini. MDT's Bootstrap.ini carries SkipBDDWelcome, which is
            the WELCOME screen, and HDT's equivalents - HDTSkipWelcome,
            HDTSkipStaticIp, HDTSkipDeployRoot, HDTSkipCredential - come from
            bootstrap.json through Get-HDTWizardSkip, because that screen runs
            before there is anything to resolve a rule against.
 
            THE ACCOUNT IS ON THE LIST BECAUSE THE SHARE IS. One boot image
            serving many sites needs an account per site as much as a share per
            site: a rule that chose \SERVER-B\Share and left SERVER-A's
            account behind has chosen a share it cannot open.
 
            THE PASSWORD IS CLEAR TEXT HERE, exactly as it was in Bootstrap.ini.
            The file is copied INTO the boot image, and anybody holding that
            image already holds the credential baked into it - so the secret is
            not made more available by being readable, only more obvious.
 
            THE DOMAIN JOIN ACCOUNT IS STILL REFUSED. HDTDomainAdminPassword
            joins a machine to a domain long after the share is open, and
            belongs in rules.yaml ON the share, which is not carried around
            inside an image.
 
        .PARAMETER Document
            What Import-HDTRuleDocument returned.
 
        .PARAMETER Path
            The file, for the message.
 
        .OUTPUTS
            None. Throws a terminating error naming the file, the variable and
            the ones it would have taken.
    #>

    [CmdletBinding()]
    [OutputType([void])]
    param(
        [Parameter(Mandatory = $true, Position = 0)]
        [AllowNull()]
        [object] $Document,

        [Parameter(Mandatory = $true, Position = 1)]
        [ValidateNotNullOrEmpty()]
        [string] $Path
    )

    Set-StrictMode -Version Latest
    $ErrorActionPreference = 'Stop'

    if ($null -eq $Document) { return }

    # WHAT A RULE MAY SET BEFORE THERE IS A SHARE - the share, and the account
    # that opens it. MDT's Bootstrap.ini carries both for the same reason: one
    # boot image serving many sites needs an account per site as much as a share
    # per site, and a rule that chose SERVER-B while leaving SERVER-A's account
    # behind has chosen a share it cannot open. The password is clear text here,
    # as it was there.
    #
    # NOTHING ELSE. See the header for why the three that used to be here were
    # never early enough to matter.
    $allowed = @('HDTDeployRoot', 'HDTUserId', 'HDTUserDomain', 'HDTUserPassword')

    foreach ($rule in @($Document.Rule)) {

        # A SCRIPT RULE NAMES A PATH UNDER Scripts\ ON THE SHARE, and there is
        # no share yet. Refused here rather than discovered as a file-not-found
        # on a machine that has not connected to anything.
        if ($null -ne $rule.PSObject.Properties['SetFrom'] -and
            -not [string]::IsNullOrWhiteSpace([string] $rule.SetFrom)) {

            throw ("{0}: rule '{1}' uses setFrom, and a bootstrap rule cannot. setFrom names a script under Scripts\ on the deployment share, which has not been connected to when this file is read. Decide it with when: here, or move the rule to rules.yaml where the script exists." -f
                $Path, [string] $rule.Name)
        }

        if ($null -eq $rule.PSObject.Properties['Set'] -or $null -eq $rule.Set) { continue }

        foreach ($variable in @($rule.Set.Keys)) {
            $name = [string] $variable

            # A DEPLOY ROOT WITH ONE LEADING BACKSLASH IS NOT A UNC, and it is
            # the mistake this file attracts - found on the lab share, written
            # by hand. YAML is not the culprit: a plain scalar keeps both
            # slashes, and the double-quoted form throws rather than eating one.
            # So it is a typo nothing else would notice until WinPE had already
            # been handed a path it cannot resolve, on a machine nobody is
            # watching.
            if ([string]::Equals($name, 'HDTDeployRoot', [System.StringComparison]::OrdinalIgnoreCase)) {

                $root = [string] $rule.Set[$variable]

                if ($root -match '^\\[^\\]') {
                    throw ("{0}: rule '{1}' sets HDTDeployRoot to '{2}', which starts with ONE backslash. A UNC path needs two: '\\{3}'. Nothing else would notice - WinPE would be handed a path it cannot resolve, on a machine nobody is watching." -f
                        $Path, [string] $rule.Name, $root, $root.TrimStart('\'))
                }
            }

            if ($allowed -contains $name) { continue }

            # A PASSWORD THAT IS NOT THE SHARE'S GETS ITS OWN SENTENCE.
            # HDTUserPassword is allowed above - it is the account this file
            # exists to choose. Anything else with Password in its name is a
            # secret used AFTER the share is open, most often the domain join
            # account, and "not allowed" would send somebody looking for the
            # right spelling rather than the right file.
            if ($name -like '*Password*' -or $name -like '*Credential*') {
                throw ("{0}: rule '{1}' sets '{2}', which is used after the deployment share is connected. This file is read before there is one, and the only credential it may carry is the share's own - HDTUserId, HDTUserDomain and HDTUserPassword. Put '{2}' in rules.yaml on the share." -f
                    $Path, [string] $rule.Name, $name)
            }

            throw ("{0}: rule '{1}' sets '{2}', which is decided after the deployment share is connected. This file is read before there is one, so it may set only: {3}. Move the rule to rules.yaml on the share." -f
                $Path, [string] $rule.Name, $name, ($allowed -join ', '))
        }
    }
}