ADMF.psm1

$script:ModuleRoot = $PSScriptRoot
$script:ModuleVersion = (Import-PowerShellDataFile -Path "$($script:ModuleRoot)\ADMF.psd1").ModuleVersion

# Detect whether at some level dotsourcing was enforced
$script:doDotSource = Get-PSFConfigValue -FullName ADMF.Import.DoDotSource -Fallback $false
if ($ADMF_dotsourcemodule) { $script:doDotSource = $true }

<#
Note on Resolve-Path:
All paths are sent through Resolve-Path/Resolve-PSFPath in order to convert them to the correct path separator.
This allows ignoring path separators throughout the import sequence, which could otherwise cause trouble depending on OS.
Resolve-Path can only be used for paths that already exist, Resolve-PSFPath can accept that the last leaf my not exist.
This is important when testing for paths.
#>


# Detect whether at some level loading individual module files, rather than the compiled module was enforced
$importIndividualFiles = Get-PSFConfigValue -FullName ADMF.Import.IndividualFiles -Fallback $false
if ($ADMF_importIndividualFiles) { $importIndividualFiles = $true }
if (Test-Path (Resolve-PSFPath -Path "$($script:ModuleRoot)\..\.git" -SingleItem -NewChild)) { $importIndividualFiles = $true }
if ("<was compiled>" -eq '<was not compiled>') { $importIndividualFiles = $true }
    
function Import-ModuleFile
{
    <#
        .SYNOPSIS
            Loads files into the module on module import.
         
        .DESCRIPTION
            This helper function is used during module initialization.
            It should always be dotsourced itself, in order to proper function.
             
            This provides a central location to react to files being imported, if later desired
         
        .PARAMETER Path
            The path to the file to load
         
        .EXAMPLE
            PS C:\> . Import-ModuleFile -File $function.FullName
     
            Imports the file stored in $function according to import policy
    #>

    [CmdletBinding()]
    Param (
        [string]
        $Path
    )
    
    $resolvedPath = $ExecutionContext.SessionState.Path.GetResolvedPSPathFromPSPath($Path).ProviderPath
    if ($doDotSource) { . $resolvedPath }
    else { $ExecutionContext.InvokeCommand.InvokeScript($false, ([scriptblock]::Create([io.file]::ReadAllText($resolvedPath))), $null, $null) }
}

#region Load individual files
if ($importIndividualFiles)
{
    # Execute Preimport actions
    . Import-ModuleFile -Path "$ModuleRoot\internal\scripts\preimport.ps1"
    
    # Import all internal functions
    foreach ($function in (Get-ChildItem "$ModuleRoot\internal\functions" -Filter "*.ps1" -Recurse -ErrorAction Ignore))
    {
        . Import-ModuleFile -Path $function.FullName
    }
    
    # Import all public functions
    foreach ($function in (Get-ChildItem "$ModuleRoot\functions" -Filter "*.ps1" -Recurse -ErrorAction Ignore))
    {
        . Import-ModuleFile -Path $function.FullName
    }
    
    # Execute Postimport actions
    . Import-ModuleFile -Path "$ModuleRoot\internal\scripts\postimport.ps1"
    
    # End it here, do not load compiled code below
    return
}
#endregion Load individual files

#region Load compiled code
<#
This file loads the strings documents from the respective language folders.
This allows localizing messages and errors.
Load psd1 language files for each language you wish to support.
Partial translations are acceptable - when missing a current language message,
it will fallback to English or another available language.
#>

Import-PSFLocalizedString -Path "$($script:ModuleRoot)\en-us\*.psd1" -Module 'ADMF' -Language 'en-US'

function Invoke-CallbackMenu
{
    <#
    .SYNOPSIS
        Calls a GUI window to pick the contexts for a specific server.
     
    .DESCRIPTION
        Calls a GUI window to pick the contexts for a specific server.
        This is used when invoking Set-AdmfContext with the (hidden) -Callback parameter.
        It is designed to be triggered automatically when trying to manage a forest / domain
        that has not yet had its context defined.
 
        Note: This makes it critical to define a context first when doing unattended automation.
     
    .PARAMETER Server
        The server / domain being connected to.
        Used for documentation purposes, as well as to potentially determine initial checkbox state.
     
    .PARAMETER Credential
        The credentials to use for this operation.
     
    .EXAMPLE
        PS C:\> Invoke-CallbackMenu -Server contoso.com
 
        Shows the context selection menu for the domain contoso.com
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]
        $Server,
        
        [pscredential]
        $Credential
    )
    
    begin
    {
        #region Utility Functions
        function New-CheckBox
        {
            [CmdletBinding()]
            param (
                $ContextObject,
                
                $Parent
            )
            
            $column = $Parent.Controls.Count % 2
            $row = [math]::Truncate(($Parent.Controls.Count / 2))
            
            $checkbox = [System.Windows.Forms.CheckBox]::new()
            $checkbox.Width = 200
            $checkbox.Height = 20
            $checkbox.AutoSize = $false
            $checkbox.Location = [System.Drawing.Point]::new((210 * $column + 15), (25 * $row + 15))
            $checkbox.Text = $ContextObject.Name
            $checkbox.Font = 'Microsoft Sans Serif,10'
            $null = $Parent.Controls.Add($checkbox)
            $tooltip = [System.Windows.Forms.ToolTip]::new()
            $tooltip.ToolTipTitle = $ContextObject.Name
            $tooltipText = $ContextObject.Description
            if ($ContextObject.Prerequisites.Count -gt 0) { $tooltipText += "`nPrerequisites: $($ContextObject.Prerequisites -join ', ')" }
            if ($ContextObject.MutuallyExclusive.Count -gt 0) { $tooltipText += "`nMutually exclusive with: $($ContextObject.MutuallyExclusive -join ', ')" }
            $tooltip.SetToolTip($checkbox, $tooltipText)
            
            $checkbox.Add_CheckedChanged({ Update-Checkbox })
            
            $checkbox
        }
        
        function Update-Checkbox
        {
            [CmdletBinding()]
            param ()
            
            # Exemption: Accessing superscope variables directly. Forms and their events are screwey enough.
            
            foreach ($checkbox in $contextCheckboxes.Values)
            {
                $checkbox.Enabled = $true
            }
            foreach ($contextObject in $allContexts)
            {
                foreach ($prerequisite in $contextObject.Prerequisites)
                {
                    if (-not $contextCheckboxes[$prerequisite].Checked)
                    {
                        $contextCheckboxes[$contextObject.Name].Enabled = $false
                        $contextCheckboxes[$contextObject.Name].Checked = $false
                        break
                    }
                }
                foreach ($exclusion in $contextObject.MutuallyExclusive)
                {
                    if (-not $contextCheckboxes[$contextObject.Name].Checked) { break }
                    if (-not $contextCheckboxes[$exclusion]) { continue }
                    
                    $contextCheckboxes[$exclusion].Enabled = $false
                    $contextCheckboxes[$exclusion].Checked = $false
                }
            }
        }
        
        function New-Form
        {
            [OutputType([System.Windows.Forms.Form])]
            [CmdletBinding()]
            param ()
            
            New-Object System.Windows.Forms.Form -Property @{
                ClientSize = '500,500'
                Text       = "Context Selection"
                TopMost    = $false
                AutoSize   = $false
            }
        }
        
        function New-GroupBox
        {
            [OutputType([System.Windows.Forms.Groupbox])]
            [CmdletBinding()]
            param (
                [string]
                $Text,
                
                [int]
                $Height,
                
                $Form
            )
            
            $newHeight = 10
            if ($Form.Controls.Count -gt 0)
            {
                $last = $Form.Controls | Sort-Object { $_.Location.Y } -Descending | Select-Object -First 1
                $newHeight = 10 + $last.Height + $last.Location.Y
            }
            
            $groupBox = New-Object System.Windows.Forms.Groupbox -Property @{
                Height   = $Height
                Width    = 480
                Text     = $Text
                AutoSize = $false
                Location = (New-Object System.Drawing.Point(10, $newHeight))
            }
            $Form.Controls.Add($groupBox)
            $groupBox
        }
        
        function New-Label
        {
            [CmdletBinding()]
            param (
                [string]
                $Text,
                
                $Parent
            )
            
            $label = New-Object system.Windows.Forms.Label -Property @{
                Text     = $Text
                AutoSize = $false
                Font     = 'Microsoft Sans Serif,10'
                Location = (New-Object System.Drawing.Point(10, 15))
                Width    = 460
                TextAlign = 'MiddleCenter'
            }
            
            $Parent.Controls.Add($label)
        }
        #endregion Utility Functions
        
        #region Form
        [System.Windows.Forms.Application]::EnableVisualStyles()
        
        $form = New-Form
        $group_Server = New-GroupBox -Text "Selected Domain / Server" -Height 50 -Form $form
        New-Label -Text $Server -Parent $group_Server
        
        #region Contexts
        $allContexts = Get-AdmfContext
        $groupedContexts = $allContexts | Group-Object Group
        $contextCheckboxes = @{ }
        foreach ($groupedContext in $groupedContexts)
        {
            $rows = [math]::Round(($groupedContext.Group.Count / 2), [System.MidpointRounding]::AwayFromZero)
            $group_Context = New-GroupBox -Text $groupedContext.Name -Height ($rows * 25 + 15) -Form $form
            foreach ($contextObject in ($groupedContext.Group | Sort-Object Name))
            {
                $contextCheckboxes[$contextObject.Name] = New-CheckBox -ContextObject $contextObject -Parent $group_Context
            }
        }
        
        $parameters = $PSBoundParameters | ConvertTo-PSFHashtable -Include Server, Credential
        if ($parameters.Server -eq '<Default Domain>') { $parameters.Server = $env:USERDNSDOMAIN }
        foreach ($context in $allContexts | Sort-Object Weight)
        {
            $path = Join-Path $context.Path 'contextPromptChecked.ps1'
            if (Test-Path $path)
            {
                try
                {
                    $result = & $path @parameters
                    if ($result) { $contextCheckboxes[$context.Name].Checked = $true }
                }
                catch { Write-PSFMessage -Level Warning -String 'Invoke-CallbackMenu.Context.Checked.Error' -StringValues $context.Name -ErrorRecord $_ }
            }
        }
        
        Update-Checkbox
        #endregion Contexts
        
        #region Buttons
        $button_Cancel = New-Object system.Windows.Forms.Button -Property @{
            Text = 'Cancel'
            Width = 60
            Height = 30
            Anchor = 'right,bottom'
            Location = (New-Object System.Drawing.Point(426, 460))
            Font = 'Microsoft Sans Serif,10'
        }
        $form.Controls.Add($button_Cancel)
        $button_OK = New-Object system.Windows.Forms.Button -Property @{
            Text     = 'OK'
            Width    = 38
            Height   = 30
            Anchor   = 'right,bottom'
            Location = (New-Object System.Drawing.Point(378, 460))
            Font     = 'Microsoft Sans Serif,10'
        }
        $form.Controls.Add($button_OK)
        #endregion Buttons
        
        #region Other Stuff
        $okbox = [System.Windows.Forms.CheckBox]::new()
        $okbox.Visible = $false
        $form.Controls.Add($okbox)
        
        $button_OK.Add_Click({
                $okbox.Checked = $true
                $this.Parent.Close()
            })
        $form.ShowIcon = $false
        $form.CancelButton = $button_Cancel
        $form.AcceptButton = $button_OK
        
        $last = $form.Controls | Where-Object { $_ -is [System.Windows.Forms.Groupbox] } | Sort-Object { $_.Location.Y } -Descending | Select-Object -First 1
        $newHeight = 90 + $last.Height + $last.Location.Y
        $form.Height = $newHeight
        #endregion Other Stuff
        #endregion Form
    }
    process
    {
        $null = $form.ShowDialog()
        if (-not $okbox.Checked) { throw "Interrupting: User cancelled operation" }
        
        $selectedNames = @(($contextCheckboxes.Values | Where-Object Checked).Text)
        $allContexts | Where-Object Name -In $selectedNames
    }
}


function Invoke-PostCredentialProvider {
    <#
        .SYNOPSIS
            Executes the PostScript action of a credential provider.
 
        .DESCRIPTION
            Executes the PostScript action of a credential provider.
 
        .PARAMETER ProviderName
            Name of the credential provider to use.
 
        .PARAMETER Server
            The original server targeted.
 
        .PARAMETER Credential
            The original credentials specified by the user.
 
        .PARAMETER Cmdlet
            The $PSCmdlet object of the calling command.
            Used to kill it with maximum prejudice in case of error.
 
        .EXAMPLE
            PS C:\> Invoke-PostCredentialProvider -ProviderName $CredentialProvider -Server $originalArgument.Server -Credential $originalArgument.Credential
 
            Performs any post-execution action registered for the $CredentialProvider (if any)
    #>

    [CmdletBinding()]
    param (
        [string]
        $ProviderName,

        [PSFComputer]
        $Server,

        [AllowNull()]
        [PSCredential]
        $Credential,

        [System.Management.Automation.PSCmdlet]
        $Cmdlet
    )

    if (-not $script:credentialProviders[$ProviderName]) {
        Stop-PSFFunction -String 'Invoke-PostCredentialProvider.Provider.NotFound' -StringValues $ProviderName -EnableException $true -Cmdlet $Cmdlet
    }

    if (-not $script:credentialProviders[$ProviderName].PostScript) { return }

    $argument = [PSCustomObject]@{
        Server = $Server
        Credential = $Credential
    }

    try { $null = $script:credentialProviders[$ProviderName].PostScript.Invoke($argument) }
    catch {
        Stop-PSFFunction -String 'Invoke-PostCredentialProvider.Provider.ExecutionError' -StringValues $ProviderName -EnableException $true -ErrorRecord $_ -Cmdlet $Cmdlet
    }
}

function Invoke-PreCredentialProvider {
    <#
        .SYNOPSIS
            Resolves credentials to use using the registered credential provider.
 
        .DESCRIPTION
            Resolves credentials to use using the registered credential provider.
 
        .PARAMETER ProviderName
            Name of the credential provider to use.
 
        .PARAMETER Server
            The server to connect to.
 
        .PARAMETER Credential
            The credentials specified by the user.
 
        .PARAMETER Parameter
            The parameter object resolved from the original user input.
 
        .PARAMETER Cmdlet
            The $PSCmdlet object of the calling command.
            Used to kill it with maximum prejudice in case of error.
 
        .EXAMPLE
            PS C:\> $originalArgument = Invoke-PreCredentialProvider @parameters -ProviderName $CredentialProvider -Parameter $parameters
 
            Resolves the credentials to use and automatically injects them into the $parameters hashtable.
            Also returns the original input for use when invoking the PostScript scriptblock of the provider.
    #>

    [CmdletBinding()]
    param (
        [string]
        $ProviderName,

        [PSFComputer]
        $Server,

        [AllowNull()]
        [PSCredential]
        $Credential,

        [Hashtable]
        $Parameter,

        [System.Management.Automation.PSCmdlet]
        $Cmdlet
    )
    
    if (-not $script:credentialProviders[$ProviderName])
    {
        Write-PSFMessage -Level Warning -String 'Invoke-PreCredentialProvider.Provider.NotFound' -StringValues $ProviderName
        Stop-PSFFunction -String 'Invoke-PreCredentialProvider.Provider.NotFound' -StringValues $ProviderName -EnableException $true -Cmdlet $Cmdlet
    }

    $argument = [PSCustomObject]@{
        Server = $Server
        Credential = $Credential
    }

    try { $results = $script:credentialProviders[$ProviderName].PreScript.Invoke($argument) | Where-Object { $_ -is [PSCredential] } | Select-Object -First 1 }
    catch
    {
        Write-PSFMessage -Level Warning -String 'Invoke-PreCredentialProvider.Provider.ExecutionError' -StringValues $ProviderName -ErrorRecord $_
        Stop-PSFFunction -String 'Invoke-PreCredentialProvider.Provider.ExecutionError' -StringValues $ProviderName -EnableException $true -ErrorRecord $_ -Cmdlet $Cmdlet
    }

    if ($results) {
        $Parameter['Credential'] = $results
    }
    elseif ($Parameter.ContainsKey('Credential')) { $Parameter.Remove('Credential') }
    
    return $argument
}

function Resolve-DomainController
{
    <#
    .SYNOPSIS
        Resolves a domain to a specific domaincontroller.
     
    .DESCRIPTION
        Resolves a domain to a specific domaincontroller.
     
    .PARAMETER Server
        The server / domain to work with.
     
    .PARAMETER Credential
        The credentials to use for this operation.
     
    .PARAMETER Type
        The type of DC to resolve to.
        Governed by the 'ADMF.DCSelectionMode' configuration setting.
     
    .EXAMPLE
        PS C:\> Resolve-DomainController @parameters
 
        Picks the server to work against.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseDeclaredVarsMoreThanAssignments", "")]
    [OutputType([string])]
    [CmdletBinding()]
    param (
        [PSFComputer]
        $Server,
        
        [PSCredential]
        $Credential,

        [ValidateSet('PDCEmulator', 'Random')]
        [string]
        $Type = (Get-PSFConfigValue -FullName 'ADMF.DCSelectionMode' -Fallback 'PDCEmulator')
    )
    
    begin
    {
        $parameters = $PSBoundParameters | ConvertTo-PSFHashtable -Include Server, Credential
        $parameters['Debug'] = $false
    }
    process
    {
        $targetString = $Server
        if (-not $targetString) { $targetString = $env:USERDNSNAME }
        $null = Invoke-PSFProtectedCommand -ActionString 'Resolve-DomainController.Connecting' -ActionStringValues $targetString -Target $targetString -ScriptBlock {
            $domainController = Get-ADDomainController @parameters -ErrorAction Stop
        } -PSCmdlet $PSCmdlet -EnableException $true -RetryCount 5 -RetryWait 2
        
        if ($domainController.HostName -eq $Server) {
            Write-PSFMessage -Level Host -String 'Resolve-DomainController.Resolved' -StringValues $domainController.HostName
            return $domainController.HostName
        }
        if ($domainController.Name -eq $Server) {
            Write-PSFMessage -Level Host -String 'Resolve-DomainController.Resolved' -StringValues $domainController.Name
            return $domainController.Name
        }

        switch ($Type) {
            'Random' {
                Write-PSFMessage -Level Host -String 'Resolve-DomainController.Resolved' -StringValues $domainController.HostName
                return $domainController.HostName
            }
            default {
                $domain = Get-ADDomain @parameters
                Write-PSFMessage -Level Host -String 'Resolve-DomainController.Resolved' -StringValues $domain.PDCEmulator
                $domain.PDCEmulator
            }
        }
    }
}


function Export-AdmfGpo
{
    <#
    .SYNOPSIS
        Creates an export of GPO objects for use in the Domain Management module.
     
    .DESCRIPTION
        Creates an export of GPO objects for use in the Domain Management module.
        Use this command to record new GPO data for the module.
     
    .PARAMETER Path
        The path to which to export the GPOs.
     
    .PARAMETER GpoObject
        The GPO objects to export.
        Only accepts output of Get-GPO
 
    .PARAMETER Domain
        The domain to export from.
     
    .EXAMPLE
        PS C:\> Get-GPO -All | Where-Object DisplayName -like 'AD-D-SEC-T0*' | Export-AdmfGpo -Path .
 
        Exports all GPOs named like 'AD-D-SEC-T0*' to the current path
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSReviewUnusedParameter", "")]
    [CmdletBinding()]
    Param (
        [PsfValidateScript('ADMF.Validate.Path', ErrorString = 'ADMF.Validate.Path')]
        [Parameter(Mandatory = $true)]
        [string]
        $Path,

        [PsfValidateScript('ADMF.Validate.Type.Gpo', ErrorString = 'ADMF.Validate.Type.Gpo')]
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        $GpoObject,

        [string]
        $Domain = $env:USERDNSDOMAIN
    )
    
    begin
    {
        $resolvedPath = Resolve-PSFPath -Path $Path -Provider FileSystem -SingleItem
        $backupCmd = { Backup-GPO -Path $resolvedPath -Domain $Domain }
        $backupGPO = $backupCmd.GetSteppablePipeline()
        $backupGPO.Begin($true)

        [System.Collections.ArrayList]$gpoData = @()
        $exportID = (New-Guid).ToString()
    }
    process
    {
        foreach ($gpoItem in $GpoObject) {
            $exportData = $backupGPO.Process(($gpoItem | Select-PSFObject 'ID as GUID'))
            $data = [PSCustomObject]@{
                DisplayName = $gpoItem.DisplayName
                Description = $gpoItem.Description
                ID = "{$($exportData.ID.ToString().ToUpper())}"
                ExportID = $exportID
            }
            $null = $gpoData.Add($data)
        }
    }
    end
    {
        $backupGPO.End()
        $gpoData | ConvertTo-Json | Set-Content "$resolvedPath\exportData.json"

        # Remove hidden attribute, top prevent issues with copy over WinRM
        foreach ($fsItem in (Get-ChildItem -Path $resolvedPath -Recurse -Force)) {
            $fsItem.Attributes = $fsItem.Attributes -band [System.IO.FileAttributes]::Directory
        }
    }
}


function Get-AdmfContext
{
<#
    .SYNOPSIS
        Return available contexts.
     
    .DESCRIPTION
        Return available contexts.
        By default, only the latest version of any given context will be returned.
     
    .PARAMETER Name
        The name of the context to filter by.
     
    .PARAMETER Store
        The context stores to look in.
     
    .PARAMETER All
        Return all versions of any given context, rather than just the latest version.
 
    .PARAMETER Current
        Displays the currently active contexts.
 
    .PARAMETER Importing
        Return the contexts that are currently being imported.
        Use this to react from within your context's scriptblocks to any other context that is selected.
        This parameter only has meaning when used within a context's scriptblocks.
     
    .EXAMPLE
        PS C:\> Get-AdmfContext
     
        Returns the latest version of all available contexts.
#>

    [CmdletBinding(DefaultParameterSetName = 'Search')]
    param (
        [Parameter(ParameterSetName = 'Search')]
        [string]
        $Name = '*',
        
        [Parameter(ParameterSetName = 'Search')]
        [string]
        $Store = '*',
        
        [Parameter(ParameterSetName = 'Search')]
        [switch]
        $All,
        
        [Parameter(ParameterSetName = 'Current')]
        [switch]
        $Current,

        [Parameter(ParameterSetName = 'Importing')]
        [switch]
        $Importing
    )
    
    process
    {
        if ($Current)
        {
            return $script:loadedContexts
        }
        if ($Importing)
        {
            return (Get-PSFTaskEngineCache -Module ADMF -Name currentlyImportingContexts)
        }
        $contextStores = Get-AdmfContextStore -Name $Store
        $allContextData = foreach ($contextStore in $contextStores)
        {
            if (-not (Test-Path $contextStore.Path)) { continue }
            foreach ($folder in (Get-ChildItem -Path $contextStore.Path -Filter $Name -Directory))
            {
                $versionFolders = Get-ChildItem -Path $folder.FullName -Directory | Where-Object { $_.Name -as [version] } | Sort-Object { [version]$_.Name } -Descending
                if (-not $All) { $versionFolders = $versionFolders | Select-Object -First 1 }
                
                foreach ($versionFolder in $versionFolders)
                {
                    $resultObject = [pscustomobject]@{
                        PSTypeName = 'ADMF.Context'
                        Name       = $folder.Name
                        Version    = ($versionFolder.Name -as [version])
                        Store       = $contextStore.Name
                        Path       = $versionFolder.FullName
                        Description = ''
                        Weight       = 50
                        Author       = ''
                        Prerequisites = @()
                        MutuallyExclusive = @()
                        Group       = 'Default'
                    }
                    if (Test-Path -Path "$($versionFolder.FullName)\context.json")
                    {
                        $contextData = Get-Content -Path "$($versionFolder.FullName)\context.json" | ConvertFrom-Json
                        if ($contextData.Weight -as [int]) { $resultObject.Weight = $contextData.Weight -as [int] }
                        if ($contextData.Description) { $resultObject.Description = $contextData.Description }
                        if ($contextData.Author) { $resultObject.Author = $contextData.Author }
                        if ($contextData.Prerequisites) { $resultObject.Prerequisites = $contextData.Prerequisites }
                        if ($contextData.MutuallyExclusive) { $resultObject.MutuallyExclusive = $contextData.MutuallyExclusive }
                        if ($contextData.Group) { $resultObject.Group = $contextData.Group }
                    }
                    
                    $resultObject
                }
            }
        }
        
        if ($All) { return $allContextData }
        
        # Only return highest version if -All has not been set
        # The same context name might be stored in multiple stores
        $allContextData | Group-Object Name | ForEach-Object {
            $_.Group | Sort-Object Version -Descending | Select-Object -First 1 | Select-PSFObject -TypeName 'ADMF.Context'
        }
    }
}

function Get-AdmfContextStore
{
<#
    .SYNOPSIS
        Returns the list of available context stores.
     
    .DESCRIPTION
        Returns the list of available context stores.
     
    .PARAMETER Name
        The name to filter by.
     
    .EXAMPLE
        PS C:\> Get-AdmfContextStore
     
        Returns all available context stores.
#>

    [CmdletBinding()]
    param (
        [string]
        $Name = '*'
    )
    
    process
    {
        foreach ($config in (Get-PSFConfig -FullName "ADMF.Context.Store.$Name"))
        {
            [PSCustomObject]@{
                PSTypeName = 'ADMF.Context.Store'
                Name       = $config.Name -replace '^Context\.Store\.'
                Path       = $config.Value
                PathExists = (Test-Path $config.Value)
            }
        }
    }
}

function Invoke-AdmfDomain
{
    <#
    .SYNOPSIS
        Brings a domain into compliance with the desired state.
     
    .DESCRIPTION
        Brings a domain into compliance with the desired state.
        It implements a wide variety of settings against the targeed domain,
        whether it be OUs, groups, users, gpos, acls or many more items.
 
        Note on order:
        - OU Creation and Updating should be done first, but DELETING ous (OUHard) should be one of the last operations performed.
        - Acl & Access operations should be performed last
        - Managing group policy yields best results in this order:
          1. Create new GPO
          2. Create Links, only disabling undesired links
          3. Delete unneeded GPO
          4. Delete undesired links
          This is due to the fact that "unneeded GPO" are detected by being linked into managed GPOs.
     
    .PARAMETER Server
        The server / domain to work with.
     
    .PARAMETER Credential
        The credentials to use for this operation.
     
    .PARAMETER Options
        The various operations that are supported.
        By default "default" operations are executed against the targeted domain.
        - Acl : The basic permission behavior of an object (e.g.: Owner, Inheritance)
        - GPLink : Manages the linking of group policies.
        - GPPermission : Managing permissions on group policy objects.
        - GroupPolicy : Deploying and updating GPOs.
        - GroupMembership : Assigning group membership
        - Group : Creating groups
        - OUSoft : Creating & modifying OUs, but not deleting them
        - OUHard : Creating, Modifying & Deleting OUs. This exists in order to be able to create
                   new OUs, then move all objects over and only when done deleting undesired OUs.
                   Will NOT delete OUs that contain objects.!
        - PSO : Implementing Finegrained Password Policies
        - Object : Custom AD object
        - User : Managing User objects
        - GPLinkDisable : Creating GP Links, but only disabling undesired links.
                          This is needed in order to detect undesired GPOs to delete:
                          Those linked when they shouldn't be!
        - GroupPolicyDelete : Deploy, update and delete Group Policy objects.
 
    .PARAMETER CredentialProvider
        The credential provider to use to resolve the input credentials.
        See help on Register-AdmfCredentialProvider for details.
 
    .PARAMETER Confirm
        If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.
     
    .PARAMETER WhatIf
        If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run.
     
    .EXAMPLE
        PS C:\> Invoke-AdmfDomain
 
        Brings the current domain into compliance with the desired state.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPlainTextForPassword', '')]
    [CmdletBinding(SupportsShouldProcess = $true)]
    Param (
        [PSFComputer]
        $Server,

        [PSCredential]
        $Credential,

        [ADMF.UpdateDomainOptions[]]
        $Options = 'Default',

        [string]
        $CredentialProvider = 'default'
    )
    
    begin
    {
        $parameters = $PSBoundParameters | ConvertTo-PSFHashtable -Include Server, Credential
        $originalArgument = Invoke-PreCredentialProvider @parameters -ProviderName $CredentialProvider -Parameter $parameters -Cmdlet $PSCmdlet
        try { $dcServer = Resolve-DomainController @parameters }
        catch {
            Invoke-PostCredentialProvider -ProviderName $CredentialProvider -Server $originalArgument.Server -Credential $originalArgument.Credential -Cmdlet $PSCmdlet
            throw
        }
        $parameters.Server = $dcServer
        Invoke-PSFCallback -Data $parameters -EnableException $true -PSCmdlet $PSCmdlet
        Set-AdmfContext @parameters -Interactive -ReUse -EnableException
        $parameters += $PSBoundParameters | ConvertTo-PSFHashtable -Include WhatIf, Confirm, Verbose, Debug
        $parameters.Server = $dcServer
        [ADMF.UpdateDomainOptions]$newOptions = $Options
    }
    process
    {
        try
        {
            if ($newOptions -band [UpdateDomainOptions]::OUSoft)
            {
                if (Get-DMOrganizationalUnit)
                {
                    Write-PSFMessage -Level Host -String 'Invoke-AdmfDomain.Executing.Invoke' -StringValues 'OrganizationalUnits - Create & Modify', $parameters.Server
                    Invoke-DMOrganizationalUnit @parameters
                }
                else { Write-PSFMessage -Level Host -String 'Invoke-AdmfDomain.Skipping.Test.NoConfiguration' -StringValues 'OrganizationalUnits - Create & Modify' }
            }
            if ($newOptions -band [UpdateDomainOptions]::Group)
            {
                if (Get-DMGroup)
                {
                    Write-PSFMessage -Level Host -String 'Invoke-AdmfDomain.Executing.Invoke' -StringValues 'Groups', $parameters.Server
                    Invoke-DMGroup @parameters
                }
                else { Write-PSFMessage -Level Host -String 'Invoke-AdmfDomain.Skipping.Test.NoConfiguration' -StringValues 'Groups' }
            }
            if ($newOptions -band [UpdateDomainOptions]::User)
            {
                if (Get-DMUser)
                {
                    Write-PSFMessage -Level Host -String 'Invoke-AdmfDomain.Executing.Invoke' -StringValues 'Users', $parameters.Server
                    Invoke-DMUser @parameters
                }
                else { Write-PSFMessage -Level Host -String 'Invoke-AdmfDomain.Skipping.Test.NoConfiguration' -StringValues 'Users' }
            }
            if ($newOptions -band [UpdateDomainOptions]::GroupMembership)
            {
                if (Get-DMGroupMembership)
                {
                    Write-PSFMessage -Level Host -String 'Invoke-AdmfDomain.Executing.Invoke' -StringValues 'GroupMembership', $parameters.Server
                    Invoke-DMGroupMembership @parameters
                }
                else { Write-PSFMessage -Level Host -String 'Invoke-AdmfDomain.Skipping.Test.NoConfiguration' -StringValues 'GroupMembership' }
            }
            if ($newOptions -band [UpdateDomainOptions]::PSO)
            {
                if (Get-DMPasswordPolicy)
                {
                    Write-PSFMessage -Level Host -String 'Invoke-AdmfDomain.Executing.Invoke' -StringValues 'PasswordPolicies', $parameters.Server
                    Invoke-DMPasswordPolicy @parameters
                }
                else { Write-PSFMessage -Level Host -String 'Invoke-AdmfDomain.Skipping.Test.NoConfiguration' -StringValues 'PasswordPolicies' }
            }
            if ($newOptions -band [UpdateDomainOptions]::GroupPolicy)
            {
                if (Get-DMGroupPolicy)
                {
                    Write-PSFMessage -Level Host -String 'Invoke-AdmfDomain.Executing.Invoke' -StringValues 'GroupPolicies - Create & Modify', $parameters.Server
                    Invoke-DMGroupPolicy @parameters
                }
                else { Write-PSFMessage -Level Host -String 'Invoke-AdmfDomain.Skipping.Test.NoConfiguration' -StringValues 'GroupPolicies - Create & Modify' }
            }
            if ($newOptions -band [UpdateDomainOptions]::GPPermission)
            {
                if (Get-DMGPPermission)
                {
                    Write-PSFMessage -Level Host -String 'Invoke-AdmfDomain.Executing.Invoke' -StringValues 'GroupPolicyPermissions', $parameters.Server
                    Invoke-DMGPPermission @parameters
                }
                else { Write-PSFMessage -Level Host -String 'Invoke-AdmfDomain.Skipping.Test.NoConfiguration' -StringValues 'GroupPolicyPermissions' }
            }
            if ($newOptions -band [UpdateDomainOptions]::GPLinkDisable)
            {
                if (Get-DMGPLink)
                {
                    Write-PSFMessage -Level Host -String 'Invoke-AdmfDomain.Executing.Invoke' -StringValues 'GroupPolicyLinks - Create, Update & Disable unwanted Links', $parameters.Server
                    Invoke-DMGPLink @parameters -Disable
                }
                else { Write-PSFMessage -Level Host -String 'Invoke-AdmfDomain.Skipping.Test.NoConfiguration' -StringValues 'GroupPolicyLinks - Create, Update & Disable unwanted Links' }
            }
            if ($newOptions -band [UpdateDomainOptions]::GroupPolicyDelete)
            {
                if (Get-DMGroupPolicy)
                {
                    Write-PSFMessage -Level Host -String 'Invoke-AdmfDomain.Executing.Invoke' -StringValues 'GroupPolicies - Delete', $parameters.Server
                    Invoke-DMGroupPolicy @parameters -Delete
                }
                else { Write-PSFMessage -Level Host -String 'Invoke-AdmfDomain.Skipping.Test.NoConfiguration' -StringValues 'GroupPolicies - Delete' }
            }
            if ($newOptions -band [UpdateDomainOptions]::GPLink)
            {
                if (Get-DMGPLink)
                {
                    Write-PSFMessage -Level Host -String 'Invoke-AdmfDomain.Executing.Invoke' -StringValues 'GroupPolicyLinks - Delete unwanted Links', $parameters.Server
                    Invoke-DMGPLink @parameters
                }
                else { Write-PSFMessage -Level Host -String 'Invoke-AdmfDomain.Skipping.Test.NoConfiguration' -StringValues 'GroupPolicyLinks - Delete unwanted Links' }
            }
            if ($newOptions -band [UpdateDomainOptions]::OUHard)
            {
                if (Get-DMOrganizationalUnit)
                {
                    Write-PSFMessage -Level Host -String 'Invoke-AdmfDomain.Executing.Invoke' -StringValues 'OrganizationalUnits - Delete', $parameters.Server
                    Invoke-DMOrganizationalUnit @parameters -Delete
                }
                else { Write-PSFMessage -Level Host -String 'Invoke-AdmfDomain.Skipping.Test.NoConfiguration' -StringValues 'OrganizationalUnits - Delete' }
            }
            if ($newOptions -band [UpdateDomainOptions]::Object)
            {
                if (Get-DMObject)
                {
                    Write-PSFMessage -Level Host -String 'Invoke-AdmfDomain.Executing.Invoke' -StringValues 'Objects', $parameters.Server
                    Invoke-DMObject @parameters
                }
                else { Write-PSFMessage -Level Host -String 'Invoke-AdmfDomain.Skipping.Test.NoConfiguration' -StringValues 'Objects' }
            }
            if ($newOptions -band [UpdateDomainOptions]::Acl)
            {
                if (Get-DMAcl)
                {
                    Write-PSFMessage -Level Host -String 'Invoke-AdmfDomain.Executing.Invoke' -StringValues 'Acls', $parameters.Server
                    Invoke-DMAcl @parameters
                }
                else { Write-PSFMessage -Level Host -String 'Invoke-AdmfDomain.Skipping.Test.NoConfiguration' -StringValues 'Acls' }
            }
            if ($newOptions -band [UpdateDomainOptions]::AccessRule)
            {
                if (Get-DMAccessRule)
                {
                    Write-PSFMessage -Level Host -String 'Invoke-AdmfDomain.Executing.Invoke' -StringValues 'AccessRules', $parameters.Server
                    Invoke-DMAccessRule @parameters
                }
                else { Write-PSFMessage -Level Host -String 'Invoke-AdmfDomain.Skipping.Test.NoConfiguration' -StringValues 'AccessRules' }
            }
        }
        catch { throw }
        finally { Invoke-PostCredentialProvider -ProviderName $CredentialProvider -Server $originalArgument.Server -Credential $originalArgument.Credential -Cmdlet $PSCmdlet }
    }
}

function Invoke-AdmfForest
{
    <#
    .SYNOPSIS
        Applies the currently desired configuration to the targeted forest.
     
    .DESCRIPTION
        Applies the currently desired configuration to the targeted forest.
        By default, this will only include sites, sitelinks and subnets.
 
        To switch to a full application, use the "-Options All" parameter.
     
    .PARAMETER Server
        The server / domain to work with.
     
    .PARAMETER Credential
        The credentials to use for this operation.
     
    .PARAMETER Options
        Which aspects to actually update.
        Defaults to "default" (Sites, SiteLinks & Subnets)
        Also available:
        - ServerRelocate (reassigns domain controllers to correct sites, if necessary)
        - Schema (applies core schema updates)
        - SchemaLdif (applies product Ldif files, such as SkypeForBusiness)
        To update everything, use "All".
 
    .PARAMETER CredentialProvider
        The credential provider to use to resolve the input credentials.
        See help on Register-AdmfCredentialProvider for details.
 
    .PARAMETER Confirm
        If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.
     
    .PARAMETER WhatIf
        If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run.
     
    .EXAMPLE
        PS C:\> Invoke-AdmfForest -Server contoso.com -Options All
 
        Applies the full forest configuration to the contoso.com domain.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPlainTextForPassword', '')]
    [CmdletBinding(SupportsShouldProcess = $true)]
    Param (
        [PSFComputer]
        $Server,

        [PSCredential]
        $Credential,

        [ADMF.UpdateForestOptions[]]
        $Options = 'Default',

        [string]
        $CredentialProvider = 'default'
    )
    
    begin
    {
        $parameters = $PSBoundParameters | ConvertTo-PSFHashtable -Include Server, Credential
        $originalArgument = Invoke-PreCredentialProvider @parameters -ProviderName $CredentialProvider -Parameter $parameters -Cmdlet $PSCmdlet
        try { $dcServer = Resolve-DomainController @parameters }
        catch {
            Invoke-PostCredentialProvider -ProviderName $CredentialProvider -Server $originalArgument.Server -Credential $originalArgument.Credential -Cmdlet $PSCmdlet
            throw
        }
        $parameters.Server = $dcServer
        Invoke-PSFCallback -Data $parameters -EnableException $true -PSCmdlet $PSCmdlet
        Set-AdmfContext @parameters -Interactive -ReUse -EnableException
        $parameters += $PSBoundParameters | ConvertTo-PSFHashtable -Include WhatIf, Confirm, Verbose, Debug
        $parameters.Server = $dcServer
        [ADMF.UpdateForestOptions]$newOptions = $Options
    }
    process
    {
        try
        {
            if ($newOptions -band [UpdateForestOptions]::Sites)
            {
                if (Get-FMSite)
                {
                    Write-PSFMessage -Level Host -String 'Invoke-AdmfForest.Executing.Invoke' -StringValues 'Sites', $parameters.Server
                    Invoke-FMSite @parameters
                }
                else { Write-PSFMessage -Level Host -String 'Invoke-AdmfForest.Skipping.Test.NoConfiguration' -StringValues 'Sites' }
            }
            if ($newOptions -band [UpdateForestOptions]::SiteLinks)
            {
                if (Get-FMSiteLink)
                {
                    Write-PSFMessage -Level Host -String 'Invoke-AdmfForest.Executing.Invoke' -StringValues 'Sitelinks', $parameters.Server
                    Invoke-FMSiteLink @parameters
                }
                else { Write-PSFMessage -Level Host -String 'Invoke-AdmfForest.Skipping.Test.NoConfiguration' -StringValues 'Sitelinks' }
            }
            if ($newOptions -band [UpdateForestOptions]::Subnets)
            {
                if (Get-FMSubnet)
                {
                    Write-PSFMessage -Level Host -String 'Invoke-AdmfForest.Executing.Invoke' -StringValues 'Subnets', $parameters.Server
                    Invoke-FMSubnet @parameters
                }
                else { Write-PSFMessage -Level Host -String 'Invoke-AdmfForest.Skipping.Test.NoConfiguration' -StringValues 'Subnets' }
            }
            if ($newOptions -band [UpdateForestOptions]::ServerRelocate)
            {
                Write-PSFMessage -Level Host -String 'Invoke-AdmfForest.Executing.Invoke' -StringValues 'Server Site Assignment', $parameters.Server
                Invoke-FMServer @parameters
            }
            if ($newOptions -band [UpdateForestOptions]::Schema)
            {
                if (Get-FMSchema)
                {
                    Write-PSFMessage -Level Host -String 'Invoke-AdmfForest.Executing.Invoke' -StringValues 'Schema (Custom)', $parameters.Server
                    Invoke-FMSchema @parameters
                }
                else { Write-PSFMessage -Level Host -String 'Invoke-AdmfForest.Skipping.Test.NoConfiguration' -StringValues 'Schema (Custom)' }
            }
            if ($newOptions -band [UpdateForestOptions]::SchemaLdif)
            {
                if (Get-FMSchemaLdif)
                {
                    Write-PSFMessage -Level Host -String 'Invoke-AdmfForest.Executing.Invoke' -StringValues 'Schema (Ldif)', $parameters.Server
                    Invoke-FMSchemaLdif @parameters
                }
                else { Write-PSFMessage -Level Host -String 'Invoke-AdmfForest.Skipping.Test.NoConfiguration' -StringValues 'Schema (Ldif)' }
            }
            if ($newOptions -band [UpdateForestOptions]::NTAuthStore)
            {
                if (Get-FMNTAuthStore)
                {
                    Write-PSFMessage -Level Host -String 'Invoke-AdmfForest.Executing.Invoke' -StringValues 'NTAuthStore', $parameters.Server
                    Invoke-FMNTAuthStore @parameters
                }
                else { Write-PSFMessage -Level Host -String 'Invoke-AdmfForest.Skipping.Test.NoConfiguration' -StringValues 'NTAuthStore' }
            }
        }
        catch { throw }
        finally { Invoke-PostCredentialProvider -ProviderName $CredentialProvider -Server $originalArgument.Server -Credential $originalArgument.Credential -Cmdlet $PSCmdlet }
    }
}

function New-AdmfContext
{
<#
    .SYNOPSIS
        Creates a new configuration context for ADMF.
     
    .DESCRIPTION
        Creates a new configuration context for ADMF.
        Contexts are a set of configuration settings.
        You can combine multiple contexts at the same time, merging the settings they contain.
         
        For more details on how contexts work, see:
         
            Get-Help about_ADMF_Context
     
    .PARAMETER Name
        The name of the context to create.
     
    .PARAMETER Store
        The context store to create the context in.
        Context Stores are registered filesystem locations where ADMF will look for contexts.
        Defaults to the default store found in %AppData%.
     
    .PARAMETER OutPath
        Create the context in a target path, rather than a registered store.
        Keep in mind, that this will require the context to be manually moved to a registered location in order for it to become available to use.
     
    .PARAMETER Weight
        The priority of the context.
        This is used to determine the import order when importing multiple contexts.
        The higher the value, the later in the import order.
        Default: 50
     
    .PARAMETER Description
        Add a description to your context (for documentation purposes only).
     
    .PARAMETER Author
        The author of the context (for documentation purposes only)
     
    .PARAMETER Group
        The group to assign the context to.
        By default, will be part of the "Default" group.
        Groups are only relevant fpr the itneractive context selection menu, where they govern the visual display style / grouping.
     
    .PARAMETER Prerequisite
        Contexts the current context depends on / requires.
     
    .PARAMETER MutuallyExclusive
        Contexts that are mutually exclusive with each other.
        E.g.: Where the user has to select between one of several environments.
     
    .PARAMETER DefaultAccessRules
        A new Active Directory environment comes with more deployed security delegations than defined in the schema.
        Several containers - especially the BuiltIn container - have a lot of extra access rules.
        When deploying a restrictive domain content mode, where these objects fall under management, it becomes necessary to also configure these delegations, lest they be removed.
        Setting this switch will include all the default delegations in your new context.
     
    .PARAMETER IncludeTemplate
        Whether to include example configuration files in the context.
        These must all be corrected or removed later on, but offer some initial guidance in how a configuration set for a given setting type might look like.
     
    .PARAMETER Force
        This command refuses to replace an existing context by default.
        Using force, it is a bit more brutish and will kill any previously existing context with the same name in the target store.
     
    .PARAMETER EnableException
        This parameters disables user-friendly warnings and enables the throwing of exceptions.
        This is less user friendly, but allows catching exceptions in calling scripts.
     
    .EXAMPLE
        PS C:\> New-AdmfContext -Name 'newContext'
         
        Creates a new context named "newContext"
     
    .EXAMPLE
        PS C:\> New-AdmfContext -Name 'Contoso_Baseline' -Store Company -Weight 10 -Author "Sad Joey" -DefaultccessRules -Description "Default baseline for contoso company forests"
         
        Creates a new context ...
        - Named "Contoso_Baseline"
        - In the context store "Company"
        - With the weight 10 (very low, causing it to be one of the first to be applied)
        - By Sad Joey (a great and non-sad person)
        - that contains the default access rules
        - has a useful description of what it is for
#>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [CmdletBinding(DefaultParameterSetName = 'Store')]
    param (
        [Parameter(Mandatory = $true)]
        [PsfValidatePattern('^[\w\d_\-\.]+$', ErrorString = 'ADMF.Validate.Pattern.ContextName')]
        [string]
        $Name,
        
        [Parameter(ParameterSetName = 'Store')]
        [PsfValidateSet(TabCompletion = 'ADMF.Context.Store')]
        [string]
        $Store = 'Default',
        
        [Parameter(ParameterSetName = 'Path')]
        [PsfValidateScript('ADMF.Validate.Path.Folder', ErrorString = 'ADMF.Validate.Path.Folder')]
        [string]
        $OutPath,
        
        [int]
        $Weight = 50,
        
        [string]
        $Description = "<Insert description-text here>",
        
        [string]
        $Author = "<Insert your name here>",
        
        [string]
        $Group = 'Default',
        
        [string[]]
        $Prerequisite = @(),
        
        [string[]]
        $MutuallyExclusive = @(),
        
        [switch]
        $DefaultAccessRules,
        
        [switch]
        $IncludeTemplate,
        
        [switch]
        $Force,
        
        [switch]
        $EnableException
    )
    
    begin
    {
        if ($OutPath)
        {
            $resolvedPath = Resolve-PSFPath -Provider FileSystem -Path $OutPath -SingleItem
            if (-not $Force -and (Test-Path -Path "$resolvedPath\$Name"))
            {
                Stop-PSFFunction -String 'New-AdmfContext.Context.AlreadyExists' -StringValues $resolvedPath, $Name -EnableException $EnableException -Category InvalidArgument -Cmdlet $PSCmdlet
                return
            }
        }
        else
        {
            $storeObject = Get-AdmfContextStore -Name $Store
            if (-not $Force -and (Test-Path -Path "$($storeObject.Path)\$Name"))
            {
                Stop-PSFFunction -String 'New-AdmfContext.Context.AlreadyExists2' -StringValues $Store, $Name -EnableException $EnableException -Category InvalidArgument -Cmdlet $PSCmdlet
                return
            }
            if (-not (Test-Path -Path $storeObject.Path))
            {
                $null = New-Item -Path $storeObject.Path -ItemType Directory -Force
            }
            $resolvedPath = Resolve-PSFPath -Provider FileSystem -Path $storeObject.Path -SingleItem
        }
    }
    process
    {
        if (Test-PSFFunctionInterrupt) { return }
        
        # This can only be $true when -Force was used, as otherwise it would fail in begin
        if (Test-Path -Path "$resolvedPath\$Name") { Remove-Item -Path "$resolvedPath\$Name" -Recurse -Force }
        
        $contextFolder = New-Item -Path $resolvedPath -Name $Name -ItemType Directory
        $contextVersionFolder = New-Item -Path $contextFolder.FullName -Name '1.0.0' -ItemType Directory
        if ($IncludeTemplate) { Copy-Item -Path "$script:ModuleRoot\internal\data\contextTemplate\*" -Destination "$($contextVersionFolder.FullName)\" -Recurse }
        else { Copy-Item -Path "$script:ModuleRoot\internal\data\context\*" -Destination "$($contextVersionFolder.FullName)\" -Recurse }
        
        #region Default Access Rules
        if ($DefaultAccessRules){
            Copy-Item -Path "$script:ModuleRoot\internal\data\domainDefaults\accessRules\*.json" -Destination "$($contextVersionFolder.FullName)\domain\accessrules\"
            Copy-Item -Path "$script:ModuleRoot\internal\data\domainDefaults\objectCategories\*.psd1" -Destination "$($contextVersionFolder.FullName)\domain\objectcategories\"
            Copy-Item -Path "$script:ModuleRoot\internal\data\domainDefaults\gppermissions\*.json" -Destination "$($contextVersionFolder.FullName)\domain\gppermissions\"
            Copy-Item -Path "$script:ModuleRoot\internal\data\domainDefaults\gppermissionfilters\*.json" -Destination "$($contextVersionFolder.FullName)\domain\gppermissionfilters\"
        }
        #endregion Default Access Rules
        
        $contextJson = [pscustomobject]@{
            Version          = '1.0.0'
            Weight          = $Weight
            Description   = $Description
            Author          = $Author
            Prerequisites = $Prerequisite
            MutuallyExclusive = $MutuallyExclusive
            Group          = $Group
        }
        $contextJson | ConvertTo-Json | Set-Content -Path "$($contextVersionFolder.FullName)\context.json"
        
        Get-AdmfContext -Name $Name -Store $Store
    }
}

function New-AdmfContextStore
{
<#
    .SYNOPSIS
        Creates a new Context Store.
     
    .DESCRIPTION
        Creates a new Context Store.
        Context Stores are locations where configuration contexts are stored and retrieved from.
     
        These contexts are stored using the PSFramework configuration system:
        https://psframework.org/documentation/documents/psframework/configuration.html
        Making it possible to deploy them using GPO, SCCM or other computer or profile management solutions.
     
    .PARAMETER Name
        The name of the store to create.
        Must not exist yet.
     
    .PARAMETER Path
        The path where the context is pointing at.
        Must be an existing folder.
     
    .PARAMETER Scope
        Where to persist the store.
        by default, this is stored in HKCU, making the store persistently available to the user.
        For more information on scopes, and what location they corespond with, see:
        https://psframework.org/documentation/documents/psframework/configuration/persistence-location.html
     
    .PARAMETER EnableException
        This parameters disables user-friendly warnings and enables the throwing of exceptions.
        This is less user friendly, but allows catching exceptions in calling scripts.
     
    .EXAMPLE
        PS C:\> New-AdmfContextStore -Name 'company' -Path '\\contoso\system\ad\contexts'
     
        Creates a new context named 'company', pointing at '\\contoso\system\ad\contexts'
#>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [PsfValidateScript('ADMF.Validate.ContextStore.ExistsNot', ErrorString = 'ADMF.Validate.ContextStore.ExistsNot')]
        [PsfValidatePattern('^[\w\d_\-\.]+$', ErrorString = 'ADMF.Validate.Pattern.ContextStoreName')]
        [string]
        $Name,
        
        [Parameter(Mandatory = $true)]
        [PsfValidateScript('ADMF.Validate.Path.Folder', ErrorString = 'ADMF.Validate.Path.Folder')]
        [string]
        $Path,
        
        [PSFramework.Configuration.ConfigScope]
        $Scope = "UserDefault",
        
        [switch]
        $EnableException
    )
    
    process
    {
        $resolvedPath = Resolve-PSFPath -Path $Path -Provider FileSystem -SingleItem
        Set-PSFConfig -FullName "ADMF.Context.Store.$Name" -Value $resolvedPath
        Register-PSFConfig -FullName "ADMF.Context.Store.$Name" -Scope $Scope -EnableException:$EnableException
    }
}

function Register-AdmfCredentialProvider {
    <#
    .SYNOPSIS
        Registers a credential provider used by the ADMF.
     
    .DESCRIPTION
        Registers a credential provider used by the ADMF.
 
        Credential providers are used for translating the credentials to use for all actions performed against active directory.
        For example, the ADMF could be extended to support a password safe solution:
        When connecting to a target domain, this provider scriptblock would retrieve the required credentials from a password safe solution.
 
        A credential provider consists of two scriptblocks:
        - A PreScript that is executed before running any commands. It must return either a PSCredential object (if applicable) or $null (if default windows credentials should be used instead).
        - A PostScript that is executed after all component commands have been executed. It need not return anything.
 
        Both scriptblocks receive a single input object, with two properties:
        - Server: The computer / domain targeted
        - Credential: The credentials originally provided (if any - this may be $null instead!)
     
    .PARAMETER Name
        The name of the credential provider.
        Each name must be unique, registering a provider using an existing name overwrites the previous provider.
        The provider "default" exists as part of ADMF and will be used if no other is specified. Overriding it allows you to change the default provider intentionally,
        but may remove your ability to NOT use any credential transformations, so use with care.
     
    .PARAMETER PreScript
        The script to execute before performing actions, in order to resolve the correct credentials to use.
        - If it returns a credential object, this object will be used for authenticating all AD operations (including WinRM against domain controllers!).
        - If it returns nothing / only non-credential objects, instead the default windows identity of the user is used.
     
    .PARAMETER PostScript
        This script is executed after performing all actions.
        You can use this optional script to perform any cleanup actions if necessary.
     
    .EXAMPLE
        PS C:\> Register-AdmfCredentialProvider -Name AZKeyVault -PreScript $keyVaultScript
 
        Registers the scriptblock defined in $keyVaultScript as "AZKeyVault" provider.
    #>

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

        [Parameter(Mandatory = $true)]
        [Scriptblock]
        $PreScript,

        [Scriptblock]
        $PostScript
    )

    $script:credentialProviders[$Name] = [PSCustomObject]@{
        PSTypeName = 'Admf.CredentialProvider'
        Name = $Name
        PreScript = $PreScript
        PostScript = $PostScript
    }
}

function Set-AdmfContext {
    <#
    .SYNOPSIS
        Applies a set of configuration contexts.
     
    .DESCRIPTION
        Applies a set of configuration contexts.
        This merges the settings from all selected contexts into one configuration set.
     
    .PARAMETER Context
        Name of context or full context object to apply.
     
    .PARAMETER Interactive
        Show an interactive context selection prompt.
        This is designed for greater convenience when managing many forests.
        The system automatically uses Set-AdmfContext with this parameter when directly testing or invoking against a new domain without first selecting a context to apply.
     
    .PARAMETER ReUse
        ADMF remembers the last contexts assigned to a specific server/domain.
        By setting this parameter, it will re-use those contexts, rather than show the prompt again.
        This parameter is used by the system to prevent prompting automatically on each call.
     
    .PARAMETER Server
        The server / domain to work with.
     
    .PARAMETER Credential
        The credentials to use for this operation.
     
    .PARAMETER EnableException
        This parameters disables user-friendly warnings and enables the throwing of exceptions.
        This is less user friendly, but allows catching exceptions in calling scripts.
     
    .EXAMPLE
        PS C:\> Set-AdmfContext -Interactive
         
        Interactively pick to select the contexts to apply to the user's own domain.
     
    .EXAMPLE
        PS C:\> Set-AdmfContext -Interactive -Server contoso.com
         
        Interactively pick to select the contexts to apply to the contoso.com domain.
     
    .EXAMPLE
        PS C:\> Set-AdmfContext -Context Default, Production, Europe -Server eu.contoso.com
         
        Configures the contexts Default, Production and Europe to be applied to eu.contoso.com.
#>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [CmdletBinding(DefaultParameterSetName = 'name')]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'name')]
        [Alias('Name')]
        [object[]]
        $Context,
        
        [Parameter(ParameterSetName = 'interactive')]
        [switch]
        $Interactive,
        
        [switch]
        $ReUse,
        
        [PSFComputer]
        $Server = $env:USERDNSDOMAIN,
        
        [System.Management.Automation.PSCredential]
        $Credential,
        
        [switch]
        $EnableException
    )
    
    begin {
        #region Utility Functions
        function Set-Context {
            [CmdletBinding()]
            param (
                $ContextObject,
                
                [string]
                $Server,
                
                [System.Management.Automation.PSCredential]
                $Credential,
                
                [System.Management.Automation.PSCmdlet]
                $Cmdlet,
                
                [bool]
                $EnableException
            )
            
            Write-PSFMessage -String 'Set-AdmfContext.Context.Applying' -StringValues $ContextObject.Name -Target $ContextObject
            $parameters = $PSBoundParameters | ConvertTo-PSFHashtable -Include Server, Credential
            $stopParam = @{
                EnableException = $EnableException
                Cmdlet          = $Cmdlet
                Target          = $ContextObject
                StepsUpward     = 1
            }
            
            #region PreImport
            if (Test-Path "$($ContextObject.Path)\preImport.ps1") {
                try { $null = & "$($ContextObject.Path)\preImport.ps1" @parameters }
                catch {
                    Clear-DMConfiguration
                    Clear-FMConfiguration
                    Stop-PSFFunction @stopParam -String 'Set-AdmfContext.Context.Error.PreImport' -StringValues $ContextObject.Name -ErrorRecord $_
                    return
                }
            }
            #endregion PreImport
            
            #region Forest
            $forestFields = @{
                'schema'    = (Get-Command Register-FMSchema)
                'sitelinks' = (Get-Command Register-FMSiteLink)
                'sites'     = (Get-Command Register-FMSite)
                'subnets'   = (Get-Command Register-FMSubnet)
            }
            
            foreach ($key in $forestFields.Keys) {
                if (-not (Test-Path "$($ContextObject.Path)\forest\$key")) { continue }
                
                foreach ($file in (Get-ChildItem "$($ContextObject.Path)\forest\$key\" -Recurse -Filter "*.json")) {
                    Write-PSFMessage -Level Debug -String 'Set-AdmfContext.Context.Loading' -StringValues $ContextObject.Name, $key, $file.FullName
                    try {
                        foreach ($dataSet in (Get-Content $file.FullName | ConvertFrom-Json -ErrorAction Stop | Write-Output | ConvertTo-PSFHashtable -Include $($forestFields[$key].Parameters.Keys))) {
                            if ($forestFields[$key].Parameters.Keys -contains 'ContextName') {
                                $dataSet['ContextName'] = $ContextObject.Name
                            }
                            & $forestFields[$key] @dataSet -ErrorAction Stop
                        }
                    }
                    catch {
                        Clear-DMConfiguration
                        Clear-FMConfiguration
                        Stop-PSFFunction @stopParam -String 'Set-AdmfContext.Context.Error.ForestConfig' -StringValues $ContextObject.Name, $key, $file.FullName -ErrorRecord $_
                        return
                    }
                }
            }
            
            if (Test-Path "$($ContextObject.Path)\forest\schemaldif") {
                $filesProcessed = @()

                #region Process Ldif Configuration
                foreach ($file in (Get-ChildItem "$($ContextObject.Path)\forest\schemaldif\" -Recurse -Filter "*.json")) {
                    $jsonData = Get-Content $file.FullName | ConvertFrom-Json
                    foreach ($jsonEntry in $jsonData) {
                        $targetPath = Join-Path "$($ContextObject.Path)\forest\schemaldif" $jsonEntry.Path
                        if ($filesProcessed -contains $targetPath) { continue }

                        try { $ldifItem = Get-Item -Path $targetPath -ErrorAction Stop -Force }
                        catch {
                            Clear-DMConfiguration
                            Clear-FMConfiguration
                            Stop-PSFFunction @stopParam -String 'Set-AdmfContext.Context.Error.ForestConfig' -StringValues $ContextObject.Name, 'Schema (ldif)', $file.FullName -ErrorRecord $_
                            return
                        }

                        $ldifParam = @{
                            Path        = $ldifItem.FullName
                            Name        = $ldifItem.BaseName
                            ContextName = $ContextObject.Name
                        }
                        if ($jsonEntry.Name) { $ldifParam.Name = $jsonEntry.Name }
                        if ($jsonEntry.Weight) { $ldifParam['Weight'] = $jsonEntry.Weight }
                        if ($jsonEntry.MissingObjectExemption) { $ldifParam['MissingObjectExemption'] = $jsonEntry.MissingObjectExemption }
                        try {
                            Register-FMSchemaLdif @ldifParam -ErrorAction Stop
                            $filesProcessed += $ldifItem.FullName
                        }
                        catch {
                            Clear-DMConfiguration
                            Clear-FMConfiguration
                            Stop-PSFFunction @stopParam -String 'Set-AdmfContext.Context.Error.ForestConfig' -StringValues $ContextObject.Name, 'schemaldif', $file.FullName -ErrorRecord $_
                            return
                        }
                    }
                }
                #endregion Process Ldif Configuration

                #region Process Ldif Files without configuration
                foreach ($file in (Get-ChildItem "$($ContextObject.Path)\forest\schemaldif\" -Recurse -Filter "*.ldf")) {
                    # Skip files already defined in json
                    if ($filesProcessed -contains $file.FullName) { continue }
                    
                    try { Register-FMSchemaLdif -Name $file.BaseName -Path $file.FullName -ContextName $ContextObject.Name -ErrorAction Stop }
                    catch {
                        Clear-DMConfiguration
                        Clear-FMConfiguration
                        Stop-PSFFunction @stopParam -String 'Set-AdmfContext.Context.Error.ForestConfig' -StringValues $ContextObject.Name, 'schemaldif', $file.FullName -ErrorRecord $_
                        return
                    }
                }
                #endregion Process Ldif Files without configuration
            }
            
            #region NTAuthStore
            if (Test-Path "$($ContextObject.Path)\forest\ntAuthStore")
            {
                foreach ($file in (Get-ChildItem "$($ContextObject.Path)\forest\ntAuthStore" -Recurse -File))
                {
                    switch ($file.Extension)
                    {
                        '.json'
                        {
                            Write-PSFMessage -Level Debug -String 'Set-AdmfContext.Context.Loading' -StringValues $ContextObject.Name, 'NTAuthStore', $file.FullName
                            try
                            {
                                $jsonData = Get-Content -Path $file.FullName | ConvertFrom-Json -ErrorAction Stop
                                if ($jsonData.PSObject.Properties.Name -eq 'Authorative')
                                {
                                    Register-FMNTAuthStore -Authorative:$jsonData.Authorative
                                }
                            }
                            catch
                            {
                                Clear-DMConfiguration
                                Clear-FMConfiguration
                                Stop-PSFFunction @stopParam -String 'Set-AdmfContext.Context.Error.ForestConfig' -StringValues $ContextObject.Name, 'NTAuthStore', $file.FullName -ErrorRecord $_
                                return
                            }
                        }
                        '.cer'
                        {
                            Write-PSFMessage -Level Debug -String 'Set-AdmfContext.Context.Loading' -StringValues $ContextObject.Name, 'NTAuthStore', $file.FullName
                            try
                            {
                                $cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::CreateFromCertFile($file.FullName)
                                Register-FMNTAuthStore -Certificate $cert
                            }
                            catch
                            {
                                Clear-DMConfiguration
                                Clear-FMConfiguration
                                Stop-PSFFunction @stopParam -String 'Set-AdmfContext.Context.Error.ForestConfig' -StringValues $ContextObject.Name, 'NTAuthStore', $file.FullName -ErrorRecord $_
                                return
                            }
                        }
                    }
                }
            }
            #endregion NTAuthStore
            #endregion Forest
            
            #region Domain
            $domainFields = @{
                'accessrules'         = (Get-Command Register-DMAccessRule)
                'accessrulemodes'     = (Get-Command Register-DMAccessRuleMode)
                'acls'                = (Get-Command Register-DMAcl)
                'builtinsids'         = (Get-Command Register-DMBuiltInSID)
                'gplinks'             = (Get-Command Register-DMGPLink)
                'gppermissions'       = (Get-Command Register-DMGPPermission)
                'gppermissionfilters' = (Get-Command Register-DMGPPermissionFilter)
                'gpregistrysettings'  = (Get-Command Register-DMGPRegistrySetting)
                'groups'              = (Get-Command Register-DMGroup)
                'groupmemberships'    = (Get-Command Register-DMGroupMembership)
                'names'               = (Get-Command Register-DMNameMapping)
                'objects'             = (Get-Command Register-DMObject)
                'organizationalunits' = (Get-Command Register-DMOrganizationalUnit)
                'psos'                = (Get-Command Register-DMPasswordPolicy)
                'users'               = (Get-Command Register-DMUser)
            }
            
            foreach ($key in $domainFields.Keys) {
                if (-not (Test-Path "$($ContextObject.Path)\domain\$key")) { continue }
                
                foreach ($file in (Get-ChildItem "$($ContextObject.Path)\domain\$key\" -Recurse -Filter "*.json")) {
                    Write-PSFMessage -Level Debug -String 'Set-AdmfContext.Context.Loading' -StringValues $ContextObject.Name, $key, $file.FullName
                    try {
                        foreach ($dataSet in (Get-Content $file.FullName | ConvertFrom-Json -ErrorAction Stop | Write-Output | ConvertTo-PSFHashtable -Include $($domainFields[$key].Parameters.Keys))) {
                            if ($domainFields[$key].Parameters.Keys -contains 'ContextName') {
                                $dataSet['ContextName'] = $ContextObject.Name
                            }
                            & $domainFields[$key] @dataSet -ErrorAction Stop
                        }
                    }
                    catch {
                        Clear-DMConfiguration
                        Clear-FMConfiguration
                        Stop-PSFFunction @stopParam -String 'Set-AdmfContext.Context.Error.DomainConfig' -StringValues $ContextObject.Name, $key, $file.FullName -ErrorRecord $_
                        return
                    }
                }
            }

            # Group Policy
            if (Test-Path "$($ContextObject.Path)\domain\grouppolicies\exportData.json") {
                $file = Get-Item "$($ContextObject.Path)\domain\grouppolicies\exportData.json"
                Write-PSFMessage -Level Debug -String 'Set-AdmfContext.Context.Loading' -StringValues $ContextObject.Name, 'Group Policy', $file.FullName
                try {
                    $dataSet = Get-Content $file.FullName | ConvertFrom-Json -ErrorAction Stop | ConvertTo-PSFHashtable -Include DisplayName, Description, ID, ExportID
                    foreach ($policyEntry in $dataSet) {
                        Register-DMGroupPolicy @policyEntry -Path "$($ContextObject.Path)\domain\grouppolicies\$($policyEntry.ID)"
                    }
                }
                catch {
                    Clear-DMConfiguration
                    Clear-FMConfiguration
                    Stop-PSFFunction @stopParam -String 'Set-AdmfContext.Context.Error.DomainConfig' -StringValues $ContextObject.Name, 'Group Policy', $file.FullName -ErrorRecord $_
                    return
                }
            }

            # Object Categories
            foreach ($file in (Get-ChildItem "$($ContextObject.Path)\domain\objectcategories" -Filter '*.psd1' -ErrorAction Ignore)) {
                try {
                    $dataSet = Import-PSFPowerShellDataFile -Path $file.FullName
                    $dataSet.TestScript = $dataSet.TestScript.Invoke() | Write-Output # Remove automatic scriptblock nesting
                    Register-DMObjectCategory @dataSet
                }
                catch {
                    Clear-DMConfiguration
                    Clear-FMConfiguration
                    Stop-PSFFunction @stopParam -String 'Set-AdmfContext.Context.Error.DomainConfig' -StringValues $ContextObject.Name, 'Object Categories', $file.FullName -ErrorRecord $_
                    return
                }
            }

            # Domain Data
            foreach ($file in (Get-ChildItem "$($ContextObject.Path)\domain\domaindata" -Filter '*.psd1' -ErrorAction Ignore)) {
                try {
                    $dataSet = Import-PSFPowerShellDataFile -Path $file.FullName
                    $dataSet.Scriptblock = $dataSet.Scriptblock.Invoke() | Write-Output # Remove automatic scriptblock nesting
                    Register-DMDomainData @dataSet
                }
                catch {
                    Clear-DMConfiguration
                    Clear-FMConfiguration
                    Stop-PSFFunction @stopParam -String 'Set-AdmfContext.Context.Error.DomainConfig' -StringValues $ContextObject.Name, 'Domain Data', $file.FullName -ErrorRecord $_
                    return
                }
            }
            
            # Content Mode
            if (Test-Path "$($ContextObject.Path)\domain\content_mode.json") {
                $file = Get-Item "$($ContextObject.Path)\domain\content_mode.json"
                Write-PSFMessage -Level Debug -String 'Set-AdmfContext.Context.Loading' -StringValues $ContextObject.Name, 'ContentMode', $file.FullName
                try {
                    $dataSet = Get-Content $file.FullName | ConvertFrom-Json -ErrorAction Stop
                    if ($dataSet.Mode) { Set-DMContentMode -Mode $dataSet.Mode }
                    if ($dataSet.Include) {
                        $includes = @((Get-DMContentMode).Include)
                        foreach ($entry in $dataSet.Include) { $includes += $entry }
                        Set-DMContentMode -Include $includes
                    }
                    if ($dataSet.Exclude) {
                        $excludes = @((Get-DMContentMode).Exclude)
                        foreach ($entry in $dataSet.Exclude) { $excludes += $entry }
                        Set-DMContentMode -Exclude $excludes
                    }
                    if ($dataSet.UserExcludePattern) {
                        $userExcludePatterns = @((Get-DMContentMode).UserExcludePattern)
                        foreach ($entry in $dataSet.UserExcludePattern) { $userExcludePatterns += $entry }
                        Set-DMContentMode -UserExcludePattern $userExcludePatterns
                    }
                }
                catch {
                    Clear-DMConfiguration
                    Clear-FMConfiguration
                    Stop-PSFFunction @stopParam -String 'Set-AdmfContext.Context.Error.DomainConfig' -StringValues $ContextObject.Name, 'ContentMode', $file.FullName -ErrorRecord $_
                    return
                }
            }
            #endregion Domain
            
            #region DC
            if (Test-Path "$($ContextObject.Path)\dc\dc_config.json") {
                try { $dcData = Get-Content "$($ContextObject.Path)\dc\dc_config.json" -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop }
                catch {
                    Clear-DMConfiguration
                    Clear-FMConfiguration
                    Stop-PSFFunction @stopParam -String 'Set-AdmfContext.Context.Error.DCConfig' -StringValues $ContextObject.Name -ErrorRecord $_
                    return
                }
                
                if ($null -ne $dcData.NoDNS) { Set-PSFConfig -FullName 'DCManagement.Defaults.NoDNS' -Value $dcData.NoDNS }
                if ($null -ne $dcData.NoReboot) { Set-PSFConfig -FullName 'DCManagement.Defaults.NoReboot' -Value $dcData.NoReboot }
                if ($dcData.DatabasePath) { Set-PSFConfig -FullName 'DCManagement.Defaults.DatabasePath' -Value $dcData.DatabasePath }
                if ($dcData.LogPath) { Set-PSFConfig -FullName 'DCManagement.Defaults.LogPath' -Value $dcData.LogPath }
                if ($dcData.SysvolPath) { Set-PSFConfig -FullName 'DCManagement.Defaults.SysvolPath' -Value $dcData.SysvolPath }
            }
            #endregion DC
            
            #region PostImport
            if (Test-Path "$($ContextObject.Path)\postImport.ps1") {
                try { $null = & "$($ContextObject.Path)\postImport.ps1" @parameters }
                catch {
                    Clear-DMConfiguration
                    Clear-FMConfiguration
                    Stop-PSFFunction @stopParam -String 'Set-AdmfContext.Context.Error.PostImport' -StringValues $ContextObject.Name -ErrorRecord $_
                    return
                }
            }
            #endregion PostImport
        }
        #endregion Utility Functions
        
        $parameters = $PSBoundParameters | ConvertTo-PSFHashtable -Include Server, Credential
        
        $selectedContexts = @{ }
        # Common parameters for Stop-PSFFunction
        $commonParam = @{
            EnableException = $EnableException
            Continue        = $true
        }
    }
    process {
        #region Explicitly specified contexts
        foreach ($contextObject in $Context) {
            if ($contextObject -is [string]) {
                $foundContext = Get-AdmfContext -Name $contextObject
                if (-not $foundContext) { Stop-PSFFunction @commonParam -String 'Set-AdmfContext.Context.NotFound' -StringValues $contextObject }
                if ($foundContext.Count -gt 1) { Stop-PSFFunction @commonParam -String 'Set-AdmfContext.Context.Ambiguous' -StringValues $contextObject, ($foundContext.Name -join ", ") }
                $selectedContexts[$foundContext.Name] = $foundContext
                continue
            }
            if ($contextObject.PSObject.Typenames -eq 'ADMF.Context') {
                $selectedContexts[$contextObject.Name] = $contextObject
                continue
            }
            Stop-PSFFunction @commonParam -String 'Set-AdmfContext.Context.InvalidInput' -StringValues $contextObject, $contextObject.GetType().FullName
        }
        #endregion Explicitly specified contexts
        #region Interactively chosen contexts
        if ($Interactive) {
            if ($ReUse -and $script:assignedContexts["$Server"]) {
                foreach ($contextObject in $script:assignedContexts["$Server"]) {
                    $selectedContexts[$contextObject.Name] = $contextObject
                }
                return
            }
            try {
                foreach ($contextObject in (Invoke-CallbackMenu @parameters)) {
                    $selectedContexts[$contextObject.Name] = $contextObject
                }
            }
            catch {
                Stop-PSFFunction -String 'Set-AdmfContext.Interactive.Cancel' -EnableException $EnableException -ErrorRecord $_
                return
            }
        }
        #endregion Interactively chosen contexts
    }
    end {
        if (Test-PSFFunctionInterrupt) { return }
        
        #region Handle errors in selection
        $missingPrerequisites = $selectedContexts.Values.Prerequisites | Where-Object { $_ -notin $selectedContexts.Values.Name }
        if ($missingPrerequisites) {
            Stop-PSFFunction -String 'Set-AdmfContext.Resolution.MissingPrerequisites' -StringValues ($missingPrerequisites -join ", ") -EnableException $EnableException -Category InvalidData
            return
        }
        $conflictingContexts = $selectedContexts.Values.MutuallyExclusive | Where-Object { $_ -in $selectedContexts.Values.Name }
        if ($conflictingContexts) {
            Stop-PSFFunction -String 'Set-AdmfContext.Resolution.ExclusionConflict' -StringValues ($conflictingContexts.Name -join ", ") -EnableException $EnableException -Category InvalidData
            return
        }
        #endregion Handle errors in selection
        
        # Do nothing if the currently loaded contexts are equal to the selected ones
        if (
            $script:loadedContexts.Name -and
            $selectedContexts.Values.Name -and
            -not (Compare-Object -ReferenceObject $selectedContexts.Values.Name -DifferenceObject $script:loadedContexts.Name)
        ) {
            # When switching from one domain to a new one, make sure that the selection is cached, even if it is the same selection.
            # Otherwise, the second domain will keep reprompting for contexts
            if (-not $script:assignedContexts["$Server"]) { $script:assignedContexts["$Server"] = $selectedContexts.Values }
            return
        }
        
        # Kill previous configuration
        $script:loadedContexts = @()
        Clear-DMConfiguration
        Clear-FMConfiguration
        
        Set-PSFTaskEngineCache -Module ADMF -Name currentlyImportingContexts -Value $selectedContexts.Values
        
        foreach ($contextObject in ($selectedContexts.Values | Sort-Object Weight)) {
            if (Test-PSFFunctionInterrupt) { return }
            Set-Context @parameters -ContextObject $contextObject -Cmdlet $PSCmdlet -EnableException $EnableException
            if (Test-PSFFunctionInterrupt) { return }
        }
        $script:assignedContexts["$Server"] = $selectedContexts.Values | Sort-Object Weight
        $script:loadedContexts = @($selectedContexts.Values | Sort-Object Weight)
        Set-PSFTaskEngineCache -Module ADMF -Name currentlyImportingContexts -Value @()
    }
}

function Test-AdmfDomain
{
    <#
    .SYNOPSIS
        Tests a domain for its domain level content and whether it matches the desired state.
     
    .DESCRIPTION
        Tests a domain for its domain level content and whether it matches the desired state.
        Executes a large battery of tests from the DomainManagement module.
        The desired state is defined using configuration files, which the module handles for the user.
     
    .PARAMETER Server
        The server / domain to work with.
     
    .PARAMETER Credential
        The credentials to use for this operation.
     
    .PARAMETER Options
        Which scan to execute.
        By default, all tests are run, but it is possibly to selectively choose which to run.
 
    .PARAMETER CredentialProvider
        The credential provider to use to resolve the input credentials.
        See help on Register-AdmfCredentialProvider for details.
     
    .EXAMPLE
        PS C:\> Test-AdmfDomain -Server corp.fabrikam.com
 
        Scans the domain corp.fabrikam.com for compliance with the desired state.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPlainTextForPassword', '')]
    [CmdletBinding()]
    Param (
        [PSFComputer]
        $Server,

        [PSCredential]
        $Credential,

        [UpdateDomainOptions[]]
        $Options = 'All',

        [string]
        $CredentialProvider = 'default'
    )
    
    begin
    {
        $parameters = $PSBoundParameters | ConvertTo-PSFHashtable -Include Server, Credential
        $originalArgument = Invoke-PreCredentialProvider @parameters -ProviderName $CredentialProvider -Parameter $parameters -Cmdlet $PSCmdlet
        try { $parameters.Server = Resolve-DomainController @parameters -ErrorAction Stop }
        catch {
            Invoke-PostCredentialProvider -ProviderName $CredentialProvider -Server $originalArgument.Server -Credential $originalArgument.Credential -Cmdlet $PSCmdlet
            throw
        }
        Invoke-PSFCallback -Data $parameters -EnableException $true -PSCmdlet $PSCmdlet
        Set-AdmfContext @parameters -Interactive -ReUse -EnableException
        [UpdateDomainOptions]$newOptions = $Options
    }
    process
    {
        try
        {
            if (($newOptions -band [UpdateDomainOptions]::OUSoft) -or ($newOptions -band [UpdateDomainOptions]::OUHard)) {
                if (Get-DMOrganizationalUnit)
                {
                    Write-PSFMessage -Level Host -String 'Test-AdmfDomain.Executing.Test' -StringValues 'OrganizationalUnits', $parameters.Server
                    Test-DMOrganizationalUnit @parameters
                }
                else { Write-PSFMessage -Level Host -String 'Test-AdmfDomain.Skipping.Test.NoConfiguration' -StringValues 'OrganizationalUnits' }
            }
            if ($newOptions -band [UpdateDomainOptions]::Group) {
                if (Get-DMGroup)
                {
                    Write-PSFMessage -Level Host -String 'Test-AdmfDomain.Executing.Test' -StringValues 'Groups', $parameters.Server
                    Test-DMGroup @parameters
                }
                else { Write-PSFMessage -Level Host -String 'Test-AdmfDomain.Skipping.Test.NoConfiguration' -StringValues 'Groups' }
            }
            if ($newOptions -band [UpdateDomainOptions]::User) {
                if (Get-DMUser)
                {
                    Write-PSFMessage -Level Host -String 'Test-AdmfDomain.Executing.Test' -StringValues 'Users', $parameters.Server
                    Test-DMUser @parameters
                }
                else { Write-PSFMessage -Level Host -String 'Test-AdmfDomain.Skipping.Test.NoConfiguration' -StringValues 'Users' }
            }
            if ($newOptions -band [UpdateDomainOptions]::GroupMembership) {
                if (Get-DMGroupMembership)
                {
                    Write-PSFMessage -Level Host -String 'Test-AdmfDomain.Executing.Test' -StringValues 'GroupMembership', $parameters.Server
                    Test-DMGroupMembership @parameters
                }
                else { Write-PSFMessage -Level Host -String 'Test-AdmfDomain.Skipping.Test.NoConfiguration' -StringValues 'GroupMembership' }
            }
            if ($newOptions -band [UpdateDomainOptions]::Acl) {
                if (Get-DMAcl)
                {
                    Write-PSFMessage -Level Host -String 'Test-AdmfDomain.Executing.Test' -StringValues 'Acls', $parameters.Server
                    Test-DMAcl @parameters
                }
                else { Write-PSFMessage -Level Host -String 'Test-AdmfDomain.Skipping.Test.NoConfiguration' -StringValues 'Acls' }
            }
            if ($newOptions -band [UpdateDomainOptions]::AccessRule) {
                if (Get-DMAccessRule)
                {
                    Write-PSFMessage -Level Host -String 'Test-AdmfDomain.Executing.Test' -StringValues 'AccessRules', $parameters.Server
                    Test-DMAccessRule @parameters
                }
                else { Write-PSFMessage -Level Host -String 'Test-AdmfDomain.Skipping.Test.NoConfiguration' -StringValues 'AccessRules' }
            }
            if ($newOptions -band [UpdateDomainOptions]::PSO) {
                if (Get-DMPasswordPolicy)
                {
                    Write-PSFMessage -Level Host -String 'Test-AdmfDomain.Executing.Test' -StringValues 'PasswordPolicies', $parameters.Server
                    Test-DMPasswordPolicy @parameters
                }
                else { Write-PSFMessage -Level Host -String 'Test-AdmfDomain.Skipping.Test.NoConfiguration' -StringValues 'PasswordPolicies' }
            }
            if ($newOptions -band [UpdateDomainOptions]::Object) {
                if (Get-DMObject)
                {
                    Write-PSFMessage -Level Host -String 'Test-AdmfDomain.Executing.Test' -StringValues 'Object', $parameters.Server
                    Test-DMObject @parameters
                }
                else { Write-PSFMessage -Level Host -String 'Test-AdmfDomain.Skipping.Test.NoConfiguration' -StringValues 'Object' }
            }
            if (($newOptions -band [UpdateDomainOptions]::GroupPolicy) -or ($newOptions -band [UpdateDomainOptions]::GroupPolicyDelete)) {
                if (Get-DMGroupPolicy)
                {
                    Write-PSFMessage -Level Host -String 'Test-AdmfDomain.Executing.Test' -StringValues 'GroupPolicies', $parameters.Server
                    Test-DMGroupPolicy @parameters
                }
                else { Write-PSFMessage -Level Host -String 'Test-AdmfDomain.Skipping.Test.NoConfiguration' -StringValues 'GroupPolicies' }
            }
            if ($newOptions -band [UpdateDomainOptions]::GPPermission) {
                if (Get-DMGPPermission)
                {
                    Write-PSFMessage -Level Host -String 'Test-AdmfDomain.Executing.Test' -StringValues 'GroupPolicyPermissions', $parameters.Server
                    Test-DMGPPermission @parameters
                }
                else { Write-PSFMessage -Level Host -String 'Test-AdmfDomain.Skipping.Test.NoConfiguration' -StringValues 'GroupPolicyPermissions' }
            }
            if (($newOptions -band [UpdateDomainOptions]::GPLink) -or ($newOptions -band [UpdateDomainOptions]::GPLinkDisable)) {
                if (Get-DMGPLink)
                {
                    Write-PSFMessage -Level Host -String 'Test-AdmfDomain.Executing.Test' -StringValues 'GroupPolicyLinks', $parameters.Server
                    Test-DMGPLink @parameters
                }
                else { Write-PSFMessage -Level Host -String 'Test-AdmfDomain.Skipping.Test.NoConfiguration' -StringValues 'GroupPolicyLinks' }
            }
        }
        catch { throw }
        finally { Invoke-PostCredentialProvider -ProviderName $CredentialProvider -Server $originalArgument.Server -Credential $originalArgument.Credential -Cmdlet $PSCmdlet }
    }
}

function Test-AdmfForest
{
    <#
        .SYNOPSIS
            Tests whether a forest is configured according to baseline configuration
         
        .DESCRIPTION
            Tests whether a forest is configured according to baseline configuration
         
        .PARAMETER Server
            The server / domain to work with.
         
        .PARAMETER Credential
            The credentials to use for this operation.
         
        .PARAMETER Options
            What tests to execute.
            Defaults to all tests.
 
        .PARAMETER CredentialProvider
            The credential provider to use to resolve the input credentials.
            See help on Register-AdmfCredentialProvider for details.
         
        .EXAMPLE
            PS C:\> Test-AdmfForest
 
            Test the current forest for baseline compliance.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPlainTextForPassword', '')]
    [CmdletBinding()]
    Param (
        [PSFComputer]
        $Server,

        [PSCredential]
        $Credential,

        [UpdateForestOptions[]]
        $Options = 'All',

        [string]
        $CredentialProvider = 'default'
    )
    
    begin
    {
        $parameters = $PSBoundParameters | ConvertTo-PSFHashtable -Include Server, Credential
        $originalArgument = Invoke-PreCredentialProvider @parameters -ProviderName $CredentialProvider -Parameter $parameters -Cmdlet $PSCmdlet
        try { $parameters.Server = Resolve-DomainController @parameters -ErrorAction Stop }
        catch {
            Invoke-PostCredentialProvider -ProviderName $CredentialProvider -Server $originalArgument.Server -Credential $originalArgument.Credential -Cmdlet $PSCmdlet
            throw
        }
        Invoke-PSFCallback -Data $parameters -EnableException $true -PSCmdlet $PSCmdlet
        Set-AdmfContext @parameters -Interactive -ReUse -EnableException
        [UpdateForestOptions]$newOptions = $Options
    }
    process
    {
        try
        {
            if ($newOptions -band [UpdateForestOptions]::Sites) {
                if (Get-FMSite)
                {
                    Write-PSFMessage -Level Host -String 'Test-AdmfForest.Executing.Test' -StringValues 'Sites', $parameters.Server
                    Test-FMSite @parameters
                }
                else { Write-PSFMessage -Level Host -String 'Test-AdmfForest.Skipping.Test.NoConfiguration' -StringValues 'Sites' }
            }
            if ($newOptions -band [UpdateForestOptions]::SiteLinks) {
                if (Get-FMSiteLink)
                {
                    Write-PSFMessage -Level Host -String 'Test-AdmfForest.Executing.Test' -StringValues 'Sitelinks', $parameters.Server
                    Test-FMSiteLink @parameters
                }
                else { Write-PSFMessage -Level Host -String 'Test-AdmfForest.Skipping.Test.NoConfiguration' -StringValues 'Sitelinks' }
            }
            if ($newOptions -band [UpdateForestOptions]::Subnets) {
                if (Get-FMSubnet)
                {
                    Write-PSFMessage -Level Host -String 'Test-AdmfForest.Executing.Test' -StringValues 'Subnets', $parameters.Server
                    Test-FMSubnet @parameters
                }
                else { Write-PSFMessage -Level Host -String 'Test-AdmfForest.Skipping.Test.NoConfiguration' -StringValues 'Subnets' }
            }
            if ($newOptions -band [UpdateForestOptions]::ServerRelocate) {
                # Requires no configuration, so no check for configuration existence required
                Write-PSFMessage -Level Host -String 'Test-AdmfForest.Executing.Test' -StringValues 'Server Site Assignment', $parameters.Server
                Test-FMServer @parameters
            }
            if ($newOptions -band [UpdateForestOptions]::Schema) {
                if (Get-FMSchema)
                {
                    Write-PSFMessage -Level Host -String 'Test-AdmfForest.Executing.Test' -StringValues 'Schema (Custom)', $parameters.Server
                    Test-FMSchema @parameters
                }
                else { Write-PSFMessage -Level Host -String 'Test-AdmfForest.Skipping.Test.NoConfiguration' -StringValues 'Schema (Custom)' }
            }
            if ($newOptions -band [UpdateForestOptions]::SchemaLdif) {
                if (Get-FMSchemaLdif)
                {
                    Write-PSFMessage -Level Host -String 'Test-AdmfForest.Executing.Test' -StringValues 'Schema (Ldif)', $parameters.Server
                    Test-FMSchemaLdif @parameters
                }
                else { Write-PSFMessage -Level Host -String 'Test-AdmfForest.Skipping.Test.NoConfiguration' -StringValues 'Schema (Ldif)' }
            }
            if ($newOptions -band [UpdateForestOptions]::NTAuthStore)
            {
                if (Get-FMNTAuthStore)
                {
                    Write-PSFMessage -Level Host -String 'Test-AdmfForest.Executing.Test' -StringValues 'NTAuthStore', $parameters.Server
                    Test-FMNTAuthStore @parameters
                }
                else { Write-PSFMessage -Level Host -String 'Test-AdmfForest.Skipping.Test.NoConfiguration' -StringValues 'NTAuthStore' }
            }
        }
        catch { throw }
        finally { Invoke-PostCredentialProvider -ProviderName $CredentialProvider -Server $originalArgument.Server -Credential $originalArgument.Credential -Cmdlet $PSCmdlet }
    }
}

<#
This is an example configuration file
 
By default, it is enough to have a single one of them,
however if you have enough configuration settings to justify having multiple copies of it,
feel totally free to split them into multiple files.
#>


<#
# Example Configuration
Set-PSFConfig -Module 'ADMF' -Name 'Example.Setting' -Value 10 -Initialize -Validation 'integer' -Handler { } -Description "Example configuration setting. Your module can then use the setting using 'Get-PSFConfigValue'"
#>


Set-PSFConfig -Module 'ADMF' -Name 'Import.DoDotSource' -Value $false -Initialize -Validation 'bool' -Description "Whether the module files should be dotsourced on import. By default, the files of this module are read as string value and invoked, which is faster but worse on debugging."
Set-PSFConfig -Module 'ADMF' -Name 'Import.IndividualFiles' -Value $false -Initialize -Validation 'bool' -Description "Whether the module files should be imported individually. During the module build, all module code is compiled into few files, which are imported instead by default. Loading the compiled versions is faster, using the individual files is easier for debugging and testing out adjustments."

Set-PSFConfig -Module 'ADMF' -Name 'DCSelectionMode' -Value 'PDCEmulator' -Initialize -Validation 'string' -Description 'When executing commands, specifying the domain name will cause the module to resolve to a single DC to work against. This setting governs the algorythm that determines the DC to work against. Either "PDCEmulator" or "Random" are valid choices.'

Set-PSFConfig -Module 'ADMF' -Name 'VerboseExecution' -Value $true -Initialize -Validation bool -Handler {
    if ($args[0])
    {
        $null = New-PSFMessageLevelModifier -Name ADMF_Verbose -Modifier 0 -IncludeModuleName ADMF
    }
    else
    {
        $null = New-PSFMessageLevelModifier -Name ADMF_Verbose -Modifier 3 -IncludeModuleName ADMF
    }
} -Description 'Enabling this will cause the ADMF module to be more verbose by default'

Set-PSFConfig -Module 'ADMF' -Name 'Context.Store.Default' -Value "$(Get-PSFPath -Name AppData)\ADMF\Contexts" -Initialize -Validation string -Description 'The default path in which ADMF will look for configuration contexts. Add additional such paths by declaring additional settings labeled "ADMF.Context.Store.*"'

<#
Stored scriptblocks are available in [PsfValidateScript()] attributes.
This makes it easier to centrally provide the same scriptblock multiple times,
without having to maintain it in separate locations.
 
It also prevents lengthy validation scriptblocks from making your parameter block
hard to read.
 
Set-PSFScriptblock -Name 'ADMF.ScriptBlockName' -Scriptblock {
     
}
#>

Set-PSFScriptblock -Name 'ADMF.Validate.Type.Gpo' -Scriptblock {
    foreach ($item in $_) {
        if (-not ($item -is [Microsoft.GroupPolicy.Gpo])) { return $false }
    }
    $true
}
Set-PSFScriptblock -Name 'ADMF.Validate.Path' -Scriptblock {
    Test-Path -Path $_
}
Set-PSFScriptblock -Name 'ADMF.Validate.Path.Folder' -Scriptblock {
    $resolvedPath = Resolve-PSFPath -Provider FileSystem -Path $_ -SingleItem
    Test-Path -Path $resolvedPath -PathType Container
}
Set-PSFScriptblock -Name 'ADMF.Validate.ContextStore.ExistsNot' -Scriptblock {
    $_ -notin (Get-AdmfContextStore).Name
}

<#
# Example:
Register-PSFTeppScriptblock -Name "ADMF.alcohol" -ScriptBlock { 'Beer','Mead','Whiskey','Wine','Vodka','Rum (3y)', 'Rum (5y)', 'Rum (7y)' }
#>


Register-PSFTeppScriptblock -Name 'ADMF.Context.Store' -ScriptBlock {
    (Get-AdmfContextStore).Name
}

Register-PSFTeppScriptblock -Name 'ADMF.CredentialProvider' -ScriptBlock {
    $module = Get-Module ADMF
    if (-not $module) { return }
    & $module { $script:credentialProviders.Keys }
}
Register-PSFTeppArgumentCompleter -Command Test-AdmfDomain -Parameter CredentialProvider -Name 'ADMF.CredentialProvider'
Register-PSFTeppArgumentCompleter -Command Invoke-AdmfDomain -Parameter CredentialProvider -Name 'ADMF.CredentialProvider'
Register-PSFTeppArgumentCompleter -Command Test-AdmfForest -Parameter CredentialProvider -Name 'ADMF.CredentialProvider'
Register-PSFTeppArgumentCompleter -Command Invoke-AdmfForest -Parameter CredentialProvider -Name 'ADMF.CredentialProvider'

<#
# Example:
Register-PSFTeppArgumentCompleter -Command Get-Alcohol -Parameter Type -Name ADMF.alcohol
#>

Register-PSFTeppArgumentCompleter -Command New-AdmfContext -Parameter Store -Name 'ADMF.Context.Store'

New-PSFLicense -Product 'ADMF' -Manufacturer 'Friedrich Weinmann' -ProductVersion $script:ModuleVersion -ProductType Module -Name MIT -Version "1.0.0.0" -Date (Get-Date "2019-12-20") -Text @"
Copyright (c) 2019 Friedrich Weinmann
 
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
 
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
 
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"@


# The list of currently applied context sets
$script:loadedContexts = @()

# The list of contexts per domain/server
$script:assignedContexts = @{ }

# The list of registered credentials providers
$script:credentialProviders = @{ }

$callbackScript = {
    [CmdletBinding()]
    param (
        [AllowNull()]
        $Server,
        
        [AllowNull()]
        $Credential,
        
        [AllowNull()]
        $ForestObject,
        
        [AllowNull()]
        $DomainObject
    )
    
    $parameters = $PSBoundParameters | ConvertTo-PSFHashtable -Include Server, Credential
    if ($parameters.Server -eq '<Default Domain>') { $parameters.Server = $env:USERDNSDOMAIN }
    Set-AdmfContext @parameters -Interactive -ReUse -EnableException
}
Register-DMCallback -Name ADMF -ScriptBlock $callbackScript
Register-FMCallback -Name ADMF -ScriptBlock $callbackScript

Set-PSFTypeAlias -Mapping @{
    'UpdateDomainOptions' = 'ADMF.UpdateDomainOptions'
    'UpdateForestOptions' = 'ADMF.UpdateForestOptions'
}

Register-AdmfCredentialProvider -Name default -PreScript {
    param (
        $Data
    )
    $Data.Credential
}
#endregion Load compiled code