0.1/TierLevelIsolation.psm1

# SPDX-License-Identifier: Apache-2.0

<#
Module Info
 
Author: Andreas Lucas [MSFT]
 
 
Module Name: TierLevelIsolation
Module Version: 0.1.20261003.1
Module GUID: 32c51271-3735-4b61-b80f-7284dafe6c77
Module Description: Manages the shared configuration for Kerberos Authentication Policy based
Tier Level isolation in an Active Directory forest.
 
Architecture:
    The module stores its configuration as JSON in the forest-root domain SYSVOL path
    \\<DNSRoot>\SYSVOL\<DNSRoot>\scripts\TierLevelIsolation.config. Exported commands read,
    validate, modify, and persist individual configuration properties. Active Directory is the
    source of truth when domains, groups, organizational units, and authentication policies are
    resolved.
 
Configuration properties:
    Tier0ComputerPath / Tier1ComputerPath
        Organizational units containing managed computers.
    Tier0ComputerGroup / Tier1ComputerGroup
        Server groups referenced by Kerberos authentication policies.
    Tier0ServiceAccountPath / Tier1ServiceAccountPath
        Organizational units containing managed service accounts.
    Tier0UsersPath / Tier1UsersPath
        Organizational units containing managed administrative users.
    T0KerbAuthPolName / T1KerbAuthPolName
        Kerberos authentication policy names assigned to each tier.
    Domains
        Forest domains included in management operations.
    scope
        Enabled scope: Tier-0, Tier-1, or All-Tiers.
    ProtectedUsers
        Tiers whose users are maintained in the Protected Users group.
    PrivilegedGroupsCleanUp
        Controls cleanup of incompatible privileged-group memberships.
    LogPath
        Optional runtime log directory used by the management scripts.
    Tier0Groups / Tier1Groups
        Additional managed groups stored by immutable Active Directory SID.
 
Prerequisites and side effects:
    - Windows PowerShell 5.1 or later and the ActiveDirectory module.
    - Importing the module queries the current Active Directory domain.
    - The default configuration path requires read/write access to SYSVOL.
    - Set, Add, and Remove commands immediately persist the complete JSON configuration.
 
Version History:
    0.1.20250315 - Initial version
    0.1.20250327 - Added functions to manage the configuration of TierLevelIsolation, bug fixing
                 - Add-TierLevelIsolationDomain support an array as import parameter
    0.1.20250331 - Change the parameter from Path to OU on Add-TierLevelIsolationComputerPath, Add-TierLevelIsolationUserPath, Add-TierLevelIsolationServiceAccountPath to clarify that it is an Organizational Unit (OU) path.
                 - Added validation to check if the specified OU exists in Active Directory before adding it to the configuration.
                 - Added error handling for invalid inputs and exceptions when retrieving OUs or groups from Active Directory.
    0.1.20250423 - Added function to set the DebugLog Path to the configuration file.
                 - Added function to get the DebugLog Path from the configuration file.
    Version 0.2.20250428
                - Added the force parameter to the Set-TierLevelIsolationComputerGroup, Set-TierLevelIsolationKerberosAuthenticationPolicy
    Version 0.2.20251219
                - Added functions to add and remove groups to/from Tier0 and Tier1 configurations.
    Version 0.2.20251223
                - Added validation to prevent adding a group to Tier1 if it already exists in Tier0.
                - Supported values in Add-TierLevelIsolationGroup are now in NetBIOS format (DOMAIN\GroupName), UPN (GroupName@DNSName) and canonical name (DNSName/GroupName).
    Version 0.1.20260825.1
                - Fixed persistence of the debug log path.
    Version 0.1.20260825.2
                - Store additional Tier 0 and Tier 1 groups by SID.
                - Added a command to display configured additional groups.
    Version 0.1.20260828.1
                - Added complete module-level and function-level code documentation.
    Version 0.1.20260828.2
                - Added maintainer-focused inline documentation for implementation logic.
    Version 0.1.20261003.1
                - Relicensed the project under Apache License 2.0 and moved the disclaimer to README.md.
 
#>


#region Module initialization
# Resolve the forest-root DNS name at import time and derive the default shared configuration file.
# Individual functions accept configFile to support testing or an alternate configuration location.
#endregion

#region Global variables
$Global:DnsRoot = (Get-ADDomain).DNSRoot
$global:configFile = "\\$DNSRoot\SYSVOL\$DNSRoot\scripts\TierLevelIsolation.config"


#endregion

#.SYNOPSIS
# Convert a distinguished name to a DNS domain name
#.DESCRIPTION
# This function converts a distinguished name (DN) to a DNS domain name. It extracts the domain components (dc=) from the DN and returns the DNS domain name.
#.PARAMETER DistinguishedName
# The distinguished name to convert. This can be a full DN or just the domain part (e.g. "DC=contoso,DC=com").
#.EXAMPLE
# ConvertFrom-DN2Dns -DistinguishedName "CN=Users,DC=contoso,DC=com"
# Returns "contoso.com".
#.OUTPUTS
# System.String. DNS name of the matching Active Directory naming context, or $null when the
# distinguished name cannot be resolved.
#.NOTES
# Internal helper. Requires access to the forest configuration partition.
function ConvertFrom-DN2Dns {
    param(
        [Parameter(Mandatory= $true, ValueFromPipeline)]
        [string]$DistinguishedName
    )
    # Keep only the DC components, then resolve the matching naming-context cross-reference.
    $DistinguishedName = [regex]::Match($DistinguishedName,"(dc=[^,]+,)*dc=.+$",[System.Text.RegularExpressions.RegexOptions]::IgnoreCase).Value
    return (Get-ADObject -Filter "nCname -eq '$DistinguishedName'" -Searchbase (Get-ADForest).PartitionsContainer -Properties dnsroot).DnsRoot
}

#.SYNOPSIS
# Reads the Tier Level Isolation configuration.
#.DESCRIPTION
# Creates a configuration object with every supported property and its safe default, then merges
# values from the JSON configuration file when it exists. This permits older configuration files
# to gain newly introduced properties without a migration step. Legacy scope values Tier0 and
# Tier1 are normalized to Tier-0 and Tier-1 in the returned object.
#.PARAMETER configFile
# JSON configuration file to read. The default is the TierLevelIsolation.config file in the
# forest-root domain SYSVOL scripts directory.
#.EXAMPLE
# Get-TierLevelIsolationConfiguration
# Returns the shared forest configuration using the default SYSVOL location.
#.EXAMPLE
# Get-TierLevelIsolationConfiguration -configFile "C:\Temp\TierLevelIsolation.config"
# Returns a configuration from an alternate file, which is useful for testing.
#.OUTPUTS
# System.Management.Automation.PSCustomObject. Contains all documented module configuration
# properties, including defaults for properties absent from the JSON file.
#.NOTES
# The function does not create a missing file. Use a configuration-changing command to persist
# the returned default structure.
function Get-TierLevelIsolationConfiguration {
    param (
        [Parameter(Mandatory = $false, Position = 0)]
        [string]$configFile = $global:configFile
    )
    # Build the complete current schema first. This supplies defaults for new or older files.
    $config = New-Object psobject
    $config | Add-Member -MemberType NoteProperty -Name Tier0ComputerPath       -Value @()
    $config | Add-Member -MemberType NoteProperty -Name Tier1ComputerPath       -Value @()
    $config | Add-Member -MemberType NoteProperty -Name Tier0ComputerGroup      -Value ""
    $config | Add-Member -MemberType NoteProperty -Name Tier1ComputerGroup      -Value ""
    $config | Add-Member -MemberType NoteProperty -Name Tier0ServiceAccountPath -Value @()
    $config | Add-Member -MemberType NoteProperty -Name Tier1ServiceAccountPath -Value @()
    $config | Add-Member -MemberType NoteProperty -Name Tier0UsersPath          -Value @()
    $config | Add-Member -MemberType NoteProperty -Name Tier1UsersPath          -Value @()
    $config | Add-Member -MemberType NoteProperty -Name T0KerbAuthPolName       -Value ""
    $config | Add-Member -MemberType NoteProperty -Name T1KerbAuthPolName       -Value ""
    $config | Add-Member -MemberType NoteProperty -Name Domains                 -Value @()
    $config | Add-Member -MemberType NoteProperty -Name scope                   -Value $null
    $config | Add-Member -MemberType NoteProperty -Name ProtectedUsers          -Value @()
    $config | Add-Member -MemberType NoteProperty -Name PrivilegedGroupsCleanUp -Value $false
    $config | Add-Member -MemberType NoteProperty -Name LogPath                 -Value ""
    $config | Add-Member -MemberType NoteProperty -Name Tier0Groups             -Value @()
    $config | Add-Member -MemberType NoteProperty -Name Tier1Groups             -Value @()

    # Merge only recognized properties so unknown JSON data cannot alter the object shape.
    if (Test-Path $configFile) {
        $CurrentConfig = Get-Content -Path $configFile -Raw | ConvertFrom-Json
        foreach ($configItem in $config.PSObject.Properties) {
            if ($null -ne $CurrentConfig.PSObject.Properties[$configItem.Name]) {
                $config.PSObject.Properties[$configItem.Name].Value = $CurrentConfig.PSObject.Properties[$configItem.Name].Value
            }
        }
    }

    # Normalize legacy internal scope names to the values consumed by current management scripts.
    if ($config.scope -eq "Tier0") {
        $config.scope = "Tier-0"
    } elseif ($config.scope -eq "Tier1") {
        $config.scope = "Tier-1"
    }
    return $config
}

#.SYNOPSIS
# Persists the complete Tier Level Isolation configuration.
#.DESCRIPTION
# Serializes the supplied configuration object as JSON and overwrites the target configuration
# file. All module commands that modify configuration call this internal persistence function.
#.PARAMETER configFile
# Destination JSON file. The default is the shared configuration in SYSVOL.
#.PARAMETER config
# Configuration object to serialize. It is normally obtained from
# Get-TierLevelIsolationConfiguration.
#.EXAMPLE
# $Configuration = Get-TierLevelIsolationConfiguration
# Set-TierLevelIsolationConfiguration -config $Configuration
# Writes the configuration back to the default file.
#.OUTPUTS
# None.
#.NOTES
# Internal helper. Write failures are reported to the host and are not rethrown.
function Set-TierLevelIsolationConfiguration {
    param (
        [Parameter(Mandatory = $false, Position = 0)]
        [string]$configFile = $global:configFile,
        [Parameter(Mandatory = $true, Position = 1)]
        $config
    )
    try {
        # Persist one complete snapshot rather than updating individual JSON fragments.
        $config | ConvertTo-Json | Set-Content -Path $configFile -Force
    } catch {
        Write-Host "Failed to write configuration to file: $configFile $($Error[0])" -ForegroundColor Red
    }
}

#.SYNOPSIS
# Adds a computer organizational-unit path to a tier.
#.DESCRIPTION
# Resolves a full distinguished name against its domain or a relative OU path against the current
# domain. The path is stored once in the selected tier's computer-path collection. A missing OU
# produces a warning but is still stored so installation workflows can define OUs before creating
# them.
#.PARAMETER TierLevel
# Target tier. Valid values are Tier0 and Tier1.
#.PARAMETER OU
# Relative OU path or complete distinguished name to add.
#.PARAMETER configFile
# Configuration file to update. Defaults to the shared SYSVOL configuration.
#.EXAMPLE
# Add-TierLevelIsolationComputerPath -TierLevel Tier0 -OU "OU=Server,OU=Tier 0,OU=Admin"
# Adds a current-domain relative Tier 0 computer path.
#.EXAMPLE
# Add-TierLevelIsolationComputerPath -TierLevel Tier1 -OU "OU=Server,OU=Tier 1,DC=emea,DC=contoso,DC=com"
# Adds a fully qualified Tier 1 computer path.
#.OUTPUTS
# None.
function Add-TierLevelIsolationComputerPath {
    param (
        [Parameter(Mandatory = $true, Position = 0)]
        [ValidateSet("Tier0", "Tier1")]
        [string]$TierLevel,
        [Parameter(Mandatory = $true, Position = 1)]
        $OU,
        [Parameter(Mandatory = $false, Position = 2)]
        [string]$configFile = $global:configFile
    )
    $config = Get-TierLevelIsolationConfiguration $configFile

    # Full DNs are resolved against their own domain; relative paths use the current domain.
    if ($OU -like "*DC=*"){
        $DNSDomain = ConvertFrom-DN2Dns -DistinguishedName $OU
        if ($null -eq $DNSDomain){
            Write-Host "The specified domain does not exist: $OU" -ForegroundColor Yellow
            return
        }
        $oOU = Get-ADOrganizationalUnit -Filter "DistinguishedName -eq '$OU'" -ErrorAction SilentlyContinue -Server $DnsDomain
    } else {
        $oOU = Get-ADOrganizationalUnit -Filter "DistinguishedName -like '$OU,$((Get-ADDomain).DistinguishedName)'" -ErrorAction SilentlyContinue
    }
    if ($null -eq $oOU ) {
        Write-Host "The specified path does not exist: $OU" -ForegroundColor Yellow
    }

    # Append only new values to keep repeated setup runs idempotent.
    switch ($TierLevel) {
        "Tier0" {
            if ($null -eq $config.Tier0ComputerPath) { $config.Tier0ComputerPath = @() }
            if ($config.Tier0ComputerPath -notcontains $OU) { $config.Tier0ComputerPath += $OU }
            break
        }
        "Tier1" {
            if ($null -eq $config.Tier1ComputerPath) { $config.Tier1ComputerPath = @() }
            if ($config.Tier1ComputerPath -notcontains $OU) { $config.Tier1ComputerPath += $OU }
            break
        }
    }
    Set-TierLevelIsolationConfiguration -configFile $configFile -config $config
}

#.SYNOPSIS
# Resolves an Active Directory group identity and domain.
#.DESCRIPTION
# Accepts a SID, DOMAIN\GroupName, GroupName@dns.name, dns.name/GroupName, or an unqualified group
# name. SID lookups search configured domains and the forest-root domain. Other formats determine
# a domain directly; unqualified names use the current domain.
#.PARAMETER Identity
# Group SID or supported name format to resolve.
#.PARAMETER Config
# Tier Level Isolation configuration whose Domains property supplies the SID search scope.
#.EXAMPLE
# Resolve-TierLevelIsolationGroup -Identity "CONTOSO\Helpdesk" -Config $Configuration
# Resolves the Helpdesk group and returns both the AD group and DNS domain.
#.OUTPUTS
# System.Management.Automation.PSCustomObject with Group and Domain properties.
#.NOTES
# Internal helper. Throws when the group or domain cannot be resolved.
function Resolve-TierLevelIsolationGroup {
    param (
        [Parameter(Mandatory = $true)]
        [string]$Identity,
        [Parameter(Mandatory = $true)]
        $Config
    )
    # SIDs do not encode a DNS domain. Search configured domains and finally the forest root.
    if ($Identity -match '^S-1-(?:\d+-){1,14}\d+$') {
        $Domains = @($Config.Domains) + @($Global:DnsRoot) | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique
        foreach ($Domain in $Domains) {
            try {
                $Group = Get-ADGroup -Identity $Identity -Server $Domain -ErrorAction Stop
                return [pscustomobject]@{ Group = $Group; Domain = $Domain }
            } catch [Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException] {
                continue
            } catch [Microsoft.ActiveDirectory.Management.ADServerDownException] {
                continue
            }
        }
        throw "The specified group SID '$Identity' could not be found in a configured domain."
    }

    # Parse supported qualified-name formats and derive the DNS domain used for the lookup.
    $GroupIdentity = $Identity
    switch -regex ($Identity) {
        '^(.+?)\\(.+)$' {
            $DomainNetBios = $matches[1]
            $GroupIdentity = $matches[2]
            $DomainDNSName = (Get-ADObject -SearchBase "CN=Partitions,$((Get-ADRootDSE).configurationNamingContext)" -LDAPFilter "(&(objectClass=crossRef)(nETBIOSName=$DomainNetBios))" -Properties dnsRoot -ErrorAction Stop).dnsRoot[0]
            break
        }
        '^(.+)@(.+)$' { $GroupIdentity = $matches[1]; $DomainDNSName = $matches[2]; break }
        '^([^/]+)/(.+)$' { $DomainDNSName = $matches[1]; $GroupIdentity = $matches[2]; break }
        default { $DomainDNSName = $Global:DnsRoot }
    }

    # Return the AD object together with its resolved domain for consistent caller output.
    $Group = Get-ADGroup -Identity $GroupIdentity -Server $DomainDNSName -ErrorAction Stop
    return [pscustomobject]@{ Group = $Group; Domain = $DomainDNSName }
}

#.SYNOPSIS
# Adds an additional Active Directory group to a tier.
#.DESCRIPTION
# Resolves the supplied group and stores its immutable SID in Tier0Groups or Tier1Groups. A group
# cannot be assigned to both tiers. Repeated additions are idempotent.
#.PARAMETER TierLevel
# Target tier. Valid values are Tier0 and Tier1.
#.PARAMETER GroupIdentity
# Group SID, DOMAIN\GroupName, GroupName@dns.name, dns.name/GroupName, or unqualified group name.
# Accepts pipeline input and the aliases GroupName and GroupSID.
#.PARAMETER configFile
# Configuration file to update. Defaults to the shared SYSVOL configuration.
#.EXAMPLE
# Add-TierLevelIsolationGroup -TierLevel Tier0 -GroupIdentity "CONTOSO\Domain Admins"
# Resolves the group and stores its SID in the Tier 0 group collection.
#.EXAMPLE
# "S-1-5-21-1000-1000-1000-1234" | Add-TierLevelIsolationGroup -TierLevel Tier1
# Adds a group by SID through the pipeline.
#.OUTPUTS
# None.
#.NOTES
# Resolution or cross-tier assignment failures are reported to the host and not rethrown.
function Add-TierLevelIsolationGroup {
    param (
        [Parameter(Mandatory = $true, Position = 0)]
        [ValidateSet("Tier0", "Tier1")]
        [string]$TierLevel,
        [Parameter(Mandatory = $true, Position = 1, ValueFromPipeline = $true)]
        [Alias("GroupName", "GroupSID")]
        [string]$GroupIdentity,
        [Parameter(Mandatory = $false, Position = 2)]
        [string]$configFile = $global:configFile
    )
    process {
        try {
            # Resolve names once and persist the immutable SID rather than a renameable group name.
            $config = Get-TierLevelIsolationConfiguration $configFile
            $ResolvedGroup = Resolve-TierLevelIsolationGroup -Identity $GroupIdentity -Config $config
            $GroupSID = $ResolvedGroup.Group.SID.Value
            $TargetProperty = "${TierLevel}Groups"
            $OtherProperty = if ($TierLevel -eq "Tier0") { "Tier1Groups" } else { "Tier0Groups" }

            # A group must not receive conflicting tier assignments.
            if ($config.$OtherProperty -contains $GroupSID) {
                Write-Host "The group $($ResolvedGroup.Group.Name) ($GroupSID) is already assigned to the other tier." -ForegroundColor Red
                return
            }
            if ($config.$TargetProperty -notcontains $GroupSID) {
                # Re-wrap the property as an array because single-item JSON arrays can deserialize variably.
                $config.$TargetProperty = @($config.$TargetProperty) + $GroupSID
                Set-TierLevelIsolationConfiguration -configFile $configFile -config $config
            }
        }
        catch {
            Write-Host "The group '$GroupIdentity' could not be added. $($_.Exception.Message)" -ForegroundColor Red
        }
    }
}

#.SYNOPSIS
# Gets additional groups configured for one or both tiers.
#.DESCRIPTION
# Reads stored group SIDs and attempts to resolve each SID in the configured domains. Unresolvable
# SIDs are retained in the output with Resolved set to $false so stale entries remain visible.
#.PARAMETER TierLevel
# Optional tier filter. Valid values are Tier0 and Tier1. Omit it to return both tiers.
#.PARAMETER configFile
# Configuration file to read. Defaults to the shared SYSVOL configuration.
#.EXAMPLE
# Get-TierLevelIsolationGroup
# Returns configured additional groups for Tier 0 and Tier 1.
#.EXAMPLE
# Get-TierLevelIsolationGroup -TierLevel Tier0 | Where-Object Resolved
# Returns only successfully resolved Tier 0 groups.
#.OUTPUTS
# System.Management.Automation.PSCustomObject with TierLevel, SID, Name, SamAccountName, Domain,
# and Resolved properties.
function Get-TierLevelIsolationGroup {
    param (
        [Parameter(Mandatory = $false, Position = 0)]
        [ValidateSet("Tier0", "Tier1")]
        [string]$TierLevel,
        [Parameter(Mandatory = $false, Position = 1)]
        [string]$configFile = $global:configFile
    )
    $config = Get-TierLevelIsolationConfiguration $configFile
    # An omitted tier deliberately expands to both configured group collections.
    $TierLevels = if ($TierLevel) { @($TierLevel) } else { @("Tier0", "Tier1") }
    foreach ($CurrentTier in $TierLevels) {
        foreach ($GroupSID in @($config."${CurrentTier}Groups")) {
            try {
                # Enrich the stored SID with current directory attributes when it still resolves.
                $ResolvedGroup = Resolve-TierLevelIsolationGroup -Identity $GroupSID -Config $config
                [pscustomobject]@{
                    TierLevel = $CurrentTier
                    SID = $GroupSID
                    Name = $ResolvedGroup.Group.Name
                    SamAccountName = $ResolvedGroup.Group.SamAccountName
                    Domain = $ResolvedGroup.Domain
                    Resolved = $true
                }
            }
            catch {
                # Preserve stale SIDs in output so administrators can identify and remove them.
                [pscustomobject]@{
                    TierLevel = $CurrentTier
                    SID = $GroupSID
                    Name = $null
                    SamAccountName = $null
                    Domain = $null
                    Resolved = $false
                }
            }
        }
    }
}

#.SYNOPSIS
# Removes an additional Active Directory group from a tier.
#.DESCRIPTION
# Accepts a stored SID or any group-name format supported by Add-TierLevelIsolationGroup. Names
# are resolved to a SID before the SID is removed from the selected tier collection.
#.PARAMETER TierLevel
# Tier from which to remove the group. Valid values are Tier0 and Tier1.
#.PARAMETER GroupIdentity
# Group SID or supported group name. Accepts pipeline input and the aliases GroupName and GroupSID.
#.PARAMETER configFile
# Configuration file to update. Defaults to the shared SYSVOL configuration.
#.EXAMPLE
# Remove-TierLevelIsolationGroup -TierLevel Tier1 -GroupIdentity "helpdesk@contoso.com"
# Removes the resolved group SID from Tier 1.
#.EXAMPLE
# "S-1-5-21-1000-1000-1000-1234" | Remove-TierLevelIsolationGroup -TierLevel Tier0
# Removes a stored Tier 0 group directly by SID.
#.OUTPUTS
# None.
function Remove-TierLevelIsolationGroup {
    param (
        [Parameter(Mandatory = $true, Position = 0)]
        [ValidateSet("Tier0", "Tier1")]
        [string]$TierLevel,
        [Parameter(Mandatory = $true, Position = 1, ValueFromPipeline = $true)]
        [Alias("GroupName", "GroupSID")]
        [string]$GroupIdentity,
        [Parameter(Mandatory = $false, Position = 2)]
        [string]$configFile = $global:configFile
    )
    process {
        $config = Get-TierLevelIsolationConfiguration $configFile
        $GroupSID = $GroupIdentity

        # Convert supported name formats to the same SID representation used by the configuration.
        if ($GroupIdentity -notmatch '^S-1-(?:\d+-){1,14}\d+$') {
            try {
                $GroupSID = (Resolve-TierLevelIsolationGroup -Identity $GroupIdentity -Config $config).Group.SID.Value
            }
            catch {
                Write-Host "The group '$GroupIdentity' could not be resolved. $($_.Exception.Message)" -ForegroundColor Red
                return
            }
        }
        $TargetProperty = "${TierLevel}Groups"
        if ($config.$TargetProperty -contains $GroupSID) {
            # Filter into a new array to retain predictable JSON array serialization.
            $config.$TargetProperty = @($config.$TargetProperty | Where-Object { $_ -ne $GroupSID })
            Set-TierLevelIsolationConfiguration -configFile $configFile -config $config
        }
    }
}
#.SYNOPSIS
# Removes a computer organizational-unit path from a tier.
#.DESCRIPTION
# Removes an exact relative or fully qualified OU path from the selected tier's computer-path
# collection. No Active Directory organizational unit is deleted.
#.PARAMETER TierLevel
# Tier from which to remove the path. Valid values are Tier0 and Tier1.
#.PARAMETER OU
# Exact OU path value stored in the configuration.
#.PARAMETER configFile
# Configuration file to update. Defaults to the shared SYSVOL configuration.
#.EXAMPLE
# Remove-TierLevelIsolationComputerPath -TierLevel Tier0 -OU "OU=Server,OU=Tier 0,OU=Admin"
# Removes the matching configured path from Tier 0.
#.OUTPUTS
# None.
function Remove-TierLevelIsolationComputerPath {
    param (
        [Parameter(Mandatory = $true, Position = 0)]
        [ValidateSet("Tier0", "Tier1")]
        [string]$TierLevel,
        [Parameter(Mandatory = $true, Position = 1)]
        $OU,
        [Parameter(Mandatory = $false, Position = 2)]
        [string]$configFile = $global:configFile
    )
    # Remove only an exact configured value; this command never changes the directory OU itself.
    $config = Get-TierLevelIsolationConfiguration $configFile 
    switch ($TierLevel) {
        "Tier0" { 
            if ($config.Tier0ComputerPath -contains $OU) {
                $config.Tier0ComputerPath = @($config.Tier0ComputerPath | Where-Object { $_ -ne $OU })
            } else {
                Write-Host "The specified path does not exist in the Tier0 configuration: $OU" -ForegroundColor Yellow
                return
            }
            break
        }
        "Tier1" {
            if ($config.Tier1ComputerPath -contains $OU) {
                $config.Tier1ComputerPath = @($config.Tier1ComputerPath | Where-Object { $_ -ne $OU})
            } else {
                Write-Host "The specified path does not exist in the Tier1 configuration: $OU" -ForegroundColor Yellow
                return
            }
            break
        }
    }
    Set-TierLevelIsolationConfiguration -configFile $configFile -config $config
    return
}
#.SYNOPSIS
# Adds a user organizational-unit path to a tier.
#.DESCRIPTION
# Validates a relative path in the current domain or a full distinguished name in its specified
# domain, then stores it once in the selected tier's user-path collection. A missing OU produces
# a warning but is still stored for installation workflows that create it later.
#.PARAMETER TierLevel
# Target tier. Valid values are Tier0 and Tier1.
#.PARAMETER OU
# Relative OU path or complete distinguished name to add.
#.PARAMETER configFile
# Configuration file to update. Defaults to the shared SYSVOL configuration.
#.EXAMPLE
# Add-TierLevelIsolationUserPath -TierLevel Tier0 -OU "OU=Admins,OU=Tier 0,OU=Admin"
# Adds a current-domain relative user path to Tier 0.
#.EXAMPLE
# Add-TierLevelIsolationUserPath -TierLevel Tier1 -OU "OU=Admins,OU=Tier 1,DC=emea,DC=contoso,DC=com"
# Adds a fully qualified user path to Tier 1.
#.OUTPUTS
# None.
function Add-TierLevelIsolationUserPath {
    param (
        [Parameter(Mandatory = $true, Position = 0)]
        [ValidateSet("Tier0", "Tier1")]
        [string]$TierLevel,
        [Parameter(Mandatory = $true, Position = 1)]
        [string]$OU,
        [Parameter(Mandatory = $false, Position = 2)]
        [string]$configFile = $global:configFile
    )
    # Resolve full DNs in their declared domain and relative paths in the current domain.
    $config = Get-TierLevelIsolationConfiguration $configFile 
    if ($OU -like "*DC=*"){
        $DNSDomain = ConvertFrom-DN2Dns -DistinguishedName $OU
        if ($null -eq $DNSDomain){
            Write-Host "The specified domain does not exist: $OU" -ForegroundColor Yellow
            return
        }
        $Path = Get-ADOrganizationalUnit -Filter "DistinguishedName -eq '$OU'" -ErrorAction SilentlyContinue -Server $DnsDomain
    } else {
        $Path = Get-ADOrganizationalUnit -Filter "DistinguishedName -like '$OU,$((Get-ADDomain).DistinguishedName)'" -ErrorAction SilentlyContinue 
    }
    if ($null -eq $Path) {
        Write-Host "The specified path does not exist: $OU" -ForegroundColor Yellow
    }
    # Store the path once in the selected tier while preserving all existing paths.
    switch ($TierLevel) {
        "Tier0" { 
            if ($null -eq $config.Tier0UsersPath) {
                $config.Tier0UsersPath = @($OU)
            }
            if ($config.Tier0UsersPath -notcontains $OU) {
                $config.Tier0UsersPath += $OU
            }
        }
        "Tier1" {
            if ($null -eq $config.Tier1UsersPath ) {
                $config.Tier1UsersPath = @($OU)
            }
            if ($config.Tier1UsersPath -notcontains $OU) {
                $config.Tier1UsersPath += $OU
            }
        }
    }
    Set-TierLevelIsolationConfiguration -configFile $configFile -config $config
    return
}
#.SYNOPSIS
# Removes a user organizational-unit path from a tier.
#.DESCRIPTION
# Removes an exact OU path from the selected tier's user-path collection. No Active Directory
# organizational unit or user account is deleted.
#.PARAMETER TierLevel
# Tier from which to remove the path. Valid values are Tier0 and Tier1.
#.PARAMETER OU
# Exact OU path value stored in the configuration.
#.PARAMETER configFile
# Configuration file to update. Defaults to the shared SYSVOL configuration.
#.EXAMPLE
# Remove-TierLevelIsolationUserPath -TierLevel Tier0 -OU "OU=Admins,OU=Tier 0,OU=Admin"
# Removes the matching configured path from Tier 0.
#.OUTPUTS
# None.
function Remove-TierLevelIsolationUserPath {
    param (
        [Parameter(Mandatory = $true, Position = 0)]
        [ValidateSet("Tier0", "Tier1")]
        [string]$TierLevel,
        [Parameter(Mandatory = $true, Position = 1)]
        [string]$OU,
        [Parameter(Mandatory = $false, Position = 2)]
        [string]$configFile = $global:configFile
    )
    # Configuration removal is intentionally independent of current OU existence in AD.
    $config = Get-TierLevelIsolationConfiguration $configFile 
    switch ($TierLevel) {
        "Tier0" { 
            if ($config.Tier0UsersPath -contains $OU) {
                $config.Tier0UsersPath = @($config.Tier0UsersPath | Where-Object {$_ -ne $OU})
            }
        }
        "Tier1" {
            if ($config.Tier1UsersPath -contains $OU){
                $config.Tier1UsersPath =@($config.Tier1UsersPath | Where-Object {$_ -ne $OU})
            }
        }
    }
    Set-TierLevelIsolationConfiguration -configFile $configFile -config $config
    return
}
#.SYNOPSIS
# Sets the Kerberos authentication policy name for a tier.
#.DESCRIPTION
# Validates that the policy exists in Active Directory and stores its name for the selected tier.
# Force bypasses existence validation so an installer can configure the name before creating the
# policy.
#.PARAMETER TierLevel
# Target tier. Valid values are Tier0 and Tier1.
#.PARAMETER KerberosPolicyName
# Authentication policy name to store.
#.PARAMETER configFile
# Configuration file to update. Defaults to the shared SYSVOL configuration.
#.PARAMETER Force
# Stores the name without requiring an existing Active Directory authentication policy.
#.EXAMPLE
# Set-TierLevelIsolationKerberosAuthenticationPolicy -TierLevel Tier0 -KerberosPolicyName "Tier 0 restriction"
# Validates and stores an existing Tier 0 policy.
#.EXAMPLE
# Set-TierLevelIsolationKerberosAuthenticationPolicy -TierLevel Tier1 -KerberosPolicyName "Tier 1 restriction" -Force
# Stores a policy name before the policy is created.
#.OUTPUTS
# None.
function Set-TierLevelIsolationKerberosAuthenticationPolicy{
    param (
        [Parameter(Mandatory = $true, Position = 0)]
        [ValidateSet("Tier0", "Tier1")]
        [string]$TierLevel,
        [Parameter(Mandatory = $true, Position = 1)]
        $KerberosPolicyName,
        [Parameter(Mandatory = $false, Position = 2)]
        [string]$configFile = $global:configFile,
        [Parameter(Mandatory = $false, Position = 3)]
        [switch]$Force
    )
    # Normal administration validates the policy; installation can use Force before policy creation.
    $config = Get-TierLevelIsolationConfiguration $configFile
    if ($Force.IsPresent -eq $false){
        $KerbAuthPol = Get-ADAuthenticationPolicy -Filter "Name -eq '$KerberosPolicyName'" -ErrorAction SilentlyContinue 
        if ($null -eq $KerbAuthPol){
            Write-Host "The specified Kerberos Authentication Policy does not exist: $KerberosPolicyName" -ForegroundColor Red
            return
        }
    }
    # Each tier has a dedicated configuration property consumed by the user-management script.
    switch ($TierLevel) {
        "Tier0" { 
            $config.T0KerbAuthPolName = $KerberosPolicyName
        }
        "Tier1" {
            $config.T1KerbAuthPolName = $KerberosPolicyName
        }
    }
    Set-TierLevelIsolationConfiguration -configFile $configFile -config $config
    return
}
#.SYNOPSIS
# Adds a forest domain to the configuration.
#.DESCRIPTION
# Verifies that each pipeline value belongs to the current forest and stores the DNS name once.
#.PARAMETER Domain
# DNS name of the forest domain to add. Accepts pipeline input.
#.PARAMETER configFile
# Configuration file to update. Defaults to the shared SYSVOL configuration.
#.EXAMPLE
# "contoso.com","emea.contoso.com" | Add-TierLevelIsolationDomain
# Adds two existing forest domains.
#.OUTPUTS
# None.
function Add-TierLevelIsolationDomain{
    param (
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
        [string]$Domain,
        [Parameter(Mandatory = $false, Position = 1)]
        [string]$configFile  = $global:configFile
    )
    process{
        $config = Get-TierLevelIsolationConfiguration $configFile 

        # Restrict entries to DNS names advertised by the current forest.
        if ((Get-ADForest).Domains -notcontains $Domain) {
            Write-Host "The specified domain does not exist: $Domain" -ForegroundColor Red
            return
        }
        # Avoid duplicate domain processing during subsequent management runs.
        if ($config.Domains -notcontains $domain) {
            $config.Domains += $domain
            Set-TierLevelIsolationConfiguration    -configFile $configFile -config $config
        }
        return
    }
}
#.SYNOPSIS
# Removes a forest domain from the configuration.
#.DESCRIPTION
# Removes an exact DNS name from the Domains collection. No Active Directory domain is modified.
#.PARAMETER Domain
# The domain to remove from the configuration.
#.PARAMETER configFile
# Configuration file to update. Defaults to the shared SYSVOL configuration.
#.EXAMPLE
# Remove-TierLevelIsolationDomain -Domain "emea.contoso.com"
# Removes the domain from the managed-domain list.
#.OUTPUTS
# None.
function Remove-TierLevelIsolationDomain{
    param (
        [Parameter(Mandatory = $true, Position = 0)]
        [string]$Domain,
        [Parameter(Mandatory = $false, Position = 1)]
        [string]$configFile = $global:configFile
    )
    # Remove only the configuration reference; no domain or trust is modified.
    $config = Get-TierLevelIsolationConfiguration $configFile 
    if ($config.Domains -contains $domain) {
        $config.Domains = @($config.Domains | Where-Object {$_ -ne $domain})
    }
    Set-TierLevelIsolationConfiguration -configFile $configFile -config $config
    return
}
#.SYNOPSIS
# Sets the enabled Tier Level Isolation scope.
#.DESCRIPTION
# Replaces the configuration scope with Tier0, Tier1, or All-Tiers.
#.PARAMETER scope
# Scope to set. Valid values are All-Tiers, Tier0, and Tier1.
#.PARAMETER configFile
# Configuration file to update. Defaults to the shared SYSVOL configuration.
#.EXAMPLE
# Set-TierLevelIsolationScope -scope All-Tiers
# Enables both tiers in the configuration.
#.OUTPUTS
# None.
function Set-TierLevelIsolationScope{
    param (
        [Parameter(Mandatory = $true, Position = 0)]
        [ValidateSet("All-Tiers", "Tier0", "Tier1")]
        [string]$scope,
        [Parameter(Mandatory = $false, Position = 1)]
        [string]$configFile = $global:configFile
    )
    # Replace the single active scope value and persist it immediately.
    $config = Get-TierLevelIsolationConfiguration $configFile 
    $config.scope = $scope
    Set-TierLevelIsolationConfiguration -configFile $configFile -config $config
    return
}
#.SYNOPSIS
# Selects which tiers participate in Protected Users management.
#.DESCRIPTION
# Stores Tier-0, Tier-1, both tiers, or an empty collection in ProtectedUsers. The scheduled user
# management script uses this setting to maintain Protected Users membership.
#.PARAMETER TierLevel
# Tier selection. Valid values are Tier-0, Tier-1, All-Tiers, and None.
#.PARAMETER configFile
# Configuration file to update. Defaults to the shared SYSVOL configuration.
#.EXAMPLE
# Set-TierLevelProtectedUsersState -TierLevel All-Tiers
# Enables Protected Users management for Tier 0 and Tier 1.
#.EXAMPLE
# Set-TierLevelProtectedUsersState -TierLevel None
# Disables Protected Users management.
#.OUTPUTS
# None.
function Set-TierLevelProtectedUsersState{
    param (
        [Parameter(Mandatory = $true, Position = 0)]
        [ValidateSet("Tier-0", "Tier-1","All-Tiers","None")]
        [string]$TierLevel,
        [Parameter(Mandatory = $false, Position = 1)]
        [string]$configFile = $global:configFile
    )
    # Convert the user-facing selection into the array consumed by account management.
    $config = Get-TierLevelIsolationConfiguration $configFile 
    switch ($TierLevel) {
        "All-Tiers" { 
            $config.ProtectedUsers = @("Tier-0", "Tier-1") 
            break
        }
        "None" { 
            $config.ProtectedUsers = @() 
            break
        }
        Default { 
            $config.ProtectedUsers = @($TierLevel) 
            break
        }
    }
    Set-TierLevelIsolationConfiguration -configFile $configFile -config $config
    return}
#.SYNOPSIS
# Enables or disables privileged-group cleanup.
#.DESCRIPTION
# Controls whether the scheduled management scripts clean up incompatible privileged-group
# memberships for tier-managed users.
#.PARAMETER state
# State to store. Valid values are True and False.
#.PARAMETER configFile
# Configuration file to update. Defaults to the shared SYSVOL configuration.
#.EXAMPLE
# Set-TierLevelPrivilegedGroupsCleanUpState -state True
# Enables privileged-group cleanup.
#.OUTPUTS
# None.
function Set-TierLevelPrivilegedGroupsCleanUpState{
    param (
        [Parameter(Mandatory = $true, Position = 0)]
        [ValidateSet("True", "False")]
        [string]$state,
        [Parameter(Mandatory = $false, Position = 1)]
        [string]$configFile = $global:configFile


    )
    # Store the validated state in the shared configuration for scheduled user management.
    $config = Get-TierLevelIsolationConfiguration $configFile 
    $config.PrivilegedGroupsCleanUp = $state
    Set-TierLevelIsolationConfiguration -configFile $configFile -config $config
    return
}
#.SYNOPSIS
# Sets the server group for a tier.
#.DESCRIPTION
# Resolves the group through a global catalog and stores its name for the selected tier. Force
# bypasses group validation so installation can configure the name before creating the group.
#.PARAMETER TierLevel
# Target tier. Valid values are Tier0 and Tier1.
#.PARAMETER GroupName
# Active Directory group name to store.
#.PARAMETER configFile
# Configuration file to update. Defaults to the shared SYSVOL configuration.
#.PARAMETER Force
# Stores the name without requiring the group to exist.
#.EXAMPLE
# Set-TierLevelIsolationComputerGroup -TierLevel Tier0 -GroupName "Tier 0 server"
# Validates and stores an existing Tier 0 server group.
#.EXAMPLE
# Set-TierLevelIsolationComputerGroup -TierLevel Tier1 -GroupName "Tier 1 server" -Force
# Stores the Tier 1 server group name before group creation.
#.OUTPUTS
# None.
function Set-TierLevelIsolationComputerGroup{
    param (
        [Parameter(Mandatory = $true, Position = 0)]
        [ValidateSet("Tier0", "Tier1")]
        [string]$TierLevel,
        [Parameter(Mandatory = $true, Position = 1)]
        [string]$GroupName,
        [Parameter(Mandatory = $false, Position = 2)]
        [string]$configFile = $global:configFile,
        [Parameter(Mandatory = $false, Position = 3)]
        [switch]$Force
    )
    # Load the current snapshot before validating and replacing one tier-specific property.
    $config = Get-TierLevelIsolationConfiguration $configFile 
    if ($null -eq $GroupName) {
        Write-Host "The specified group name is null or empty." -ForegroundColor Red
        return
    }
    # Query a global catalog so universal groups created elsewhere in the forest can be resolved.
    if ($Force.IsPresent -eq $false){
        $Adgroup = Get-ADGroup -Filter "Name -eq '$GroupName'" -ErrorAction SilentlyContinue -Server "$((Get-ADDomainController -Discover -Service GlobalCatalog).HostName):3268" 
        if ($null -eq $Adgroup) {
            Write-Host "The specified group does not exist: $GroupName" -ForegroundColor Yellow
            return
        }
    }
    # Force changes validation only; both paths persist the same group-name value.
    switch ($TierLevel) {
        "Tier0" {
            $config.Tier0ComputerGroup = $GroupName
            break
        }
        "Tier1" {
            $config.Tier1ComputerGroup = $GroupName
            break
        }
    }
    Set-TierLevelIsolationConfiguration -configFile $configFile -config $config
    return
}
#.SYNOPSIS
# Adds a service-account organizational-unit path to a tier.
#.DESCRIPTION
# Validates a relative path in the current domain or a full distinguished name in its specified
# domain, then stores it once in the selected tier collection. Force permits a missing OU so an
# installation can configure the path before creating it.
#.PARAMETER TierLevel
# Target tier. Valid values are Tier0 and Tier1.
#.PARAMETER OU
# Relative OU path or complete distinguished name to add.
#.PARAMETER configFile
# Configuration file to update. Defaults to the shared SYSVOL configuration.
#.PARAMETER Force
# Stores the path even when the OU does not currently exist.
#.EXAMPLE
# Add-TierLevelIsolationServiceAccountPath -TierLevel Tier0 -OU "OU=Service Accounts,OU=Tier 0,OU=Admin"
# Adds a relative Tier 0 service-account path.
#.EXAMPLE
# Add-TierLevelIsolationServiceAccountPath -TierLevel Tier1 -OU "OU=Service Accounts,OU=Tier 1,DC=contoso,DC=com" -Force
# Stores a fully qualified path without requiring the OU to exist.
#.OUTPUTS
# None.
function Add-TierLevelIsolationServiceAccountPath {
    param (
        [Parameter(Mandatory = $true, Position = 0)]
        [ValidateSet("Tier0", "Tier1")]
        [string]$TierLevel,
        [Parameter(Mandatory = $true, Position = 1)]
        [string]$OU,
        [Parameter(Mandatory = $false, Position = 2)]
        [string]$configFile = $global:configFile,
        [Parameter(Mandatory = $false, Position = 3)]
        [switch]$Force
    )
    $config = Get-TierLevelIsolationConfiguration $configFile
    if ($null -eq $OU) {
        Write-Host "The specified OU is null or empty." -ForegroundColor Red
        return
    }
    # Full DNs select their domain through DC components; relative paths use the current domain.
    if ($OU -like "*DC=*"){
        $DNSDomain = ConvertFrom-DN2Dns -DistinguishedName $OU
        if ($null -eq $DNSDomain){
            Write-Host "The specified domain does not exist: $OU" -ForegroundColor Yellow
            return
        }
        $Path = Get-ADOrganizationalUnit -Filter "DistinguishedName -eq '$OU'" -ErrorAction SilentlyContinue -Server $DnsDomain
    } else {
        $Path = Get-ADOrganizationalUnit -Filter "DistinguishedName -like '$OU,$((Get-ADDomain).DistinguishedName)'" -ErrorAction SilentlyContinue 
    }
    if ($null -eq $Path -and !$Force) {
        Write-Host "The specified path does not exist: $OU" -ForegroundColor Yellow
    }
    # Add only missing values so rerunning configuration remains idempotent.
    switch ($TierLevel) {
        "Tier0" {
            if ($null -eq $config.Tier0ServiceAccountPath) {
                $config.Tier0ServiceAccountPath = @($OU)
            }
            if ($config.Tier0ServiceAccountPath -notcontains $OU) {
                $config.Tier0ServiceAccountPath += $OU
            }
            break
        }
        "Tier1" {
            if ($null -eq $config.Tier1ServiceAccountPath) {
                $config.Tier1ServiceAccountPath = @($OU)
            }
            if ($config.Tier1ServiceAccountPath -notcontains $OU) {
                $config.Tier1ServiceAccountPath += $OU
            }
            break
        }
    }
    Set-TierLevelIsolationConfiguration -configFile $configFile -config $config
    return
}
#.SYNOPSIS
# Removes a service-account organizational-unit path from a tier.
#.DESCRIPTION
# Removes an exact OU path from the selected tier's service-account-path collection. No Active
# Directory organizational unit or account is deleted.
#.PARAMETER TierLevel
# Tier from which to remove the path. Valid values are Tier0 and Tier1.
#.PARAMETER OU
# Exact OU path value stored in the configuration.
#.PARAMETER configFile
# Configuration file to update. Defaults to the shared SYSVOL configuration.
#.EXAMPLE
# Remove-TierLevelIsolationServiceAccountPath -TierLevel Tier1 -OU "OU=Service Accounts,OU=Tier 1,OU=Admin"
# Removes the matching path from Tier 1.
#.OUTPUTS
# None.
function Remove-TierLevelIsolationServiceAccountPath {
    param (
        [Parameter(Mandatory = $true, Position = 0)]
        [ValidateSet("Tier0", "Tier1")]
        [string]$TierLevel,
        [Parameter(Mandatory = $true, Position = 1)]
        [string]$OU,
        [Parameter(Mandatory = $false, Position = 2)]
        [string]$configFile = $global:configFile
    )
    $config = Get-TierLevelIsolationConfiguration $configFile
    if ($null -eq $OU) {
        Write-Host "The specified OU is null or empty." -ForegroundColor Red
        return
    }
    # Remove the exact stored string without deleting the corresponding Active Directory OU.
    switch ($TierLevel) {
        "Tier0" {
            if ($config.Tier0ServiceAccountPath -contains $OU) {
                $config.Tier0ServiceAccountPath = @($config.Tier0ServiceAccountPath | Where-Object { $_ -ne $OU })
            } else {
                Write-Host "The specified path does not exist in the Tier0 configuration: $OU" -ForegroundColor Yellow
                return
            }
            break
          }
        "Tier1" {
            if ($config.Tier1ServiceAccountPath -contains $OU) {
                $config.Tier1ServiceAccountPath = @($config.Tier1ServiceAccountPath | Where-Object { $_ -ne $OU })
            } else {
                Write-Host "The specified path does not exist in the Tier1 configuration: $OU" -ForegroundColor Yellow
                return
            }
            break
        }
    }
    Set-TierLevelIsolationConfiguration -configFile $configFile -config $config
    return
}
#.SYNOPSIS
# Sets the runtime debug-log directory.
#.DESCRIPTION
# Stores the directory used by TierLevelComputerManagement.ps1 and
# TierLevelUserManagement.ps1. An empty value instructs those scripts to use local AppData.
#.PARAMETER LogPath
# Existing log directory, or an empty string to select local AppData at runtime.
#.PARAMETER configFile
# Configuration file to update. Defaults to the shared SYSVOL configuration.
#.EXAMPLE
# Set-DebugLogPath -LogPath "C:\Logs\TierLevelIsolation"
# Configures a central runtime log directory.
#.EXAMPLE
# Set-DebugLogPath -LogPath ""
# Restores the local AppData behavior.
#.OUTPUTS
# None.
function Set-DebugLogPath {
    param (
        [Parameter(Mandatory = $true, Position = 0)]
        [AllowEmptyString()]
        [string]$LogPath,
        [Parameter(Mandatory = $false, Position = 1)]
        [string]$configFile = $global:configFile
    )
    $config = Get-TierLevelIsolationConfiguration $configFile
    # Force creates the property for compatibility with configuration files from older versions.
    $config | Add-Member -MemberType NoteProperty -Name LogPath -Value $LogPath -Force
    Set-TierLevelIsolationConfiguration -configFile $configFile -config $config
}
#.SYNOPSIS
# Gets the configured runtime debug-log directory.
#.DESCRIPTION
# Returns the LogPath value used by the scheduled management scripts. An empty string means the
# scripts use the local AppData directory of their execution identity.
#.PARAMETER configFile
# Configuration file to read. Defaults to the shared SYSVOL configuration.
#.EXAMPLE
# Get-DebugLogPath
# Returns the configured runtime log directory.
#.OUTPUTS
# System.String. Configured directory or an empty string.
function Get-DebugLogPath {
    param (
        [Parameter(Mandatory = $false, Position = 0)]
        [string]$configFile = $global:configFile
    )
    # Reading through the schema-aware loader guarantees that LogPath exists for legacy files.
    $config = Get-TierLevelIsolationConfiguration $configFile 
    return $config.LogPath
}

#.SYNOPSIS
# Adds a group using the legacy name-based storage format.
#.DESCRIPTION
# Resolves a group from NetBIOS, UPN-style, canonical, or unqualified input and stores the group
# as DOMAIN\SamAccountName. Tier 1 rejects groups already assigned to Tier 0.
#.PARAMETER TierLevel
# Target tier. Valid values are Tier0 and Tier1.
#.PARAMETER GroupName
# Group in DOMAIN\GroupName, GroupName@dns.name, dns.name/GroupName, or unqualified format.
# Accepts pipeline input.
#.PARAMETER configFile
# Configuration file to update. Defaults to the shared SYSVOL configuration.
#.EXAMPLE
# Add-TierLevelIsolationGroupLegacy -TierLevel Tier0 -GroupName "CONTOSO\Legacy Operators"
# Stores the group's normalized NetBIOS name in Tier 0.
#.EXAMPLE
# Add-TierLevelIsolationGroupLegacy -TierLevel Tier1 -GroupName "Legacy Operators@emea.contoso.com"
# Resolves and stores a Tier 1 group from UPN-style input.
#.OUTPUTS
# None.
#.NOTES
# Internal compatibility helper; it is not exported. New code must use
# Add-TierLevelIsolationGroup, which stores stable group SIDs instead of names.
function Add-TierLevelIsolationGroupLegacy {
    param (
        [Parameter(Mandatory = $true, Position = 0)]
        [ValidateSet("Tier0", "Tier1")]
        [string]$TierLevel,
        [Parameter(Mandatory = $true, Position = 1, ValueFromPipeline = $true)]
        [string]$GroupName,
        [Parameter(Mandatory = $false, Position = 2)]
        [string]$configFile = $global:configFile
    )
    process{
        # Read the legacy name-based collections from the shared configuration.
        $config = Get-TierLevelIsolationConfiguration $configFile 
        # Extract domain and group name from each historically supported input format.
        try{
            switch -regex ($GroupName) {
                '^(.+?)\\(.+)$' {
                    # NetBIOS format: DOMAIN\GroupName
                    $DomainNetBios = $matches[1]
                    $GroupName = $matches[2]
                    $DomainDNSName = (Get-ADobject `
                        -SearchBase "CN=Partitions,$((Get-ADRootDSE).configurationNamingContext)" `
                        -LDAPFilter "(&(objectClass=crossRef)(nETBIOSName=$DomainNetBios))" `
                        -Properties nETBIOSName,nCName,dnsRoot -ErrorAction Stop ).dnsRoot[0]
                    break                 
                }
                '@(.+)$' {
                    # UPN format: GroupName@domain.com
                    $DomainDNSName = $matches[1]
                    $GroupName = $GroupName.Split('@')[0]
                    $DomainNetBios = (Get-ADDomain -Server $DomainDNSName -ErrorAction Stop).NetBIOSName
                    break
                }
                '/(.+)$'{
                    # LDAP format: domain.com/GroupName
                    $DomainDNSName = $GroupName.Split('/')[0]
                    $DomainNetBios = (Get-ADDomain -server $DomainDNSName -ErrorAction Stop ).NetBIOSName 
                    $GroupName = $GroupName.Split('/')[1]
                    break
                }
                default {
                    # Just group name without domain
                    $DomainDNSName = (Get-ADDomain -ErrorAction Stop).DNSRoot
                    $DomainNetBios = (Get-ADDomain -ErrorAction Stop).NetBIOSName
                    break
                }
            }
        # Validate the group and normalize it to DOMAIN\SamAccountName before persistence.
        $Adgroup = Get-ADGroup -Identity $GroupName -Server $DomainDNSName -ErrorAction SilentlyContinue
        if ($null -eq $ADgroup) {
            Write-Host "The specified group does not exist for group: $GroupName in $DomainDNSName" -ForegroundColor Red
            return
        }
        $GroupNameInNetBiosNotation = "$DomainNetBios\$($ADgroup.SamAccountName)"
        }
        catch [Microsoft.ActiveDirectory.Management.ADServerDownException]{
            Write-Host "The specified domain could not be reached for group: $GroupName" -ForegroundColor Red
            return
        }
        catch [Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException]{
            Write-Host "The specified group does not exist for group: $GroupName in $DomainDNSName" -ForegroundColor Red
            return
        }
        catch{
            Write-Host "An error occurred while processing the group: $GroupName. Error: $_" -ForegroundColor Red
            return
        }                 
        # Tier 0 takes precedence in the legacy model; cross-tier assignment is rejected.
        if ($TierLevel -eq "Tier0") {
            if ($config.Tier0Groups -notcontains $GroupNameInNetBiosNotation) {
                $config.Tier0Groups += $GroupNameInNetBiosNotation
            }
        } else {
            if ($config.Tier0Groups -contains $GroupNameInNetBiosNotation) {
                Write-Host "The group $GroupNameInNetBiosNotation already exists in Tier0. Remove it from Tier0 before adding it to Tier1." -ForegroundColor Red
                return
            }
            if ($config.Tier1Groups -notcontains $GroupNameInNetBiosNotation) {
                $config.Tier1Groups += $GroupNameInNetBiosNotation
            }
         }
        Set-TierLevelIsolationConfiguration -configFile $configFile -config $config
    }
}

#.SYNOPSIS
# Removes a group stored in the legacy name-based format.
#.DESCRIPTION
# Normalizes an unqualified group name to the current domain's NetBIOS format and removes the
# exact DOMAIN\GroupName value from the selected tier collection.
#.PARAMETER TierLevel
# Tier from which to remove the group. Valid values are Tier0 and Tier1.
#.PARAMETER GroupName
# Stored DOMAIN\GroupName value or an unqualified current-domain group name. Accepts pipeline input.
#.PARAMETER configFile
# Configuration file to update. Defaults to the shared SYSVOL configuration.
#.EXAMPLE
# Remove-TierLevelIsolationGroupLegacy -TierLevel Tier0 -GroupName "CONTOSO\Legacy Operators"
# Removes the exact legacy Tier 0 entry.
#.OUTPUTS
# None.
#.NOTES
# Internal compatibility helper; it is not exported. New code must use
# Remove-TierLevelIsolationGroup, which operates on SID-based entries.
function Remove-TierLevelIsolationGroupLegacy {
    param (
        [Parameter(Mandatory = $true, Position = 0)]
        [ValidateSet("Tier0", "Tier1")]
        [string]$TierLevel,
        [Parameter(Mandatory = $true, Position = 1, ValueFromPipeline = $true)]
        [string]$GroupName,
        [Parameter(Mandatory = $false, Position = 2)]
        [string]$configFile = $global:configFile
    )
    process{
        $config = Get-TierLevelIsolationConfiguration $configFile 
        # Legacy entries are stored in NetBIOS notation; qualify current-domain names when needed.
        If ($GroupName -notcontains '\'){
            $DomainNetBios = (Get-ADDomain).NetBIOSName
            $GroupName = "$DomainNetBios\$GroupName"    
        }
        # Filter the selected legacy collection and leave the opposite tier untouched.
        if ($TierLevel -eq "Tier0") {
            if ($config.Tier0Groups -contains $GroupName) {
                $config.Tier0Groups = @($config.Tier0Groups | Where-Object {$_ -ne $GroupName})
            }
        } elseif ($TierLevel -eq "Tier1") { 
                if ($config.Tier1Groups -contains $GroupName) {
                    $config.Tier1Groups = @($config.Tier1Groups | Where-Object {$_ -ne $GroupName})
                }        
        }
        Write-Host "Removing group $GroupName from tier level $TierLevel" -ForegroundColor Yellow
        Set-TierLevelIsolationConfiguration -configFile $configFile -config $config
        return
    }
}