xfunctions/Import-DryADConfiguration.ps1

using Namespace System.IO
using Namespace System.Management.Automation
using Namespace System.Collections.Generic
<#
    This is an AD Config module for use with DryDeploy, or by itself.
    Copyright (C) 2021 Bjørn Henrik Formo (bjornhenrikformo@gmail.com)
    LICENSE: https://raw.githubusercontent.com/bjoernf73/dry.module.ad/main/LICENSE
 
    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.
 
    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    GNU General Public License for more details.
 
    You should have received a copy of the GNU General Public License along
    with this program; if not, write to the Free Software Foundation, Inc.,
    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#>

function Import-DryADConfiguration {
    [CmdLetBinding(DefaultParameterSetName = 'Local')]
    param (
        [Parameter(HelpMessage='Variables are used for to replace "replacement patterns" in configs and GPOs. The replacement-pattern in dry.module.ad is:
###name###
In your configuration, any value ####MyVar### will be replaced by the value in custom objects created with for instance:
PS C:\> $MyVariables = @(
    [PSCustomObject]@{
        "name": "MyVar"
        "value": "TheValueToReplaceMyVar"
    },
    [PSCustomObject]@{
        "name": "MyOtherVar"
        "value": "TheValueToReplaceMyOtherVar"
    }
)
PS C:\> .\Import-DryADConfiguration.ps1 -Variables $MyVariables ...'
)]
        [List[PSCustomObject]]$Variables,

        [Parameter(HelpMessage = 'Instead of or in addition to -Variables, you may specify a path to a json file containing an array of objects resolving replacement patterns in the same manner as with -Variables. For instance, the file vars.json may contain
[
    {
        "name": "MyVar",
        "value": "TheValueToReplaceMyVar"
    },
    {
        "name": "MyOtherVar",
        "value": "TheValueToReplaceMyOtherVar"
    }
]
and you call the function like:
PS C:\> .\Import-DryADConfiguration.ps1 -VariablePath .\path\to\vars.json...'
)]
        [ValidateScript({Resolve-Path -Path $_ })]
        [fileinfo]$VariablesPath,

        [Parameter(Mandatory, HelpMessage="Path of the configuration directory. You may split the configuration into multiple files, as long as they're all in this directory. The README.md of this module will guide you to the wiki which contains the full guide on making configurations ")]
        [ValidateScript({Resolve-Path -Path $_})]
        [directoryinfo]$ConfigurationPath,

        [Parameter(HelpMessage = "Array of one or more Types to process. By default, all are processed")]
        [ValidateSet('ou_schema', 'groups', 'group_members', 'rights', 'gpo_imports', 'gpo_links',
        'wmi_filters', 'wmi_filters_links', 'ad_schema', 'netlogon', 'adm_templates', 'users', 'users_memberof')] 
        [string[]]$Types,

        [Parameter(Mandatory, ParameterSetName = 'Local', HelpMessage = "Specify a resolvable name or IP to a Domain Controller to perform AD actions on")]
        [string]$DomainController,

        [Parameter(Mandatory, ParameterSetName = 'Remote')]
        [pssession]$PSSession,

        [Parameter(HelpMessage = "Should only be `$true when called from DryDeploy. If true, this module will expect access to some of DryDeploy's modules. This will allow autogenerated passwords for users that are created to be stored in DryDeploy's Credentials store. If you're running standalone (not as part of DryDeploy, hence `$DryDeploy = `$false), then passwords of users that are configured with the property .password.get_or_generate = 'generate', are autogenerated, but lost. You will have to reset the passwords of the created users to access those accounts. Use instead .password.get_or_generate = 'get' to be prompted for the credentials.")]
        [switch]$DryDeploy,

        [Parameter(HelpMessage = "In certain conditions, for instance when variables passed in matches automatic variables, the function will warn and pause for some time to alert the user. This switch suppresses the wait")]
        [switch]$NoConfirm
    )
    try {
        olad i ''
        olad i 'dry.module.ad' -h -air
        olad i ''
        
        if ($DebugPreference) { 
            if ($DebugPreference -eq 'Inquire') {
                $DebugPreference = 'Continue'
            }
        }

        [string]$ExecutionType = $PSCmdlet.ParameterSetName
        olad i 'Execution Type', "$ExecutionType"

        $ConfigurationPath = (Resolve-Path -Path $ConfigurationPath -ErrorAction Stop).Path
        olad i 'ConfigurationPath', "$ConfigurationPath"
        
        # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
        #
        # VARIABLES
        #
        #
        # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
        
        # Automatic Variables
        if ($ExecutionType -eq 'Remote') {
            $ConfigurationPublicCertificatePath = Join-Path -Path $ConfigurationPath -ChildPath "RemoteSystemPublicCertificate.cer"
            $DomainDN = Get-DryADServiceProperty -Service 'domain' -Property 'DistinguishedName' -PSSession $PSSession
            $DomainFQDN = Get-DryADServiceProperty -Service 'domain' -Property 'DNSRoot' -PSSession $PSSession
            $DomainNB = Get-DryADServiceProperty -Service 'domain' -Property 'NetBIOSName' -PSSession $PSSession
            $ConfigurationNC = Get-DryADServiceProperty -Service 'rootdse' -Property 'configurationNamingContext' -PSSession $PSSession
            $SchemaNC = Get-DryADServiceProperty -Service 'rootdse' -Property 'schemaNamingContext' -PSSession $PSSession
            $SchemaMaster = Get-DryADServiceProperty -Service 'forest' -Property 'SchemaMaster' -PSSession $PSSession
        }
        else {
            $DomainDN = Get-DryADServiceProperty -Service 'domain' -Property 'DistinguishedName' -DomainController $DomainController
            $DomainFQDN = Get-DryADServiceProperty -Service 'domain' -Property 'DNSRoot' -DomainController $DomainController
            $DomainNB = Get-DryADServiceProperty -Service 'domain' -Property 'NetBIOSName' -DomainController $DomainController
            $ConfigurationNC = Get-DryADServiceProperty -Service 'rootdse' -Property 'configurationNamingContext' -DomainController $DomainController
            $SchemaNC = Get-DryADServiceProperty -Service 'rootdse' -Property 'schemaNamingContext' -DomainController $DomainController
            $SchemaMaster = Get-DryADServiceProperty -Service 'forest' -Property 'SchemaMaster' -DomainController $DomainController
        }
        $AutomaticVariables = @('DomainDN', 'DomainFQDN', 'DomainNB', 'ConfigurationNC', 'SchemaNC', 'SchemaMaster')
        
        # Variables from json file, $VariablesPath
        if ($VariablesPath) {
            $VariablesPath = (Resolve-Path -Path $VariablesPath -ErrorAction Stop).Path
            olad i 'VariablesPath', "$VariablesPath"
            # combine $Variables and objects from $VariablesPath json
            $Variables += Get-DryADJson -File $VariablesPath
        }
       
        # Add the automatic variables to the $Variables array, but warn if they overlap with input variables
        $AutomaticVariables.foreach({
            $CurrentAutomaticVariable = $_
            if ($null -eq ($Variables | Where-Object { $_.Name -eq $CurrentAutomaticVariable })) {
                $Variables += New-Object -TypeName PSCustomObject -Property @{
                    Name  = "$CurrentAutomaticVariable"
                    Value = Get-Variable -Name $CurrentAutomaticVariable -Value
                }
            }
            else{
                $CurrentAutomaticVariableValue = Get-Variable -Name $CurrentAutomaticVariable -Value
                $MatchingVariable = $Variables | Where-Object { $_.Name -eq $CurrentAutomaticVariable }
                if ($MatchingVariable.Value -ne $CurrentAutomaticVariableValue) {
                    olad w "In variables, you've specified '$CurrentAutomaticVariable' with value '$($MatchingVariable.Value)', `
                    but '$($MatchingVariable.Name)' already exists in this function's automatic variables with value '$CurrentAutomaticVariableValue'. `
                    My value is extracted from the system - is your value wrong, or is it from a different system? Your value will overwrite the `
                    automatic variable, but if you made a mistake, you should quit now (crtl+c), remove the variable, and try again. I'll sleep for `
                    20 seconds to give you some time to think, unless you used the -NoConfirm switch, in which case I'll just continue."

                    
                    if($NoConfirm) {
                        olad i "NoConfirm switch is set - continuing without waiting"
                    }
                    else {
                        $Seconds = 20
                        for ($i = $Seconds; $i -ge 0; $i--) {
                            $percentComplete = (($Seconds - $i) / $Seconds) * 100
                            Write-Progress -Activity "Waiting..." -Status "Countdown: $i seconds remaining" -PercentComplete $percentComplete
                            Start-Sleep -Seconds 1
                        }
                        $MatchingVariable.Value = $CurrentAutomaticVariableValue 
                    }    
                }
                else {
                    olad w "Variable '$CurrentAutomaticVariable' already exists as an Automatic Variable - you don't have to specify it."
                }
            }
        })
        
        # For debug - display all variables
        olad i ""
        olad i "Variables" -sh -air
        foreach($variable in $Variables){
            if($variable.name -in $AutomaticVariables){
                olad i @("[auto] '$($variable.name)'","'$($variable.Value)'")
            }
            else{
                olad i @("[input] '$($variable.name)'","'$($variable.Value)'")
            }
            # warn if whitespaces at start or end of name or value
            if(( $variable.name -match '^\s') -or 
                ( $variable.name -match '\s$') -or 
                ( $variable.value -match '^\s') -or 
                ( $variable.value -match '\s$')) {
                olad w "Variable '$($variable.name)'='$($variable.value)' contains one or more whitespace characters at the start or end of it's name or value."
            }
        }
        
        # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
        #
        # REPLACEMENT HASH
        # In configurations, patterns matching the format ###name### will be replaced
        # if there is a variable with name 'name'
        #
        # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
        $ReplacementHash = @{}
        foreach ($Var in $Variables) {
            $ReplacementHash.Add("###$($Var.Name)###", $Var.Value)
        }
        
        # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
        #
        # AD CONFIGURATION OBJECT
        # Pick up all jsons (*.json) in $ConfigurationPath, and merge
        # into $ADConfiguration
        #
        # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
        Remove-Variable -Name ADConfiguration, ADConfObject, ConfigurationPathFiles -ErrorAction Ignore
        $ADConfiguration = New-Object -TypeName PSCustomObject
        $ConfigurationPathFiles = @(Get-ChildItem -Path "$ConfigurationPath\*" -Include "*.jsonc", "*.json" -ErrorAction Stop)
        
        foreach ($ADConfFile in $ConfigurationPathFiles) {
            $ADConfObject = Get-DryADJson -Path $ADConfFile.FullName -ErrorAction Stop
            $ADConfiguration = (Merge-DryADPSObjects -FirstObject $ADConfiguration -SecondObject $ADConfObject)
        }
 
        # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
        #
        # CASING
        # Variables that specifies case modification of OUs, GPOs, groups and users.
        # Valid values are 'UPPER', lower', 'Capitalized' and 'ignore'. Defaults
        # to ignore. Should allow a regex to be used, but for now, only the above
        #
        # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
        [string]$OUCase    = 'ignore'
        [string]$GPOCase   = 'ignore'
        [string]$GroupCase = 'ignore'
        [string]$UserCase  = 'ignore'

        if ($ADConfiguration.casing.ad_organizational_unit_case) {
            [string]$OUCase = $ADConfiguration.casing.ad_organizational_unit_case
        }
        if ($ADConfiguration.casing.ad_gpo_case) {
            [string]$GPOCase = $ADConfiguration.casing.ad_gpo_case
        }
        if ($ADConfiguration.casing.ad_group_case) { 
            [string]$GroupCase = $ADConfiguration.casing.ad_group_case
        }
        if ($ADConfiguration.casing.ad_user_case) { 
            [string]$UserCase = $ADConfiguration.casing.ad_user_case
        }

        olad v 'Casing - OUs',    "$OUCase"
        olad v 'Casing - GPOs',   "$GPOCase"
        olad v 'Casing - Groups', "$GroupCase"
        olad v 'Casing - Users',  "$UserCase"

        # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
        #
        # PROGRESS COUNTERS
        # Counts configurations to process before start, to show
        # a progress bar during configuration
        #
        # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
        [int]$ElementsCounter = 0
        [int]$NumberOfElementsToProcess = 0

        # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
        #
        # AD SCHEMA
        # Action: Get and Count
        # Dataset: $ADConfiguration
        #
        # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
        if (Test-Path -Path "$ConfigurationPath\ad_schema" -ErrorAction Ignore) {
            if (($Types -icontains 'ad_schema') -or ($null -eq $Types)){ 
                $ProcessADSchema = $true
                $ADSchemaExtensions = @(Get-ChildItem -Path "$ConfigurationPath\ad_schema\*" -Include "*.ldf")
                $NumberOfElementsToProcess += $ADSchemaExtensions.Count
            } 
            $NumberOfADSchemaExtensions = $ADSchemaExtensions.Count
        }

        # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
        #
        # NETLOGON
        # Action: Count
        #
        # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
        if (Test-Path -Path "$ConfigurationPath\netlogon" -ErrorAction Ignore) {  
            if (($Types -icontains 'netlogon') -or 
                ($null -eq $Types)) {

                $ProcessNETLOGON = $true
                $NumberOfElementsToProcess++
                $NumberOfNETLOGONs = 1
            } 
        }

        # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
        #
        # adm_templates
        # Action: Count
        #
        # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
        if (Test-Path -Path "$ConfigurationPath\adm_templates" -ErrorAction Ignore) {  
            if (($Types -icontains 'adm_templates') -or ($null -eq $Types)) { 
                $ProcessAdmTemplates = $true
                $NumberOfElementsToProcess++
            } 
        }

        # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
        #
        # ORGANIZATIONAL UNITS
        # Action: Get, Count and String Replacement
        #
        # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
        if ($ADConfiguration.ou_schema) {
            [array]$DomainOUs = @($ADConfiguration.ou_schema)
            if (($Types -icontains 'ou_schema') -or ($null -eq $Types)) { 
                $ProcessOUs = $true 
                $NumberOfElementsToProcess += $DomainOUs.Count
            }
        }

        # Resolve replacement patterns
        if ($DomainOUs) {
            $DomainOUs = Resolve-DryADReplacementPatterns -inputobject $DomainOUs -Variables $Variables
        }
        $NumberOfOUs = $DomainOUs.Count

        # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
        #
        # ORGANIZATIONAL UNITS PATHS
        # Action: Resolve .path from .parent_alias and .child_path
        #
        # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
        
        $AnOUWasResolved = $true
        do {
            $AnOUWasResolved = $false 
            # Loop through DomainOUs that already have a path
            foreach ($ResolvedOU in $DomainOUs | Where-Object { $null -ne $_.path }) {
                
                # Loop through DomainOUs that do not have a path
                foreach ($UnresolvedOU in $DomainOUs | Where-Object { $null -eq $_.path }) {
                    try {
                        if ($UnresolvedOU.parent_alias -eq $ResolvedOU.alias) {
                            $Path = ($ResolvedOU.path + '/' + $UnresolvedOU.child_path) -replace '//', '/'
                            $AnOUWasResolved = $true #keeps the loop going
                            
                            # Add Path property and remove parent_alias and child_path
                            $UnresolvedOU | Add-Member -MemberType NoteProperty -Name 'Path' -Value $Path -Force
                            $UnresolvedOU.PSObject.Properties.Remove('parent_alias')
                            $UnresolvedOU.PSObject.Properties.Remove('child_path')
                        }
                    }
                    catch {
                        $PSCmdLet.ThrowTerminatingError($_)
                    }
                    finally {
                        $Path = $null
                    }
                }
            }
        }
        While ($AnOUWasResolved)

        # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
        #
        # ORGANIZATIONAL UNITS PATHS
        # Action: Display resolved paths
        #
        # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
        if ($DomainOUs.count -gt 0) {
            olad i ''
            olad i "Resolved OU Aliases" -sh -air
            foreach ($OU in $DomainOUs) {
                olad i "$($OU.Alias)", "$($OU.Path)"
                if($null -eq $OU.Path) {
                    olad e "Unable to resolve OU for '$($OU.Alias)'"
                    throw "Unable to resolve OU for '$($OU.Alias)'"
                }
            }
        }
    
        # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
        #
        # ORGANIZATIONAL UNITS PATHS
        # Action: Convert To 'distinguishedName'
        #
        # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
        foreach ($OU in $DomainOUs) {
            $OU.Path = ConvertTo-DryADDistinguishedName -Name $OU.Path -Case $OUcase
            olad d "Domain OU dN for $($OU.Alias) is: '$($OU.Path)'" 
        }

        # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
        #
        # WMI Filter Imports and Links
        # Action: Get, Count and String Replacement
        #
        # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
        if (($Types -icontains 'wmi_filters') -or 
            ($Types -icontains 'wmi_filters_links') -or 
            ($null -eq $Types)){ 
            if ($ADConfiguration.wmi_filters) {
                [array]$DomainWMIFilters += @($ADConfiguration.wmi_filters)
                $DomainWMIFilters = Resolve-DryADReplacementPatterns -inputobject $DomainWMIFilters -Variables $Variables

                # Count the wmi_filters, but only if we're actually importing them
                if (($Types -icontains 'wmi_filters') -or ($null -eq $Types)){
                    $ProcessWMIFilterImports = $true
                    $NumberOfElementsToProcess += $DomainWMIFilters.Count
                    $NumberOfWMIFilters = $DomainWMIFilters.Count
                }

                if (($Types -icontains 'wmi_filters_links') -or ($null -eq $Types)){
                    $ProcessWMIFilterLinks = $true
                    $DomainWmiFilterLinksCount = 0
                    $DomainWMIFilters.foreach({
                        $_.links.foreach({
                            $DomainWmiFilterLinksCount++
                        })
                    })
                    $NumberOfWMIFilterLinks = $DomainWmiFilterLinksCount
                    $NumberOfElementsToProcess += $NumberOfWMIFilterLinks
                }
            }
        }

        # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
        #
        # SECURITY GROUPS
        # Action: Get, Count, String Replacement, Resolve Paths and Convert Case
        #
        # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #

        if ($ADConfiguration.security_groups) {
            $DomainSecurityGroups = @($ADConfiguration.security_groups)
            if (($Types -icontains 'security_groups') -or ($null -eq $Types)) { 
                $ProcessSecurityGroups = $true
                $NumberOfElementsToProcess += $DomainSecurityGroups.Count
                $NumberOfSecurityGroups = $DomainSecurityGroups.Count
            }
            # Replace regardless of $Types -icontains 'security_groups', since SecurityGroups are referenced by Rights, GroupMembers and GPOImports
            $DomainSecurityGroups = Resolve-DryADReplacementPatterns -inputobject $DomainSecurityGroups -Variables $Variables

            # Resolve OU from schema
            foreach ($SecurityGroup in $DomainSecurityGroups) {
                if ($null -eq $SecurityGroup.path) {
                    $Path = Get-DryADOUPathFromAlias -Alias $SecurityGroup.Alias -OUs $DomainOUs
                    $SecurityGroup | Add-Member -MemberType NoteProperty -Name path -Value $Path
                }
                # Convert to $GroupCase
                $SecurityGroup.name = ConvertTo-DryADCase -Name $SecurityGroup.name -Case $Groupcase
            }
        }

        # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
        #
        # GROUP MEMBERS
        # Action: Count
        #
        # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
        $NumberOfDomainMemberAndMemberOf = 0
       
        if (($Types -icontains 'group_members') -or ($null -eq $Types)){ 
            $ProcessGroupMembers = $true

            foreach ($DomainSecurityGroup in $DomainSecurityGroups) {
                $NumberOfDomainMemberAndMemberOf += $DomainSecurityGroup.Member.Count
                $NumberOfDomainMemberAndMemberOf += $DomainSecurityGroup.MemberOf.Count
            }
            $NumberOfElementsToProcess += $NumberOfDomainMemberAndMemberOf
        }

        # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
        #
        # RIGHTS
        # Action: Count and Resolve Paths
        #
        # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
        if (($Types -icontains 'rights') -or ($null -eq $Types)) { 
            $ProcessRights = $true
            $NumberOfDomainRights = 0

            foreach ($DomainSecurityGroup in $DomainSecurityGroups) {
                foreach ($DomainRight in $DomainSecurityGroup.Rights) {
                    $NumberOfElementsToProcess++
                    # Update the debug counter as well
                    $NumberOfDomainRights++
                    # Resolve Path
                    if ($null -eq $DomainRight.Path) {
                        $Path = Get-DryADOUPathFromAlias -Alias $DomainRight.Alias -OUs $DomainOUs
                        $DomainRight | Add-Member -MemberType NoteProperty -Name Path -Value $Path

                        # Once resolved, remove Alias.
                        $DomainRight.PSObject.Properties.Remove('Alias')
                    }
                }
            }
        }

        # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
        #
        # GROUP POLICY IMPORTS
        # Action: Count and String replacement
        #
        # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
        if (($Types -icontains 'gpo_imports') -or ($null -eq $Types)){  
            if ($ADConfiguration.gpo_imports){   
                $ProcessGPOImports = $true   
                $DomainGPOImports = @($ADConfiguration.gpo_imports)
                
                # If the configuration set contains json-gpos...
                $JsonGPOImports = @($ADConfiguration.gpo_imports | Where-Object {$_.type -eq 'json'})
                $RequiresGPOHelper = $false
                if ($JsonGPOImports.count -gt 0) {
                    $RequiresGPOHelper = $true
                }
 
                $NumberOfElementsToProcess += $DomainGPOImports.Count
                $NumberOfGPOImports += $DomainGPOImports.Count 
                $DomainGPOImports = Resolve-DryADReplacementPatterns -inputobject $DomainGPOImports -Variables $Variables
            }
        }

        # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
        #
        # GROUP POLICY LINKS
        # Action: Count, String replacement and Resolve paths from aliases
        #
        # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
        if (($Types -icontains 'gpo_links') -or ($null -eq $Types)){  
            if ($ADConfiguration.gpo_links) {  
                $ProcessGPOLinks = $true
                $DomainGPOLinks = @($ADConfiguration.gpo_links)
                $DomainGPOLinksCount = 0 
                foreach ($DomainGPOLink in $DomainGPOLinks) {
                    $DomainGPOLink.gplinks.foreach({
                        $DomainGPOLinksCount++
                    })
                }
                $NumberOfElementsToProcess += $DomainGPOLinksCount
                $NumberOfGPOLinks = $DomainGPOLinksCount

                # String Replacements
                $DomainGPOLinks = Resolve-DryADReplacementPatterns -inputobject $DomainGPOLinks -Variables $Variables
                
                # Resolve Domain Paths from OU schema
                foreach ($DomainGPOLink in $DomainGPOLinks) {
                    if ($null -eq $DomainGPOLink.path) {
                        $Path = Get-DryADOUPathFromAlias -Alias $DomainGPOLink.alias -OUs $DomainOUs
                        $DomainGPOLink | Add-Member -MemberType NoteProperty -Name Path -Value $Path
                    }
                }
            } 
        }

        # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
        #
        # USERS
        # Action: Count, String replacement, resolve OUs
        #
        # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
        if (($Types -icontains 'users') -or ($null -eq $Types)){  
            if ($ADConfiguration.users) {
                
                $DomainUsers = @($ADConfiguration.users)
                if (($Types -icontains 'users') -or ($null -eq $Types)){ 
                    $ProcessUsers = $true
                    $NumberOfElementsToProcess += $DomainUsers.Count
                    $NumberOfUsers = $DomainUsers.Count
                }

                # Replace any replacement pattern
                if ($DomainUsers.count -gt 0) {
                    $DomainUsers = @(Resolve-DryADReplacementPatterns -inputobject $DomainUsers -Variables $Variables)
                }

                # Resolve domain OU paths from schema
                foreach ($User in $DomainUsers) {
                    if ($null -eq $User.path) {
                        # Resolve domain paths from OU schema
                        $Path = Get-DryADOUPathFromAlias -Alias $User.Alias -OUs $DomainOUs
                        $User | Add-Member -MemberType NoteProperty -Name Path -Value $Path
                    }
                    # Convert to $GroupCase
                    $User.name = ConvertTo-DryADCase -Name $User.name -Case $UserCase
                }
            }
        }

        # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
        #
        # USER'S GROUP MEMBERSHIPS
        # Action: Count
        #
        # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
        if (($Types -icontains 'users_memberof') -or ($null -eq $types)) {  
            
            $ProcessUserMemberOf = $true
            # Count User's MemberOfs
            $NumberOfDomainUserMemberOf = 0
            foreach ($DomainUser in $DomainUsers) {
                $NumberOfDomainUserMemberOf += $DomainUser.MemberOf.Count
            }
            $NumberOfElementsToProcess += $NumberOfDomainUserMemberOf
        }

        # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
        #
        # AD DRIVE
        # Action: Make sure the AD Drive on the executing system is created when
        # importing the ActiveDirectory module, and that it points to the correct
        # Domain Controller.
        # - If Remote execution, we remote into a Domain Controller, and the AD
        # drive should be pointed to localhost.
        # - If Local execution, the AD Drive should point to $DomainController
        #
        # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
        olad i ''
        olad i "Connecting to Active Directory" -sh -air
        switch ($ExecutionType) {
            'Remote' {
                olad i "Configuring AD Drive","$($PSSession.ComputerName)"
                Set-DryADDrive -PSSession $PSSession
            }
            'Local' {
                olad i "Configuring AD Drive","$DomainController"
                Set-DryADDrive -DomainController $DomainController
            }
        }
        
        # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
        #
        # AD WEB SERVICES ON DOMAIN CONTROLLER
        # Action: Test, and wait for the service to become available
        #
        # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
        if ($ExecutionType -eq 'Remote') {
            olad i "Testing and Waiting for AD Availability..."
            Wait-DryADForADWebServices -DomainDN $DomainDN -PSSession $PSSession
        }

        # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
        #
        # SCHEMA UPDATES
        # Action: Invoke/Update. Will only work when 'Remote' and $DomainController
        # is the Schema Master
        #
        # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
        if ($ProcessADSchema) {
            olad i ''
            olad i "AD Schema Extensions ($($ADSchemaExtensions.count) to configure)" -sh -air
            foreach ($ADSchemaExtension in $ADSchemaExtensions) {

                # increment the element counter and update progress
                $ElementsCounter++
                $WriteProgressParameters = @{
                    Activity        = 'Configuring Active Directory'
                    Status          = "Item ($ElementsCounter / $NumberOfElementsToProcess): Schema extension: $($ADSchemaExtension.BaseName)"
                    PercentComplete = (($ElementsCounter / $NumberOfElementsToProcess) * 100 )   
                }
                Write-Progress @WriteProgressParameters
                
                $ADSchemaExtJson = Get-Content -Path (Join-Path -Path (Split-Path -Path $ADSchemaExtension.FullName) -ChildPath "$($ADSchemaExtension.BaseName).json") |
                    ConvertFrom-Json -ErrorAction 'Stop'

                $ADSchemaExtContent = Get-Content -Path $ADSchemaExtension.FullName -Raw -ErrorAction 'Stop'

                $ExtendDryADSchemaParams = @{
                    Type         = $ADSchemaExtension.BaseName
                    SuccessCount = $ADSchemaExtJson.success_string_match_count
                    Content      = $ADSchemaExtContent
                    Variables    = $Variables
                    SchemaMaster = $SchemaMaster 
                }
                if ($ExecutionType -eq 'Remote') {
                    $ExtendDryADSchemaParams += @{
                        PSSession = $PSSession
                    }
                }
                
                olad i 'Extending AD Schema, type', "$($ADSchemaExtension.BaseName)"
                Set-DryADSchemaExtension @ExtendDryADSchemaParams
            }

            $DebugCounter = $NumberOfADSchemaExtensions
            if ($ElementsCounter -ne $DebugCounter) {
                throw "Elementscounter is $ElementsCounter, but was supposed to be $DebugCounter"
            }
        } # if ($ProcessSchema)

        # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
        #
        # NETLOGON
        # Action: Configure
        #
        # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
        if ($ProcessNETLOGON) {
            olad i ''
            olad i "NETLOGON File Copy" -sh -air
            # increment the element counter and update progress
            $ElementsCounter++
            $WriteProgressParameters = @{
                Activity        = 'Configuring Active Directory'
                Status          = "Item ($ElementsCounter / $NumberOfElementsToProcess): NETLOGON"
                PercentComplete = (($ElementsCounter / $NumberOfElementsToProcess) * 100 )   
            }
            Write-Progress @WriteProgressParameters
            $NETLOGONSourcePath = Join-Path -Path $ConfigurationPath -ChildPath 'netlogon'
            switch ($ExecutionType) {
                'Local' {
                    $NETLOGONTargetPath = "\\$DomainFQDN\NETLOGON\"
                }
                'Remote' {
                    $NETLOGONTargetPath = "C:\Windows\SYSVOL\domain\scripts\"
                }
            }
            olad i "NETLOGON source path", "$NETLOGONSourcePath\*"
            olad i "NETLOGON target path", "$NETLOGONTargetPath"

            $CopyNETLOGONParams = @{
                Path        = "$NETLOGONSourcePath\*"
                Destination = "$NETLOGONTargetPath" 
                Recurse     = $true 
                Force       = $true
                ErrorAction = 'Stop'
            }
            if ($ExecutionType -eq 'Remote') {
                $CopyNETLOGONParams += @{
                    ToSession = $PSSession        
                }
            }
            
            Copy-Item @CopyNETLOGONParams
        
            $DebugCounter += $NumberOfNETLOGONs
            if ($ElementsCounter -ne $DebugCounter) {
                throw "Elementscounter is $ElementsCounter, but was supposed to be $DebugCounter"
            }
        }

        # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
        #
        # adm_templates
        # Action: Configure
        #
        # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
        if ($ProcessAdmTemplates) {
            olad i ''
            olad i "Administrative Templates File Copy" -sh -air
            # increment the element counter and update progress
            $ElementsCounter++
            $WriteProgressParameters = @{
                Activity        = 'Configuring Active Directory'
                Status          = "Item ($ElementsCounter / $NumberOfElementsToProcess): Administrative Templates"
                PercentComplete = (($ElementsCounter / $NumberOfElementsToProcess) * 100 )   
            }
            Write-Progress @WriteProgressParameters
            
            $AdmTemplatesSourcePath = "$ConfigurationPath\adm_templates\*"
            switch ($ExecutionType) { 
                'Local' {
                    $AdmTemplatesTargetPath = "\\\\$DomainFQDN\\SYSVOL\\$DomainFQDN\\Policies\\PolicyDefinitions\\"
                }
                'Remote' {
                    $AdmTemplatesTargetPath = "C:\\Windows\\SYSVOL\\domain\\Policies\\PolicyDefinitions\\"
                }
            }

            olad i "adm_templates source path", $AdmTemplatesSourcePath
            olad i "adm_templates target path", $AdmTemplatesTargetPath

            $CopyDryFilesToRemoteTargetParams = @{
                SourcePath = "$AdmTemplatesSourcePath" 
                TargetPath = "$AdmTemplatesTargetPath" 
            }
            
            if ($PSSession) {
                $CopyDryFilesToRemoteTargetParams += @{
                    PSSession = $PSSession
                }   
            }
            Copy-DryADFilesToRemoteTarget @CopyDryFilesToRemoteTargetParams | 
                Out-Null
        
            $DebugCounter += 1
            if ($ElementsCounter -ne $DebugCounter) {
                throw "Elementscounter is $ElementsCounter, but was supposed to be $DebugCounter"
            }
        }


        # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
        #
        # ORGANIZATIONAL UNITS
        # Action: Create
        #
        # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
        if ($ProcessOUs) {
            olad i ''
            olad i "OUs ($($DomainOUs.count) tasks)" -sh -air
            foreach ($OU in $DomainOUs) {

                # increment the element counter and update progress
                $ElementsCounter++
                $WriteProgressParameters = @{
                    Activity        = 'Configuring Active Directory'
                    Status          = "Item ($ElementsCounter / $NumberOfElementsToProcess): Creating OU: $($OU.Path)"
                    PercentComplete = (($ElementsCounter / $NumberOfElementsToProcess) * 100 )   
                }
                Write-Progress @WriteProgressParameters
    
                # Create instance of class OU and invoke method CreateOU()
                olad i "Creating OU", "$($OU.path)"
                
                switch ($ExecutionType) {
                    'Local' {
                        [OU]$OUObject = [OU]::new("$($OU.path)", "$DomainFQDN", $DomainController)
                    }
                    'Remote' {
                        [OU]$OUObject = [OU]::new("$($OU.path)", "$DomainFQDN", $PSSession)
                    }
                }
                $OUObject.CreateOU()
                
                Remove-Variable -Name OUObject -ErrorAction Ignore            
            }

            $DebugCounter += $NumberOfOUs
            if ($ElementsCounter -ne $DebugCounter) {
                throw "Elementscounter is $ElementsCounter, but was supposed to be $DebugCounter"
            }

        } # if ($ProcessOU)


        # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
        #
        # WMI FILTERS
        # Action: Create
        #
        # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #

        if ($ProcessWMIFilterImports) {
            olad i ''
            olad i "WMIFilters ($($DomainWMIfilters.count) tasks)" -sh -air

            # Make sure 'Allow System Only Change' in registry on domain controller is 1
            # If it isn't, WMIFilter creation will fail with access denied
            olad d "Calling 'Set-DryADRemoteRegistry' to set 'Allow System Only Change' to 1"
            
            $AllowSystemOnlyChangeParameters = @{
                BaseKey     = 'HKEY_LOCAL_MACHINE' 
                LeafKey     = 'System\\CurrentControlSet\\Services\\NTDS\\Parameters' 
                ValueName   = 'Allow System Only Change' 
                ValueData   = 1 
                ValueType   = 'DWORD'
                PSSession   = $PSSession
                ErrorAction = 'Stop'
            }
            
            Set-DryADRemoteRegistry @AllowSystemOnlyChangeParameters
            
            # $DomainWMIfilters
            foreach ($GPOWMIFilter in $DomainWMIfilters) {
                
                # Progress
                $ElementsCounter++  
                $WriteProgressParameters = @{
                    Activity        = 'Configuring Active Directory'
                    Status          = "Item ($ElementsCounter / $NumberOfElementsToProcess): Importing WMIFilter '$($GPOWMIFilter.Name)'"
                    PercentComplete = (($ElementsCounter / $NumberOfElementsToProcess) * 100 )
                }
                Write-Progress @WriteProgressParameters
            
                # Any GPMC command must run in a remote session, since the cmdlets lack the -credentials parameter
                $NewDryWmiFilterParameters = @{
                    Name        = $GPOWMIFilter.Name 
                    Description = $GPOWMIFilter.Description 
                    Query       = [array]$GPOWMIFilter.Queries 
                }
                switch ($ExecutionType) {
                    'Local' {
                        $NewDryWmiFilterParameters += @{
                            DomainController = $DomainController
                        }
                    }
                    'Remote' {
                        $NewDryWmiFilterParameters += @{
                            PSSession = $PSSession
                        }
                    }
                }
                olad i "Importing WMI Filter", "$($GPOWMIFilter.Name)"
                New-DryADWmiFilter @NewDryWmiFilterParameters
            }


            $DebugCounter += $NumberOfWMIFilters
            if ($ElementsCounter -ne $DebugCounter) {
                throw "Elementscounter is $ElementsCounter, but was supposed to be $DebugCounter"
            }
        }

        # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
        #
        # SECURITY GROUPS
        # Action: Create
        #
        # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
        if ($ProcessSecurityGroups) {
            olad i ''
            olad i "Security Groups ($($DomainSecurityGroups.count) tasks)" -sh -air
 
            foreach ($SecurityGroup in $DomainSecurityGroups) {

                # increment the element counter and update progress
                $ElementsCounter++
                $WriteProgressParameters = @{
                    Activity        = 'Configuring Active Directory'
                    Status          = "Item ($ElementsCounter / $NumberOfElementsToProcess): Security Group: $($SecurityGroup.name)"
                    PercentComplete = (($ElementsCounter / $NumberOfElementsToProcess) * 100 )   

                }
                Write-Progress @WriteProgressParameters

                $NewDryADSecurityGroupParams = @{
                    Name        = $SecurityGroup.name
                    Path        = $SecurityGroup.path
                    Description = $SecurityGroup.description
                    GroupScope  = $SecurityGroup.groupscope
                }
                switch ($ExecutionType) {
                    'Local' {
                        $NewDryADSecurityGroupParams += @{
                            DomainController = $DomainController
                        }
                    }
                    'Remote' {
                        $NewDryADSecurityGroupParams += @{
                            PSSession = $PSSession
                        }
                    }
                }
                olad i "Creating Security Group", "$($SecurityGroup.name)" 
                New-DryADSecurityGroup @NewDryADSecurityGroupParams
            }

            $DebugCounter += $NumberOfSecurityGroups
            if ($ElementsCounter -ne $DebugCounter) {
                throw "Elementscounter is $ElementsCounter, but was supposed to be $DebugCounter"
            }
        } # if ($ProcessSecurityGroups)

        # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
        #
        # GROUP'S GROUP MEMBERS
        # Action: Add Role Groups to SecurityGroups
        #
        # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
        if ($ProcessGroupMembers) {
            olad i ''
            olad i "Group Members ($NumberOfDomainMemberAndMemberOf tasks)" -sh -air
            
            foreach ($DomainSecurityGroup in $DomainSecurityGroups) {
                foreach ($DomainSecurityGroupMember in $DomainSecurityGroup.Member) {
                    # increment the element counter and update progress
                    
                    $ElementsCounter++
                    $WriteProgressParameters = @{
                        Activity        = 'Configuring Active Directory'
                        Status          = "Item ($ElementsCounter / $NumberOfElementsToProcess): Group Member: $($DomainSecurityGroup.name)"
                        PercentComplete = (($ElementsCounter / $NumberOfElementsToProcess) * 100 )   
                    }
                    Write-Progress @WriteProgressParameters

                    $AddDryADGroupMemberParams = @{
                        Group  = $DomainSecurityGroup.name
                        Member = $DomainSecurityGroupMember
                    }
                    switch ($ExecutionType) {
                        'Local' {
                            $AddDryADGroupMemberParams += @{
                                DomainController = $DomainController
                            }
                        }
                        'Remote' {
                            $AddDryADGroupMemberParams += @{
                                PSSession = $PSSession
                            }
                        }
                    }
                    olad i "Configure Group Members", "Adding '$DomainSecurityGroupMember' to '$($DomainSecurityGroup.name)'"
                    Add-DryADGroupMember @AddDryADGroupMemberParams
                    Remove-Variable -Name AddDryADGroupMemberParams -ErrorAction Ignore
                }
                foreach ($DomainSecurityGroupMemberOf in $DomainSecurityGroup.MemberOf) {
                    
                    # increment the element counter and update progress
                    $ElementsCounter++
                    $WriteProgressParameters = @{
                        Activity        = 'Configuring Active Directory'
                        Status          = "Item ($ElementsCounter / $NumberOfElementsToProcess): Group Member Of: $($DomainSecurityGroup.name)"
                        PercentComplete = (($ElementsCounter / $NumberOfElementsToProcess) * 100 )   
                    }
                    Write-Progress @WriteProgressParameters

                    # Add $DomainSecurityGroup to each MemberOf
                    $AddDryADGroupMemberParams = @{
                        Group  = $DomainSecurityGroupMemberOf
                        Member = $DomainSecurityGroup.name
                    }
                    switch ($ExecutionType) {
                        'Local' {
                            $AddDryADGroupMemberParams += @{
                                DomainController = $DomainController
                            }
                        }
                        'Remote' {
                            $AddDryADGroupMemberParams += @{
                                PSSession = $PSSession
                            }
                        }
                    }
                    olad i "Configure Group Members", "Adding '$($DomainSecurityGroup.name)' to '$DomainSecurityGroupMemberOf'"
                    Add-DryADGroupMember @AddDryADGroupMemberParams
                    Remove-Variable -Name AddDryADGroupMemberParams -ErrorAction Ignore
                }
            }

            $DebugCounter += $NumberOfDomainMemberAndMemberOf
            if ($ElementsCounter -ne $DebugCounter) {
                throw "Elementscounter is $ElementsCounter, but was supposed to be $DebugCounter"
            }
        }

        # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
        #
        # USERS
        # Action: Getting the connection point's public certificate.
        #
        # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
        if ($ProcessUsers) {
            if (($NumberOfUsers.Count -gt 0) -and ($ExecutionType -eq 'Remote')){
                olad i ''
                olad i "Users - Getting the Connection Point's public certificate" -sh -air
                try {
                    Get-DryADRemotePublicCertificate -PSSession $PSSession -CertificateFile $ConfigurationPublicCertificatePath
                }
                catch {
                    $PSCmdLet.ThrowTerminatingError($_)
                }
            }

            # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
            #
            # USERS
            # Action: Create
            #
            # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
            olad i ''
            olad i "Users ($($DomainUsers.count) tasks)" -sh -air

            foreach ($User in $DomainUsers) {

                $ElementsCounter++
                $WriteProgressParameters = @{
                    Activity        = 'Configuring Active Directory'
                    Status          = "Item ($ElementsCounter / $NumberOfElementsToProcess): User: $($User.name)"
                    PercentComplete = (($ElementsCounter / $NumberOfElementsToProcess) * 100 )   

                }
                Write-Progress @WriteProgressParameters

                $NewDryADUserParams = @{
                    User      = $User
                    DomainNB  = $DomainNB
                    DryDeploy = $DryDeploy
                }
                switch ($ExecutionType) {
                    'Local' {
                        $NewDryADUserParams += @{
                            DomainController = $DomainController
                        }
                    }
                    'Remote' {
                        $NewDryADUserParams += @{
                            PSSession                   = $PSSession
                            DCPublicCertificateFilePath = $ConfigurationPublicCertificatePath
                        }
                    }
                }
                olad i "Creating User", "$($User.name)" 
                New-DryADUser @NewDryADUserParams
            }

            $DebugCounter += $NumberOfUsers
            if ($ElementsCounter -ne $DebugCounter) {
                throw "Elementscounter is $ElementsCounter, but was supposed to be $DebugCounter"
            }
        } # if ($ProcessUsers)


        # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
        #
        # USER'S GROUP MEMBERSHIPS
        # Action: Add Users to Groups
        #
        # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
        if ($ProcessUserMemberOf) {
            olad i ''
            olad i "User's Group Memberships ($NumberOfDomainUserMemberOf tasks)" -sh -air
            
            # Security Groups may have .member and .memberof
            foreach ($DomainUser in $DomainUsers) {
                foreach ($DomainUserMemberOf in $DomainUser.MemberOf) {
                    
                    # increment the element counter and update progress
                    $ElementsCounter++
                    $WriteProgressParameters = @{
                        Activity        = 'Configuring Active Directory'
                        Status          = "Item ($ElementsCounter / $NumberOfElementsToProcess): $($DomainUser.name) member of: $DomainUserMemberOf"
                        PercentComplete = (($ElementsCounter / $NumberOfElementsToProcess) * 100 )   
                    }
                    Write-Progress @WriteProgressParameters
        
                    # Add $DomainUser to each MemberOf
                    $AddDryADGroupMemberParams = @{
                        Group  = $DomainUserMemberOf
                        Member = $DomainUser.name
                    }
                    switch ($ExecutionType) {
                        'Local' {
                            $AddDryADGroupMemberParams += @{
                                DomainController = $DomainController
                            }
                        }
                        'Remote' {
                            $AddDryADGroupMemberParams += @{
                                PSSession = $PSSession
                            }
                        }
                    }
                    olad i "Configure User Group Members", "Adding '$($DomainUser.name)' to '$DomainUserMemberOf'"
                    Add-DryADGroupMember @AddDryADGroupMemberParams
                    Remove-Variable -Name AddDryADGroupMemberParams -ErrorAction Ignore
                }
            }
        
        
            $DebugCounter += $NumberOfDomainUserMemberOf
            if ($ElementsCounter -ne $DebugCounter) {
                throw "Elementscounter is $ElementsCounter, but was supposed to be $DebugCounter"
            }
        }

        # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
        #
        # ACTIVE DIRECTORY RIGHTS
        # Action: Delegate Rights in AD
        #
        # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
        if ($ProcessRights) {
            olad i ''
            olad i "Rights ($NumberOfDomainRights tasks)" -sh -air

            # Domain Rights
            foreach ($DomainSecurityGroup in $DomainSecurityGroups) {
                foreach ($DomainRight in $DomainSecurityGroup.Rights) {
                    olad v "Setting rights for group '$($DomainSecurityGroup.name)'"
                    
                    # create hash from properties of the object
                    Remove-Variable -Name SetDryADAccessRuleParams -ErrorAction Ignore
                    $SetDryADAccessRuleParams = @{}
                    
                    # Add all properties to the rights hash
                    $DomainRight.PSObject.Properties | foreach-Object {
                        $SetDryADAccessRuleParams.Add($_.Name, $_.Value)
                    }

                    # Add the name as 'Group' - the owner of the right
                    $SetDryADAccessRuleParams.Add('Group', $DomainSecurityGroup.name)

                    switch ($ExecutionType) {
                        'Local' {
                            $SetDryADAccessRuleParams += @{
                                DomainController = $DomainController
                            }
                        }
                        'Remote' {
                            $SetDryADAccessRuleParams += @{
                                PSSession = $PSSession
                            }
                        }
                    }

                    # Debug logging
                    olad d -hash $SetDryADAccessRuleParams

                    # increment the element counter and update progress
                    $ElementsCounter++
                    $WriteProgressParameters = @{
                        Activity        = 'Configuring Active Directory'
                        Status          = "Item ($ElementsCounter / $NumberOfElementsToProcess): Domain Right: $($DomainRight.Path)"
                        PercentComplete = (($ElementsCounter / $NumberOfElementsToProcess) * 100 )   
                    }
                    Write-Progress @WriteProgressParameters
                    
                    # Set the right
                    olad i "$($DomainSecurityGroup.name)", "$($DomainRight.Path)"
                    #olad i "ACL - granted on target", "$($DomainRight.Path)"
                    if ((Set-DryADAccessRule @SetDryADAccessRuleParams) -eq $true) {
                        #olad i '',''
                    } 
                    else {
                        olad e @("Rights", "Group '$($DomainSecurityGroup.name)', Target '$($DomainRight.Path)'")
                        throw "Failed: domain '$DomainFQDN': Group '$($DomainSecurityGroup.name)', Target '$($DomainRight.Path)'"
                    }

                    # Clean up
                    Remove-Variable -Name SetDryADAccessRuleParams, WriteProgressParameters -ErrorAction Ignore   
                }
            }

            $DebugCounter += $NumberOfDomainRights
            if ($ElementsCounter -ne $DebugCounter) {
                throw "Elementscounter is $ElementsCounter, but was supposed to be $DebugCounter"
            }
        }

        # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
        #
        # GROUP POLICIES
        # Action: Define paths, copy helper modules and GPOs to remote target if
        # ExecutionType 'Remote'
        #
        # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
        if ($ProcessGPOImports) {
            $SourceGPOsPath = Join-Path -Path $ConfigurationPath -ChildPath "gpo_imports"
            olad i ''
            olad i "GPO Imports - Copying helpers to remote" -sh -air
            switch ($ExecutionType) {
                'Remote' {
                    [string]$RemoteRootPath = "C:\DryDeploy\"
                    [string]$RemoteModulesPath = Join-Path -Path $RemoteRootPath -ChildPath 'modules'
                    [string]$GPOsPath = Join-Path -Path $RemoteRootPath -ChildPath 'gpo_imports'

                    # Only invoke if json-gpos in configuration
                    if ($RequiresGPOHelper) {
                        olad i "Copying helper modules to remote target"
                        $DryADGPOHelpersPath = Join-Path -Path (Split-Path -Path ((Get-Module -Name dry.module.ad).Path)) -ChildPath 'helpers\dry.ad.gpohelper' 
                        Copy-DryADFilesToRemoteTarget -PSSession $PSSession -TargetPath $RemoteModulesPath -SourcePath $DryADGPOHelpersPath | Out-Null
                        $DryADGPRegistryPolicyParserPath = Join-Path -Path (Split-Path -Path ((Get-Module -Name dry.module.ad).Path)) -ChildPath 'helpers\GPRegistryPolicyParser' 
                        Copy-DryADFilesToRemoteTarget -PSSession $PSSession -TargetPath $RemoteModulesPath -SourcePath $DryADGPRegistryPolicyParserPath | Out-Null
                    }

                    Add-DryADPSModulesPath -PSSession $PSSession -Path $RemoteModulesPath -Modules @('dry.ad.gpohelper','GPRegistryPolicyParser') | Out-Null
                    Copy-DryADFilesToRemoteTarget -PSSession $PSSession -TargetPath $RemoteRootPath -SourcePath $SourceGPOsPath | Out-Null
                }
                'Local' {
                    [string]$GPOsPath = $SourceGPOsPath
                }
            }  

            # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
            #
            # GROUP POLICIES
            # Action: Import
            #
            # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
            olad i ''
            olad i "GPO Imports ($($DomainGPOImports.count) tasks)" -sh -air
            foreach ($GPO in $DomainGPOImports) {
                # Ensure TargetName exists, and is converted to the desired case
                if ($null -eq $GPO.TargetName) { 
                    $GPO | Add-Member -MemberType NoteProperty -Name 'TargetName' -Value $(ConvertTo-DryADCase -Name $GPO.Name -Case $GPOcase)
                }
                else {
                    $GPO.TargetName = ConvertTo-DryADCase -Name $GPO.TargetName -Case $GPOcase
                }

                # increment the element counter and update progress
                $ElementsCounter++
                $WriteProgressParameters = @{
                    Activity        = 'Configuring Active Directory'
                    Status          = "Item ($ElementsCounter / $NumberOfElementsToProcess): Importing GPO '$($GPO.TargetName)'"
                    PercentComplete = (($ElementsCounter / $NumberOfElementsToProcess) * 100 )   
                }
                Write-Progress @WriteProgressParameters
                
                $ImportDryADGPOParams = @{
                    GPO             = $GPO 
                    GPOsPath        = $GPOsPath 
                    ReplacementHash = $ReplacementHash
                }

                switch ($ExecutionType) {
                    'Local' {
                        $ImportDryADGPOParams += @{
                            DomainController = $DomainController
                        }
                    }
                    'Remote' {
                        $ImportDryADGPOParams += @{
                            PSSession = $PSSession
                        }
                    }
                }
                olad i "GPO Import", "$($GPO.Name)"
                Import-DryADGPO @ImportDryADGPOParams      
            }

            $DebugCounter += $NumberOfGPOImports
            if ($ElementsCounter -ne $DebugCounter) {
                throw "Elementscounter is $ElementsCounter, but was supposed to be $DebugCounter"
            }

        } # if ($ProcessGPO)


        # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
        #
        # GROUP POLICIES
        # Action: Links
        #
        # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #

        if ($ProcessGPOLinks) {
            olad i ''
            olad i "GPO Links ($NumberOfGPOLinks tasks)" -sh -air
            foreach ($DomainGPOLink in $DomainGPOLinks | Where-Object { $_.defined_in -eq 'OS' }) {
                
                # increment the element counter and update progress
                $ElementsCounter += $DomainGPOLink.gplinks.count
                $WriteProgressParameters = @{
                    Activity        = 'Configuring Active Directory'
                    Status          = "Item ($ElementsCounter / $NumberOfElementsToProcess): Linking GPOs to '$($DomainGPOLink.Path)'"
                    PercentComplete = (($ElementsCounter / $NumberOfElementsToProcess) * 100 )   
                }
                Write-Progress @WriteProgressParameters
                 
                $SetDryGPLinkParams = @{
                    GPOLinkObject = $DomainGPOLink
                    DomainDN      = $DomainDN
                    DomainFQDN    = $DomainFQDN
                }

                switch ($ExecutionType) {
                    'Local' {
                        $SetDryGPLinkParams += @{
                            DomainController = $DomainController
                        }
                    }
                    'Remote' {
                        $SetDryGPLinkParams += @{
                            PSSession = $PSSession
                        }
                    }
                }

                $DomainGPOLinkPath = $DomainGPOLink.Path
                if ($DomainGPOLinkPath -eq '') {
                    $DomainGPOLinkPath = '<domain root>'
                }
                olad i "Link GPOs to OU", "$DomainGPOLinkPath"
                Set-DryADGPLink @SetDryGPLinkParams 
            }

            foreach ($DomainGPOLink in $DomainGPOLinks | Where-Object { $_.defined_in -ne 'OS' }) {
                
                # increment the element counter and update progress
                $ElementsCounter += $DomainGPOLink.gplinks.count
                $WriteProgressParameters = @{
                    Activity        = 'Configuring Active Directory'
                    Status          = "Item ($ElementsCounter / $NumberOfElementsToProcess): Linking GPOs to '$($DomainGPOLink.Path)'"
                    PercentComplete = (($ElementsCounter / $NumberOfElementsToProcess) * 100 )   
                }
                Write-Progress @WriteProgressParameters
                 
                $SetDryGPLinkParams = @{
                    GPOLinkObject = $DomainGPOLink
                    DomainDN      = $DomainDN
                    DomainFQDN    = $DomainFQDN
                }
                switch ($ExecutionType) {
                    'Local' {
                        $SetDryGPLinkParams += @{
                            DomainController = $DomainController
                        }
                    }
                    'Remote' {
                        $SetDryGPLinkParams += @{
                            PSSession = $PSSession
                        }
                    }
                }

                $DomainGPOLinkPath = $DomainGPOLink.Path
                if ($DomainGPOLinkPath -eq '') {
                    $DomainGPOLinkPath = '<domain root>'
                }
                olad i "Link GPOs to OU", "$DomainGPOLinkPath"
                Set-DryADGPLink @SetDryGPLinkParams 
            }

            $DebugCounter += $NumberOfGPOLinks
            if ($ElementsCounter -ne $DebugCounter) {
                throw "Elementscounter is $ElementsCounter, but was supposed to be $DebugCounter"
            }
        } # if ($ProcessGPOLinks)
        
        # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
        #
        # Wmi Filter Links
        # Action: Link to GPOs
        #
        # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #

        if ($ProcessWMIFilterLinks) {
            olad i ''
            olad i "WMIFilterLinks ($DomainWmiFilterLinksCount tasks)" -sh -air
            foreach ($GPOWMIFilter in $DomainWMIFilters) {
                foreach ($GPOWMIFilterLink in $GPOWMIFilter.links) {
                    # Progress
                    $ElementsCounter++  
                    $WriteProgressParameters = @{
                        Activity        = 'Configuring Active Directory'
                        Status          = "Item ($ElementsCounter / $NumberOfElementsToProcess): Linking WMIFilter '$($GPOWMIFilter.Name)'"
                        PercentComplete = (($ElementsCounter / $NumberOfElementsToProcess) * 100 )
                    }
                    Write-Progress @WriteProgressParameters
                
                    $SetDryWmiFilterLinkParams = @{
                        GPOName       = $GPOWMIFilterLink
                        WMIFilterName = $GPOWMIFilter.Name
                    }
                    switch ($ExecutionType) {
                        'Local' {
                            $SetDryWmiFilterLinkParams += @{
                                DomainController = $DomainController
                            }
                        }
                        'Remote' {
                            $SetDryWmiFilterLinkParams += @{
                                PSSession = $PSSession
                            }
                        }
                    }
                    olad i "WMI Filter Link", "$($GPOWMIFilter.Name)"
                    Set-DryADWmiFilterLink @SetDryWmiFilterLinkParams
                }
            }

            $DebugCounter += $NumberOfWMIFilterLinks
            if ($ElementsCounter -ne $DebugCounter) {
                throw "Elementscounter is $ElementsCounter, but was supposed to be $DebugCounter"
            }
        }
        olad i ''
        olad i "Import-DryADConfiguration ran successfully" -h -air
    }
    catch {
        $PSCmdLet.ThrowTerminatingError($_)
    }
    finally {
        Write-Progress -Completed -Activity "Configuring AD objects"
    }
}