ADLDSMF.psm1

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

# Detect whether at some level dotsourcing was enforced
$script:doDotSource = Get-PSFConfigValue -FullName ADLDSMF.Import.DoDotSource -Fallback $false
if ($ADLDSMF_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 ADLDSMF.Import.IndividualFiles -Fallback $false
if ($ADLDSMF_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
    foreach ($path in (& "$ModuleRoot\internal\scripts\preimport.ps1")) {
        . Import-ModuleFile -Path $path
    }
    
    # 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
    foreach ($path in (& "$ModuleRoot\internal\scripts\postimport.ps1")) {
        . Import-ModuleFile -Path $path
    }
    
    # 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 'ADLDSMF' -Language 'en-US'

function New-Change {
    <#
    .SYNOPSIS
        Create a new change object.
     
    .DESCRIPTION
        Create a new change object.
        Helper command that unifies result generation.
     
    .PARAMETER Identity
        The identity the change applies to.
     
    .PARAMETER Property
        What property is being modified.
     
    .PARAMETER OldValue
        The old value that is being updated.
     
    .PARAMETER NewValue
        The new value that will be set instead.
     
    .PARAMETER DisplayStyle
        How the change will display in text form.
        Defaults to: NewValue
     
    .PARAMETER Data
        Additional data to include in the change.
     
    .EXAMPLE
        PS C:\> New-Change -Identity "CN=max,OU=Users,DC=Fabrikam,DC=org" Property LuckyNumber -OldValue 1 -NewValue 42
 
        Creates a new change.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]
        $Identity,
        
        [Parameter(Mandatory = $true)]
        [string]
        $Property,

        [AllowEmptyCollection()]
        [AllowNull()]
        $OldValue,

        [AllowEmptyCollection()]
        [AllowNull()]
        $NewValue,

        [ValidateSet('NewValue', 'RemoveValue')]
        [string]
        $DisplayStyle = 'NewValue',

        [AllowEmptyCollection()]
        [AllowNull()]
        $Data
    )

    $dsStyles = @{
        'NewValue'    = {
            '{0} -> {1}' -f $this.Property, $this.New
        }
        'RemoveValue' = {
            '{0} Remove {1}' -f $this.Property, $this.Old
        }
    }

    $object = [PSCustomObject]@{
        PSTypeName = 'AdLds.Change'
        Identity   = $Identity
        Property   = $Property
        Old        = $OldValue
        New        = $NewValue
        Data       = $Data
    }
    Add-Member -InputObject $object -MemberType ScriptMethod -Name ToString -Value $dsStyles[$DisplayStyle] -Force
    $object
}

function New-Password {
    <#
        .SYNOPSIS
            Generate a new, complex password.
         
        .DESCRIPTION
            Generate a new, complex password.
         
        .PARAMETER Length
            The length of the password calculated.
            Defaults to 32
 
        .PARAMETER AsSecureString
            Returns the password as secure string.
         
        .EXAMPLE
            PS C:\> New-Password
 
            Generates a new 32v character password.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingConvertToSecureStringWithPlainText", "")]
    [CmdletBinding()]
    Param (
        [int]
        $Length = 32,

        [switch]
        $AsSecureString
    )
    
    begin {
        $characters = @{
            0 = @('A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z')
            1 = @('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z')
            2 = @(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9)
            3 = @('#', '$', '%', '&', "'", '(', ')', '*', '+', ',', '-', '.', '/', ':', ';', '<', '=', '>', '?', '@')
            4 = @('A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z')
            5 = @('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z')
            6 = @(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9)
            7 = @('#', '$', '%', '&', "'", '(', ')', '*', '+', ',', '-', '.', '/', ':', ';', '<', '=', '>', '?', '@')
        }
    }
    process {
        $letters = foreach ($number in (5..$Length)) {
            $characters[(($number % 4) + (0..4 | Get-Random))] | Get-Random
        }
        0, 1, 2, 3 | ForEach-Object {
            $letters += $characters[$_] | Get-Random
        }
        $letters = $letters | Sort-Object { Get-Random }
        if ($AsSecureString) { $letters -join "" | ConvertTo-SecureString -AsPlainText -Force }
        else { $letters -join "" }
    }
}

function New-TestResult {
    <#
    .SYNOPSIS
        A new test result, as produced by any of the test commands.
     
    .DESCRIPTION
        A new test result, as produced by any of the test commands.
        This helper function ensures that all test results look the same.
     
    .PARAMETER Type
        What kind object is being tested.
        Should receive the objectclass being affected.
     
    .PARAMETER Action
        What we do with the object in question.
     
    .PARAMETER Identity
        The specific object being changed.
     
    .PARAMETER Change
        Any specific change data that will be applied to the object.
        See New-Change for more details on that structure.
     
    .PARAMETER ADObject
        The actual AD LDS Object being modified.
        Will usually be $null when creating something new.
     
    .PARAMETER Configuration
        The configuration object based on which the change will be applied.
     
    .EXAMPLE
        PS C:\> New-TestResult -Type User -Action Create -Identity $userName -Configuration $configSet
         
        Test result heralding the creation of a new user.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [CmdletBinding()]
    param (
        [string]
        $Type,

        [ValidateSet('Create', 'Update', 'Delete', 'Add', 'Remove', 'Rename')]
        [string]
        $Action,

        [string]
        $Identity,

        $Change,

        $ADObject,

        $Configuration
    )

    [PSCustomObject]@{
        PSTypeName    = 'AdLds.Testresult'
        Type          = $Type
        Action        = $Action
        Identity      = $Identity
        Change        = $Change
        ADObject      = $ADObject
        Configuration = $Configuration
    }
}

function Resolve-SchemaGuid {
    <#
    .SYNOPSIS
        Resolves the name of an attribute or objectclass to its GUID form.
     
    .DESCRIPTION
        Resolves the name of an attribute or objectclass to its GUID form.
        Used to enable user-friendly names in configuration.
 
        + Supports caching requests to optimize performance
        + Will return guids unmodified
     
    .PARAMETER Name
        The name or guid of the attribute or object class.
        Guids will be returned unverified.
     
    .PARAMETER Server
        The LDS Server to connect to.
 
    .PARAMETER Credential
        The credentials - if any - to use to the specified server.
     
    .PARAMETER Cache
        A hashtable used for caching requests.
     
    .EXAMPLE
        PS C:\> Resolve-SchemaGuid -Name contact -Server lds1.contoso.com -Cache $cache
         
        Returns the GUID form of the "contact" object class if present.
    #>

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

        [Parameter(Mandatory = $true)]
        [string]
        $Server,

        [PSCredential]
        $Credential,

        [hashtable]
        $Cache = @{ }
    )

    begin {
        $ldsParam = $PSBoundParameters | ConvertTo-PSFHashtable -Include Server, Credential
    }
    process {
        if ($Name -as [Guid]) {
            return $Name
        }

        if ($Cache[$Name]) {
            return $Cache[$Name]
        }

        $rootDSE = [adsi]"LDAP://$Server/rootDSE"
        $targetObjectClassObj = Get-ADObject @ldsParam -SearchBase ($rootdse.schemaNamingContext.value) -LDAPFilter "CN=$Name" -Properties 'schemaIDGUID'
        if (-not $targetObjectClassObj) {
            throw "Unknown attribute or object class: $Name"
        }
    
        $bytes = [byte[]]$targetObjectClassObj.schemaIDGUID
        $guid = [guid]::new($bytes)
        $Cache[$Name] = "$guid"

        "$guid"
    }
}

function Unprotect-OrganizationalUnit {
    <#
    .SYNOPSIS
        Removes deny rules on OrganizationalUnits.
     
    .DESCRIPTION
        Removes deny rules on OrganizationalUnits.
        Necessary whenever we want to delete an OU.
     
    .PARAMETER Server
        The LDS Server to target.
     
    .PARAMETER Partition
        The Partition on the LDS Server to target.
     
    .PARAMETER Credential
        Credentials to use for the operation.
     
    .PARAMETER Identity
        The OU to unprotect.
        Specify the full distinguishedname.
     
    .EXAMPLE
        PS C:\> Unprotect-OrganizationalUnit @ldsParam -Identity $ouPath
         
        Removes the deletion protection from the OU specified in $ouPath
    #>

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

        [Parameter(Mandatory = $true)]
        [string]
        $Partition,

        [PSCredential]
        $Credential,

        [Parameter(Mandatory = $true)]
        [string]
        $Identity
    )

    begin {
        Update-ADSec
        $ldsParam = $PSBoundParameters | ConvertTo-PSFHashtable -Include Server, Credential
    }

    process {
        $adObject = Get-ADObject @ldsParam -Identity $Identity -Partition $Partition -Properties DistinguishedName

        $acl = Get-AdsAcl @ldsParam -Path $adObject.DistinguishedName
        $denyRules = $acl.Access | Where-Object AccessControlType -eq Deny
        if (-not $denyRules) { return }

        foreach ($rule in $denyRules) {
            $null = $acl.RemoveAccessRule($rule)
        }
        $acl | Set-AdsAcl @ldsParam -Path $adObject.DistinguishedName
    }
}

function Update-ADSec {
    <#
    .SYNOPSIS
        Injects Get-LdsDomain into the ADSec module to overwrite its use of Get-ADDomain.
     
    .DESCRIPTION
        Injects Get-LdsDomain into the ADSec module to overwrite its use of Get-ADDomain.
        This enables us to override the AD domain connection verification performed by the module.
     
    .EXAMPLE
        PS C:\> Update-ADSec
         
        Injects Get-LdsDomain into the ADSec module to overwrite its use of Get-ADDomain.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [CmdletBinding()]
    param ()

    & (Get-Module ADSec) {
        Set-Alias -Name Get-ADDomain -Value Get-LdsDomain -Scope Script
    }
}

function Update-LdsConfiguration {
    <#
    .SYNOPSIS
        Updates the reference to the currently "connected to" LDS instance.
     
    .DESCRIPTION
        Updates the reference to the currently "connected to" LDS instance.
        This is used by Get-LdsDomain, which is injected into the ADSec module to avoid issues with domain resolution.
     
    .PARAMETER LdsServer
        The server hosting the LDS instance.
     
    .PARAMETER LdsPartition
        The partition of the LDS instance.
     
    .EXAMPLE
        PS C:\> Update-LdsConfiguration -LdsServer lds1.contoso.com -LdsPartition 'DC=Fabrikam,DC=org'
         
        Registers lds1.contoso.com as the current server and 'DC=Fabrikam,DC=org' as the current partition.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]
        $LdsServer,

        [Parameter(Mandatory = $true)]
        [string]
        $LdsPartition
    )

    $script:_ldsServer = $LdsServer
    $script:_ldsPartition = $LdsPartition
}

function Get-LdsDomain {
    <#
    .SYNOPSIS
        Returns a pseudo-domain object from an LDS instance.
     
    .DESCRIPTION
        Returns a pseudo-domain object from an LDS instance.
        Use to transparently redirect Get-ADDomain calls.
     
    .PARAMETER LdsServer
        LDS Server instance to use.
        Reads from cache if provided.
     
    .PARAMETER LdsPartition
        LDS partition to use.
        Reads from cache if provided.
     
    .EXAMPLE
        PS C:\> Get-LdsDomain
         
        Returns the default domain
    #>

    param (
        [string]
        $LdsServer = $script:_ldsServer,

        [string]
        $LdsPartition = $script:_ldsPartition
    )

    $object = Get-ADObject -LdapFilter '(objectClass=domainDns)' -Server $LdsServer -SearchBase $LdsPartition -Properties *
    Add-Member -InputObject $object -MemberType NoteProperty -Name NetbiosName -Value $object.Name -Force
    Add-Member -InputObject $object -MemberType NoteProperty -Name DnsRoot -Value ($object.DistinguishedName -replace "DC=" -replace ",", ".") -Force
    $groupSid = Get-ADObject -LdapFilter '(&(objectClass=group)(isCriticalSystemObject=TRUE))' -Server $LdsServer -SearchBase $LdsPartition -Properties ObjectSID -ResultSetSize 1 | ForEach-Object ObjectSID
    Add-Member -InputObject $object -MemberType NoteProperty -Name DomainSID -Value (($groupSid.Value -replace '-\d+$') -as [System.Security.Principal.SecurityIdentifier]) -Force
    $object
}

function Import-LdsConfiguration {
    <#
    .SYNOPSIS
        Import a set of configuration files.
     
    .DESCRIPTION
        Import a set of configuration files.
        Each configuration file must be a psd1, json or (at PS7+) jsonc file.
        They can be stored any levels of nested folder deep, but they cannot be hidden.
 
        Each file shall contain an array of entries and each entry shall have an objectclass plus all the attributes it should have.
        Note to include everything an object of the given type must have.
        For each entry, specifying an objectclass is optional: If none is specified, the name of the parent folder is chosen instead.
        Thus, creating a folder named "user" will have all settings directly within default to the objectclass "user".
 
        Supported Object Classes:
        - AccessRule
        - Group
        - GroupMembership
        - OrganizationalUnit
        - SchemaAttribute
        - User
 
        Note: Group Memberships and access rules are not really object entities in AD LDS, but are treated the same for configuration purposes.
 
        Example Content:
 
        > user.psd1
 
        @{
            Name = 'Thomas'
            Path = 'OU=Admin,%DomainDN%'
            Enabled = $true
        }
     
    .PARAMETER Path
        Path to a folder containing all configuration sets.
     
    .EXAMPLE
        PS C:\> Import-LdsConfiguration -Path C:\scripts\lds\config
 
        Imports all the configuration files under the specified path, no matter how deeply nested.
    #>

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

    $objectClasses = 'AccessRule', 'Group', 'GroupMembership', 'OrganizationalUnit', 'SchemaAttribute', 'User'
    $extensions = '.json', '.psd1'
    if ($PSVersionTable.PSVersion.Major -ge 7) {$extensions = '.json', '.jsonc', '.psd1'}

    foreach ($file in Get-ChildItem -Path $Path -Recurse -File | Where-Object Extension -In $extensions) {
        $datasets = Import-PSFPowerShellDataFile -LiteralPath $file.FullName -Psd1Mode Unsafe
        $defaultObjectClass = $file.Directory.Name.ToLower()

        foreach ($dataset in $datasets) {
            if (-not $dataset.ObjectClass) { $dataset.ObjectClass = $defaultObjectClass }

            switch ($dataset.ObjectClass) {
                'groupmembership' {
                    $identity = "$($dataset.Group)|$($dataset.Member)|$($dataset.Type)"
                    $script:content.groupmembership.$identity = $dataset
                }
                'accessrule' {
                    $identity = "$($dataset.Path)|$($dataset.Identity)|$($dataset.IdentityType)|$($dataset.Rights)|$($dataset.ObjectType)"
                    $script:content.accessrule.$identity = $dataset
                }
                'SchemaAttribute' {
                    $script:content.SchemaAttribute[$dataSet.AttributeID] = $dataSet
                }
                default {
                    if ($dataset.ObjectClass -notin $objectClasses) {
                        Write-PSFMessage -Level Warning -Message 'Invalid Object Class: {0} Importing file "{1}". Legal Values: {2}' -StringValues $dataset.ObjectClass, $file.FullName, ($objectClasses -join ', ') -Tag 'badClass' -Target $dataset
                    }
                    $identity = "$($dataset.Name),$($dataset.Path)"
                    if (-not $script:content.$($dataset.ObjectClass)) {
                        $script:content.$($dataset.ObjectClass) = @{ }
                    }
                    $script:content.$($dataset.ObjectClass)[$identity] = $dataset
                }
            }
        }
    }
}

function Invoke-LdsConfiguration {
    <#
    .SYNOPSIS
        Applies all currently configured settings to the target AD LDS server.
     
    .DESCRIPTION
        Applies all currently configured settings to the target AD LDS server.
        Use Import-LdsConfiguration first to load one or more configuration sets.
     
    .PARAMETER Server
        The LDS Server to target.
     
    .PARAMETER Partition
        The Partition on the LDS Server to target.
     
    .PARAMETER Credential
        Credentials to use for the operation.
     
    .PARAMETER Options
        Which part of the configuration to deploy.
        Defaults to all of them ('User', 'Group', 'OrganizationalUnit', 'GroupMembership', 'AccessRule', 'SchemaAttribute')
 
    .PARAMETER Delete
        Undo everything defined in configuration.
        Allows rolling back after deployment.
         
    .EXAMPLE
        PS C:\> Invoke-LdsConfiguration -Server lds1.contoso.com -Partition 'DC=fabrikam,DC=org'
         
        Applies all currently configured settings to the target AD LDS server.
         
    .EXAMPLE
        PS C:\> Invoke-LdsConfiguration -Server lds1.contoso.com -Partition 'DC=fabrikam,DC=org' -Options User, Group, OrganizationalUnit
         
        Applies all currently configured users, groups and OUs to the target AD LDS server.
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)]
        [string]
        $Server,

        [Parameter(Mandatory = $true)]
        [string]
        $Partition,

        [PSCredential]
        $Credential,

        [ValidateSet('User', 'Group', 'OrganizationalUnit', 'GroupMembership', 'AccessRule', 'SchemaAttribute')]
        [string[]]
        $Options = @('User', 'Group', 'OrganizationalUnit', 'GroupMembership', 'AccessRule', 'SchemaAttribute'),

        [switch]
        $Delete
    )
    
    begin {
        $ldsParam = $PSBoundParameters | ConvertTo-PSFHashtable -Include Server, Partition, Credential
    }
    process {
        if ($Options -contains 'SchemaAttribute') {
            Invoke-LdsSchemaAttribute @ldsParam
        }
        if ($Options -contains 'OrganizationalUnit') {
            Invoke-LdsOrganizationalUnit @ldsParam -Delete:$Delete
        }
        if ($Options -contains 'Group') {
            Invoke-LdsGroup @ldsParam -Delete:$Delete
        }
        if ($Options -contains 'User') {
            Invoke-LdsUser @ldsParam -Delete:$Delete
        }
        if ($Options -contains 'GroupMembership') {
            Invoke-LdsGroupMembership @ldsParam -Delete:$Delete
        }
        if ($Options -contains 'AccessRule') {
            Invoke-LdsAccessRule @ldsParam -Delete:$Delete
        }
    }
}

function Reset-LdsAccountPassword {
    <#
    .SYNOPSIS
        Reset the password of any given user account.
     
    .DESCRIPTION
        Reset the password of any given user account.
        The new password will be pasted to clipboard.
     
    .PARAMETER UserName
        Name of the user to reset.
     
    .PARAMETER Server
        LDS Server to contact.
     
    .PARAMETER Partition
        Partition of the LDS Server to search.
 
    .PARAMETER NewPassword
        The new password to assign.
        Autogenerates a random password if not specified.
     
    .PARAMETER Credential
        Credential to use for the request
     
    .EXAMPLE
        PS C:\> Reset-LdsAccountPassword -Name svc_whatever -Server lds1.contoso.com -Partition 'DC=fabrikam,DC=org'
         
        Resets the password of account 'svc_whatever'
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]
        $UserName,

        [Parameter(Mandatory = $true)]
        [string]
        $Server,

        [Parameter(Mandatory = $true)]
        [string]
        $Partition,

        [SecureString]
        $NewPassword = (New-Password -AsSecureString),

        [PSCredential]
        $Credential
    )

    $ldsParam = $PSBoundParameters | ConvertTo-PSFHashtable -Include Server, Partition, Credential
    $ldsParamLight = $ldsParam | ConvertTo-PSFHashtable -Exclude Partition
    $userObject = Get-ADUser @ldsParamLight -LDAPFilter "(name=$UserName)" -SearchBase $Partition
    if (-not $userObject) {
        Stop-PSFFunction -Cmdlet $PSCmdlet -Message "Unable to find $UserName!" -EnableException $true
    }

    if (1 -lt @($userObject).Count) {
        Stop-PSFFunction -Cmdlet $PSCmdlet -Message "More than one account found for $UserName!`n$($userObject.DistinguishedName -join "`n")" -EnableException $true
    }

    Set-ADAccountPassword @ldsParam -NewPassword $NewPassword -Identity $userObject.ObjectGUID

    if (-not $userObject.Enabled) {
        Write-PSFMessage -Level Host -Message "Enabling account: $($userObject.Name)"
        Enable-ADAccount @ldsParam -Identity $userObject.ObjectGuid
    }

    Write-PSFMessage -Level Host -Message "Password reset for $($userObject.Name) executed."
    $null = Read-Host "Press enter to paste the new password to the clipboard."
    $cred = [PSCredential]::new("whatever", $NewPassword)
    $cred.GetNetworkCredential().Password | Set-Clipboard
    Write-PSFMessage -Level Host -Message "Password for $($userObject.Name) has been written to clipboard."
}

function Reset-LdsConfiguration {
    <#
    .SYNOPSIS
        Removes all registered configuration settings.
     
    .DESCRIPTION
        Removes all registered configuration settings.
     
    .EXAMPLE
        PS C:\> Reset-LdsConfiguration
         
        Removes all registered configuration settings.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [CmdletBinding()]
    param ()

    $script:content = @{
        user               = @{ }
        group              = @{ }
        organizationalUnit = @{ }
        groupmembership    = @{ }
        accessrule         = @{ }
        SchemaAttribute    = @{ }
    }
}

function Test-LdsConfiguration {
    <#
    .SYNOPSIS
        Test all configured settings against the target LDS instance.
     
    .DESCRIPTION
        Test all configured settings against the target LDS instance.
     
    .PARAMETER Server
        The LDS Server to target.
     
    .PARAMETER Partition
        The Partition on the LDS Server to target.
     
    .PARAMETER Credential
        Credentials to use for the operation.
     
    .PARAMETER Options
        Which part of the configuration to test for.
        Defaults to all of them ('User', 'Group', 'OrganizationalUnit', 'GroupMembership', 'AccessRule', 'SchemaAttribute')
 
    .PARAMETER Delete
        Undo everything defined in configuration.
        Allows rolling back after deployment.
     
    .EXAMPLE
        PS C:\> Test-LdsConfiguration -Server lds1.contoso.com -Partition 'DC=Fabrikam,DC=org'
 
        Test all configured settings against the 'DC=Fabrikam,DC=org' LDS instance on server lds1.contoso.com.
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)]
        [string]
        $Server,

        [Parameter(Mandatory = $true)]
        [string]
        $Partition,

        [PSCredential]
        $Credential,

        [ValidateSet('User', 'Group', 'OrganizationalUnit', 'GroupMembership', 'AccessRule', 'SchemaAttribute')]
        [string[]]
        $Options = @('User', 'Group', 'OrganizationalUnit', 'GroupMembership', 'AccessRule', 'SchemaAttribute'),

        [switch]
        $Delete
    )
    
    begin {
        $ldsParam = $PSBoundParameters | ConvertTo-PSFHashtable -Include Server, Partition, Credential, Delete
    }
    process {
        if ($Options -contains 'SchemaAttribute' -and -not $Delete) {
            Test-LdsSchemaAttribute @ldsParam
        }
        if ($Options -contains 'OrganizationalUnit') {
            Test-LdsOrganizationalUnit @ldsParam
        }
        if ($Options -contains 'Group') {
            Test-LdsGroup @ldsParam
        }
        if ($Options -contains 'User') {
            Test-LdsUser @ldsParam
        }
        if ($Options -contains 'GroupMembership') {
            Test-LdsGroupMembership @ldsParam
        }
        if ($Options -contains 'AccessRule') {
            Test-LdsAccessRule @ldsParam
        }
    }
}

function Invoke-LdsAccessRule {
    <#
    .SYNOPSIS
        Applies all the configured access rules.
     
    .DESCRIPTION
        Applies all the configured access rules.
     
    .PARAMETER Server
        The LDS Server to target.
     
    .PARAMETER Partition
        The Partition on the LDS Server to target.
     
    .PARAMETER Credential
        Credentials to use for the operation.
     
    .PARAMETER Delete
        Undo everything defined in configuration.
        Allows rolling back after deployment.
     
    .PARAMETER TestResult
        Result objects of the associated Test-Command.
        Allows cherry-picking which change to apply.
        If not specified, it will a test and apply all test results instead.
     
    .EXAMPLE
        PS C:\> Invoke-LdsAccessRule -Server lds1.contoso.com -Partition 'DC=fabrikam,DC=org'
 
        Apply all configured access rules to the 'DC=fabrikam,DC=org' partition on lds1.contoso.com
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)]
        [string]
        $Server,

        [Parameter(Mandatory = $true)]
        [string]
        $Partition,

        [PSCredential]
        $Credential,

        [switch]
        $Delete,

        [Parameter(ValueFromPipeline = $true)]
        $TestResult
    )
    
    begin {
        Update-ADSec
        Update-LdsConfiguration -LdsServer $Server -LdsPartition $Partition
        $ldsParam = $PSBoundParameters | ConvertTo-PSFHashtable -Include Server, Credential
    }
    process {
        if (-not $TestResult) {
            $TestResult = Test-LdsAccessRule @ldsParam -Partition $Partition -Delete:$Delete
        }
        foreach ($testItem in $TestResult | Sort-Object Action -Descending) {
            switch ($testItem.Action) {
                'Add' {
                    $acl = Get-AdsAcl @ldsParam -Path $testItem.Identity
                    $acl.AddAccessRule($testItem.Change.Rule)
                    $acl | Set-AdsAcl @ldsParam -Path $testItem.Identity
                }
                'Remove' {
                    $acl = Get-AdsAcl @ldsParam -Path $testItem.Identity
                    $null = $acl.RemoveAccessRule($testItem.Change.Rule)
                    $acl | Set-AdsAcl @ldsParam -Path $testItem.Identity
                }
            }
        }
    }
}

function Test-LdsAccessRule {
    <#
    .SYNOPSIS
        Tests, whether the current access rules match the configured state.
     
    .DESCRIPTION
        Tests, whether the current access rules match the configured state.
     
    .PARAMETER Server
        The LDS Server to target.
     
    .PARAMETER Partition
        The Partition on the LDS Server to target.
     
    .PARAMETER Credential
        Credentials to use for the operation.
     
    .PARAMETER Delete
        Undo everything defined in configuration.
        Allows rolling back after deployment.
     
    .EXAMPLE
        PS C:\> Test-LdsAccessRule -Server lds1.contoso.com -Partition 'DC=fabrikam,DC=org'
 
        Tests, whether the current access rules on lds1.contoso.com match the configured state.
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)]
        [string]
        $Server,

        [Parameter(Mandatory = $true)]
        [string]
        $Partition,

        [PSCredential]
        $Credential,

        [switch]
        $Delete
    )
    
    begin {
        #region Functions
        function Resolve-AccessRule {
            [OutputType([System.DirectoryServices.ActiveDirectoryAccessRule])]
            [CmdletBinding()]
            param (
                [Parameter(Mandatory = $true)]
                $RuleCfg,

                [Parameter(Mandatory = $true)]
                [string]
                $Server,

                [Parameter(Mandatory = $true)]
                [string]
                $Partition,

                [PSCredential]
                $Credential,

                [hashtable]
                $SchemaCache = @{ },

                [hashtable]
                $PrincipalCache = @{ },

                [string]
                $DomainSID
            )

            $ldsParam = $PSBoundParameters | ConvertTo-PSFHashtable -Include Server, Credential

            $rights = $script:adrights[$RuleCfg.Rights]
            
            # resolve AccessRule settings
            $inheritanceType = 'None'
            if ($RuleCfg.Inheritance) {
                $inheritanceType = $RuleCfg.Inheritance
            }
            $objectType = [guid]::Empty
            $inheritedObjectType = [guid]::Empty
            if ($RuleCfg.ObjectType) { $objectType = $RuleCfg.ObjectType | Resolve-SchemaGuid @ldsParam -Cache $SchemaCache }
            if ($RuleCfg.InheritedObjectType) { $inheritedObjectType = $RuleCfg.InheritedObjectType | Resolve-SchemaGuid @ldsParam -Cache $SchemaCache }
            $type = 'Allow'
            if ($RuleCfg.Type) { $type = $RuleCfg.Type }

            $principal = $PrincipalCache["$($RuleCfg.IdentityType):$($RuleCfg.Identity)"]
            if (-not $principal -and 'SID' -eq $RuleCfg.IdentityType){
                $ruleIdentity = $RuleCfg.Identity -replace '%DomainSID%', $DomainSID
                $sid = $ruleIdentity -as [System.Security.Principal.SecurityIdentifier]
                if (-not $sid) {
                    throw "Principal is not a legal SID: $($RuleCfg.Identity)!"
                }
                $PrincipalCache["$($RuleCfg.IdentityType):$($RuleCfg.Identity)"] = $sid
                $principal = $PrincipalCache["$($RuleCfg.IdentityType):$($RuleCfg.Identity)"]
            }
            elseif (-not $principal) {
                $principalObject = Get-ADObject @ldsParam -SearchBase $Partition -LDAPFilter "(&(objectClass=$($RuleCfg.IdentityType))(name=$($RuleCfg.Identity)))" -Properties ObjectSID -ErrorAction Stop
                if (-not $principalObject) {
                    throw "Principal not found: $($RuleCfg.IdentityType) - $($RuleCfg.Identity)"
                }

                $PrincipalCache["$($RuleCfg.IdentityType):$($RuleCfg.Identity)"] = $principalObject.ObjectSID
                $principal = $PrincipalCache["$($RuleCfg.IdentityType):$($RuleCfg.Identity)"]
            }

            [System.DirectoryServices.ActiveDirectoryAccessRule]::new(
                $principal,
                $rights,
                $type,
                $objectType,
                $inheritanceType,
                $inheritedObjectType
            )
        }
        
        function Compare-AccessRule {
            [CmdletBinding()]
            param (
                [Parameter(Mandatory = $true)]
                [System.DirectoryServices.ActiveDirectoryAccessRule[]]
                $Reference,

                [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
                $InputObject,

                [switch]
                $NoMatch
            )

            process {
                if (-not $InputObject) { return }


                $isMatched = $false

                foreach ($referenceObject in $Reference) {
                    if ($referenceObject.ActiveDirectoryRights -bxor $InputObject.ActiveDirectoryRights) { continue }
                    if ($referenceObject.InheritanceType -ne $InputObject.InheritanceType) { continue }
                    if ($referenceObject.ObjectType -ne $InputObject.ObjectType) { continue }
                    if ($referenceObject.InheritedObjectType -ne $InputObject.InheritedObjectType) { continue }
                    if ($referenceObject.AccessControlType -ne $InputObject.AccessControlType) { continue }
                    if ("$($referenceObject.IdentityReference)" -ne "$($InputObject.IdentityReference)") { continue }

                    $isMatched = $true
                    break
                }

                if ($isMatched -eq -not $NoMatch) {
                    $InputObject
                }
            }
        }

        function Get-ObjectDefaultRule {
            [CmdletBinding()]
            param (
                [string]
                $Path,

                [hashtable]
                $LdsParam,

                [hashtable]
                $LdsParamLight,

                $RootDSE,

                [hashtable]
                $DefaultPermissions
            )

            $adObject = Get-ADObject @LdsParam -Identity $Path -Properties ObjectClass
            if ($DefaultPermissions.ContainsKey($adObject.ObjectClass)) {
                return $DefaultPermissions[$adObject.ObjectClass]
            }

            $class = Get-ADObject @ldsParamLight -SearchBase $RootDSE.schemaNamingContext -LDAPFilter "(&(objectClass=classSchema)(ldapDisplayName=$($adObject.ObjectClass)))" -Properties defaultSecurityDescriptor
            $acl = [System.DirectoryServices.ActiveDirectorySecurity]::new()
            $acl.SetSecurityDescriptorSddlForm($class.defaultSecurityDescriptor)
            $DefaultPermissions[$adObject.ObjectClass] = $acl.GetAccessRules($true, $false, [System.Security.Principal.SecurityIdentifier])
            $DefaultPermissions[$adObject.ObjectClass]
        }
        #endregion Functions

        Update-ADSec
        Update-LdsConfiguration -LdsServer $Server -LdsPartition $Partition
        $ldsParam = $PSBoundParameters | ConvertTo-PSFHashtable -Include Server, Partition, Credential
        $ldsParamLight = $ldsParam | ConvertTo-PSFHashtable -Exclude Partition
        $rootDSE = Get-ADRootDSE @ldsParamLight
        $domainSID = (Get-ADObject @ldsParamLight -LDAPFilter '(&(objectCategory=group)(name=Administrators))' -SearchBase $ldsParam.Partition -Properties objectSID).ObjectSID.Value -replace '-512$'

        $principals = @{ }
        $schemaCache = @{ }
        $pathCache = @{ }
    }
    process {
        #region Adding
        foreach ($ruleCfg in $script:content.accessrule.Values) {
            $resolvedPath = $ruleCfg.Path -replace '%DomainDN%', $Partition
            try { $rule = Resolve-AccessRule @ldsParam -RuleCfg $ruleCfg -SchemaCache $schemaCache -PrincipalCache $principals -DomainSID $domainSID }
            catch {
                Write-PSFMessage -Level Warning -Message "Failed to process rule for $resolvedPath, granting $($ruleCfg.Rights) to $($ruleCfg.Identity)" -ErrorRecord $_
                continue
            }
            if (-not $pathCache[$resolvedPath]) { $pathCache[$resolvedPath] = @($rule) }
            else { $pathCache[$resolvedPath] = @($pathCache[$resolvedPath]) + @($rule) }

            $acl = Get-AdsAcl @ldsParamLight -Path $resolvedPath
            $currentRules = $acl.GetAccessRules($true, $false, [System.Security.Principal.SecurityIdentifier])
            $matching = $currentRules | Compare-AccessRule -Reference $rule

            $change = [PSCustomObject]@{
                Path  = $resolvedPath
                Name  = $ruleCfg.Identity
                Right = $ruleCfg.Rights
                Type  = $rule.AccessControlType
                Rule  = $rule
            }
            Add-Member -InputObject $change -MemberType ScriptMethod -Name ToString -Force -Value {
                if ('Allow' -eq $this.Type) { '{0} -> {1}' -f $this.Name, $this.Right }
                else { '{0} != {1}' -f $this.Name, $this.Right }
            }

            if ($matching) {
                if ($Delete) {
                    $change.Rule = $matching
                    New-TestResult -Type AccessRule -Action Remove -Identity $resolvedPath -Configuration $ruleCfg -ADObject $acl -Change $change
                }
                continue
            }
            if ($Delete) { continue }

            New-TestResult -Type AccessRule -Action Add -Identity $resolvedPath -Configuration $ruleCfg -ADObject $acl -Change $change
        }
        #endregion Adding

        #region Removing
        $schemaDefaultPermissions = @{ }
        $sidToName = @{ }

        foreach ($adPath in $pathCache.Keys) {
            $defaultRules = Get-ObjectDefaultRule -Path $adPath -LdsParam $ldsParam -LdsParamLight $ldsParamLight -RootDSE $rootDSE -DefaultPermissions $schemaDefaultPermissions
            $intendedRules = @($defaultRules) + @($pathCache[$adPath]) | Remove-PSFNull

            $acl = Get-AdsAcl @ldsParamLight -Path $adPath
            $currentRules = $acl.GetAccessRules($true, $false, [System.Security.Principal.SecurityIdentifier])
            $surplusRules = $currentRules | Compare-AccessRule -Reference $intendedRules -NoMatch

            foreach ($surplusRule in $surplusRules) {
                # Skip OU deletion protection
                if ('S-1-1-0' -eq $surplusRule.IdentityReference -and 'Deny' -eq $surplusRule.AccessControlType) { continue }
                
                if (-not $sidToName[$surplusRule.IdentityReference]) {
                    try { $sidToName[$surplusRule.IdentityReference] = Get-ADObject @ldsParamLight -SearchBase $Partition -LDAPFilter "(objectSID=$($surplusRule.IdentityReference))" -Properties Name }
                    catch { $sidToName[$surplusRule.IdentityReference] = @{ Name = $surplusRule.IdentityReference }}
                    if (-not $sidToName[$surplusRule.IdentityReference]) { $sidToName[$surplusRule.IdentityReference] = @{ Name = $surplusRule.IdentityReference } }
                }
                $change = [PSCustomObject]@{
                    Path  = $adPath
                    Name  = $sidToName[$surplusRule.IdentityReference].Name
                    Right = $surplusRule.ActiveDirectoryRights
                    Type  = $surplusRule.AccessControlType
                    Rule  = $surplusRule
                }

                Add-Member -InputObject $change -MemberType ScriptMethod -Name ToString -Force -Value {
                    if ('Allow' -eq $this.Type) { '{0} -> {1}' -f $this.Name, $this.Right }
                    else { '{0} != {1}' -f $this.Name, $this.Right }
                }
    
                New-TestResult -Type AccessRule -Action Remove -Identity $adPath -ADObject $acl -Change $change
            }
        }
        #endregion Removing
    }
}

function Invoke-LdsGroup {
    <#
    .SYNOPSIS
        Applies all configured groups.
     
    .DESCRIPTION
        Applies all configured groups, creating or updating their settings as needed.
     
    .PARAMETER Server
        The LDS Server to target.
     
    .PARAMETER Partition
        The Partition on the LDS Server to target.
     
    .PARAMETER Credential
        Credentials to use for the operation.
     
    .PARAMETER Delete
        Undo everything defined in configuration.
        Allows rolling back after deployment.
     
    .PARAMETER TestResult
        Result objects of the associated Test-Command.
        Allows cherry-picking which change to apply.
        If not specified, it will a test and apply all test results instead.
     
    .EXAMPLE
        PS C:\> Invoke-LdsGroup -Server lds1.contoso.com -Partition 'DC=fabrikam,DC=org'
 
        Applies all configured groups to 'DC=fabrikam,DC=org' on the server 'lds1.contoso.com'.
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)]
        [string]
        $Server,

        [Parameter(Mandatory = $true)]
        [string]
        $Partition,

        [PSCredential]
        $Credential,

        [switch]
        $Delete,

        [Parameter(ValueFromPipeline = $true)]
        $TestResult
    )
    
    begin {
        Update-LdsConfiguration -LdsServer $Server -LdsPartition $Partition
        $ldsParam = $PSBoundParameters | ConvertTo-PSFHashtable -Include Server, Partition, Credential
        $ldsParamLight = $ldsParam | ConvertTo-PSFHashtable -Exclude Partition
        $systemProperties = 'ObjectClass', 'Path', 'Name', 'GroupScope'
    }
    process {
        if (-not $TestResult) {
            $TestResult = Test-LdsGroup @ldsParam -Delete:$Delete
        }
        foreach ($testItem in $TestResult) {
            switch ($testItem.Action) {
                'Create' {
                    $attributes = $testItem.Configuration | ConvertTo-PSFHashtable -Exclude $systemProperties
                    $newParam = @{
                        Name = $testItem.Configuration.Name
                        GroupScope = $testItem.Configuration.GroupScope
                        Path = ($testItem.Identity -replace '^.+?,')
                    }
                    if (0 -lt $attributes.Count) { $newParam.OtherAttributes = $attributes }
                    if (-not $newParam.GroupScope) { $newParam.GroupScope = 'DomainLocal' }
                    New-ADGroup @ldsParamLight @newParam
                }
                'Delete' {
                    Remove-ADGroup @ldsParam -Identity $testItem.ADObject.ObjectGUID -Recursive -Confirm:$false
                }
                'Update' {
                    $update = @{ }
                    foreach ($change in $testItem.Change) {
                        $update[$change.Property] = $change.New
                    }
                    Set-ADObject @ldsParam -Identity $testItem.ADObject.ObjectGUID -Replace $update
                }
            }
        }
    }
}

function Test-LdsGroup {
    <#
    .SYNOPSIS
        Tests, whether the targeted ad lds server conforms to the group configuration.
     
    .DESCRIPTION
        Tests, whether the targeted ad lds server conforms to the group configuration.
     
    .PARAMETER Server
        The LDS Server to target.
     
    .PARAMETER Partition
        The Partition on the LDS Server to target.
     
    .PARAMETER Credential
        Credentials to use for the operation.
     
    .PARAMETER Delete
        Undo everything defined in configuration.
        Allows rolling back after deployment.
     
    .EXAMPLE
        PS C:\> Test-LdsGroup -Server lds1.contoso.com -Partition 'DC=fabrikam,DC=org'
 
        Tests whether the groups in 'DC=fabrikam,DC=org' on lds1.contoso.com are in their desired state.
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)]
        [string]
        $Server,

        [Parameter(Mandatory = $true)]
        [string]
        $Partition,

        [PSCredential]
        $Credential,

        [switch]
        $Delete
    )
    
    begin {
        Update-LdsConfiguration -LdsServer $Server -LdsPartition $Partition
        $ldsParam = $PSBoundParameters | ConvertTo-PSFHashtable -Include Server, Partition, Credential
        $systemProperties = 'ObjectClass', 'Path', 'Name'
    }
    process {
        foreach ($configurationItem in $script:content.group.Values) {
            $path = 'CN={0},{1}' -f $configurationItem.Name, ($configurationItem.Path -replace '%DomainDN%',$Partition)
            if ($path -notmatch ',DC=') { $path = $path, $Partition -join ',' }

            $resultDefaults = @{
                Type = 'Group'
                Identity = $path
                Configuration = $configurationItem
            }

            $failed = $null
            $adObject = $null
            try { $adObject = Get-ADGroup @ldsParam -Identity $path -Properties * -ErrorAction SilentlyContinue -ErrorVariable failed }
            catch { $failed = $_ }
            if ($failed -and $failed.CategoryInfo.Category -ne 'ObjectNotFound') {
                foreach ($failure in $failed) { Write-Error $failure }
                continue
            }

            #region Cases
            # Case: Does not Exist
            if (-not $adObject) {
                if ($Delete) { continue }

                New-TestResult @resultDefaults -Action Create
                continue
            }

            # Case: Exists
            $resultDefaults.ADObject = $adObject
            if ($Delete) {
                New-TestResult @resultDefaults -Action Delete
                continue
            }

            $changes = foreach ($pair in $configurationItem.GetEnumerator()) {
                if ($pair.Key -in $systemProperties) { continue }
                if ($pair.Value -ne $adObject.$($pair.Key)) {
                    New-Change -Identity $path -Property $pair.Key -OldValue $adObject.$($pair.Key) -NewValue $pair.Value
                }
            }

            if ($changes) {
                New-TestResult @resultDefaults -Action Update -Change $changes
            }
            #endregion Cases
        }
    }
}

function Invoke-LdsGroupMembership {
    <#
    .SYNOPSIS
        Applies the configuration-defined group memberships.
     
    .DESCRIPTION
        Applies the configuration-defined group memberships.
        It is generally good idea to apply groups and users first.
     
    .PARAMETER Server
        The LDS Server to target.
     
    .PARAMETER Partition
        The Partition on the LDS Server to target.
     
    .PARAMETER Credential
        Credentials to use for the operation.
     
    .PARAMETER Delete
        Undo everything defined in configuration.
        Allows rolling back after deployment.
     
    .PARAMETER TestResult
        Result objects of the associated Test-Command.
        Allows cherry-picking which change to apply.
        If not specified, it will a test and apply all test results instead.
     
    .EXAMPLE
        PS C:\> Invoke-LdsGroupMembership -Server lds1.contoso.com -Partition 'DC=fabrikam,DC=org'
 
        Applies the configuration-defined group memberships against 'DC=fabrikam,DC=org' on lds1.contoso.com.
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)]
        [string]
        $Server,

        [Parameter(Mandatory = $true)]
        [string]
        $Partition,

        [PSCredential]
        $Credential,

        [switch]
        $Delete,

        [Parameter(ValueFromPipeline = $true)]
        $TestResult
    )
    
    begin {
        Update-LdsConfiguration -LdsServer $Server -LdsPartition $Partition
        $ldsParam = $PSBoundParameters | ConvertTo-PSFHashtable -Include Server, Partition, Credential
    }
    process {
        if (-not $TestResult) {
            $TestResult = Test-LdsGroupMembership @ldsParam -Delete:$Delete
        }
        foreach ($testItem in $TestResult) {
            switch ($testItem.Action) {
                'Update' {
                    foreach ($change in $testItem.Change) {
                        switch ($change.Action) {
                            'Add' { Add-ADGroupMember @ldsParam -Identity $testItem.ADObject -Members $change.DN }
                            'Remove' { Remove-ADGroupMember @ldsParam -Identity $testItem.ADObject -Members $change.DN }
                        }
                    }
                }
            }
        }
    }
}


function Test-LdsGroupMembership {
    <#
    .SYNOPSIS
        Test whether the group memberships are in their desired state.
     
    .DESCRIPTION
        Test whether the group memberships are in their desired state.
     
    .PARAMETER Server
        The LDS Server to target.
     
    .PARAMETER Partition
        The Partition on the LDS Server to target.
     
    .PARAMETER Credential
        Credentials to use for the operation.
     
    .PARAMETER Delete
        Undo everything defined in configuration.
        Allows rolling back after deployment.
     
    .EXAMPLE
        PS C:\> Test-LdsGroupMembership -Server lds1.contoso.com -Partition 'DC=fabrikam,DC=org'
         
        Test whether the group memberships are in their desired state for 'DC=fabrikam,DC=org' on lds1.contoso.com
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)]
        [string]
        $Server,

        [Parameter(Mandatory = $true)]
        [string]
        $Partition,

        [PSCredential]
        $Credential,

        [switch]
        $Delete
    )
    
    begin {
        Update-LdsConfiguration -LdsServer $Server -LdsPartition $Partition
        $ldsParam = $PSBoundParameters | ConvertTo-PSFHashtable -Include Server, Partition, Credential
        $ldsParamLight = $ldsParam | ConvertTo-PSFHashtable -Remap @{ Partition = 'SearchBase' }

        $members = @{ }
        $ldsObjects = @{ }
    }
    process {
        foreach ($configurationSets in $script:content.groupmembership.Values | Group-Object { $_.Group }) {
            Write-PSFMessage -Level Verbose -Message "Processing group memberships of {0}" -StringValues $configurationSets.Name
            $groupObject = Get-ADGroup @ldsParamLight -LDAPFilter "(name=$($configurationSets.Name))" -Properties *
            if (-not $groupObject) {
                Write-PSFMessage -Level Warning -Message "Group not found: {0}! Cannot process members" -StringValues  $configurationSets.Name
                continue
            }
            $ldsObjects[$groupObject.DistinguishedName] = $groupObject

            #region Determine intended members
            $intendedMembers = foreach ($entry in $configurationSets.Group) {
                # Read from Cache
                if ($members["$($entry.Type):$($entry.Member)"]) {
                    $members["$($entry.Type):$($entry.Member)"]
                    continue
                }

                # Read from LDS Instance
                $ldsObject = Get-ADObject @ldsParamLight -LDAPFilter "(&(objectClass=$($entry.Type))(name=$($entry.Member)))" -Properties *
                
                # Not Yet Created
                if (-not $ldsObject) {
                    Write-PSFMessage -Level Warning -Message 'Unable to find {0} {1}, will be unable to add it to group {2}' -StringValues $entry.Type, $entry.Member, $entry.Group
                    continue
                }

                $members["$($entry.Type):$($entry.Member)"] = $ldsObject
                $ldsObjects[$ldsObject.DistinguishedName] = $ldsObject
                $ldsObject
            }
            #endregion Determine intended members

            #region Determine actual members
            $actualMembers = foreach ($member in $groupObject.Members) {
                if ($ldsObjects[$member]) {
                    $ldsObjects[$member]
                    continue
                }

                try { $ldsObject = Get-ADObject @ldsParam -Identity $member -Properties * -ErrorAction Stop }
                catch {
                    Write-PSFMessage -Level Warning -Message "Error resolving member of {0}: {1}" -StringValues $configurationSets.Name, $member -ErrorRecord $_
                    continue
                }
                $ldsObjects[$ldsObject.DistinguishedName] = $ldsObject
                $ldsObject
            }
            #endregion Determine actual members
        
            #region Compare and generate changes
            $toAdd = $intendedMembers | Where-Object DistinguishedName -NotIn $actualMembers.DistinguishedName | ForEach-Object {
                [PSCustomObject]@{
                    PSTypename = 'AdLdsTools.Change.GroupMembership'
                    Action     = 'Add'
                    Member     = $_.Name
                    Type       = $_.ObjectClass
                    DN         = $_.DistinguishedName
                    Group      = $configurationSets.Name
                }
            }
            if ($Delete) { $toAdd = @() }

            $toRemove = $actualMembers | Where-Object {
                (-not $Delete -and $_.DistinguishedName -NotIn $intendedMembers.DistinguishedName) -or
                ($Delete -and $_.DistinguishedName -in $intendedMembers.DistinguishedName)
            } | ForEach-Object {
                [PSCustomObject]@{
                    PSTypename = 'AdLdsTools.Change.GroupMembership'
                    Action     = 'Remove'
                    Member     = $_.Name
                    Type       = $_.ObjectClass
                    DN         = $_.DistinguishedName
                    Group      = $configurationSets.Name
                }
            }

            $changes = @($toAdd) + @($toRemove) | Remove-PSFNull | Add-Member -MemberType ScriptMethod -Name ToString -Value {
                '{0} -> {1}' -f $this.Action, $this.Member
            } -Force -PassThru
            #endregion Compare and generate changes

            if ($changes) {
                New-TestResult -Type GroupMemberShip -Action Update -Identity $groupObject.Name -ADObject $groupObject -Configuration $configurationSets -Change $changes
            }
        }
    }
}

function Invoke-LdsOrganizationalUnit {
    <#
    .SYNOPSIS
        Creates the desired organizational units.
     
    .DESCRIPTION
        Creates the desired organizational units.
     
    .PARAMETER Server
        The LDS Server to target.
     
    .PARAMETER Partition
        The Partition on the LDS Server to target.
     
    .PARAMETER Credential
        Credentials to use for the operation.
     
    .PARAMETER Delete
        Undo everything defined in configuration.
        Allows rolling back after deployment.
     
    .PARAMETER TestResult
        Result objects of the associated Test-Command.
        Allows cherry-picking which change to apply.
        If not specified, it will a test and apply all test results instead.
     
    .EXAMPLE
        PS C:\> Invoke-LdsOrganizationalUnit -Server lds1.contoso.com -Partition 'DC=fabrikam,DC=org'
         
        Creates the desired organizational units in 'DC=fabrikam,DC=org' on lds1.contoso.com
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)]
        [string]
        $Server,

        [Parameter(Mandatory = $true)]
        [string]
        $Partition,

        [PSCredential]
        $Credential,

        [switch]
        $Delete,

        [Parameter(ValueFromPipeline = $true)]
        $TestResult
    )
    
    begin {
        Update-LdsConfiguration -LdsServer $Server -LdsPartition $Partition
        $ldsParam = $PSBoundParameters | ConvertTo-PSFHashtable -Include Server, Partition, Credential
        $ldsParamLight = $ldsParam | ConvertTo-PSFHashtable -Exclude Partition
        $systemProperties = 'ObjectClass', 'Path', 'Name'
        $filter = {
            # Delete actions should go from innermost to top-level
            # Create actions should go from top-level to most nested
            if ($_.Action -eq 'Delete') { $_.Identity.Length * -1 }
            else { $_.Identity.Length }
        }
    }
    process {
        if (-not $TestResult) {
            $TestResult = Test-LdsOrganizationalUnit @ldsParam -Delete:$Delete
        }
        foreach ($testItem in $TestResult | Sort-Object Action, $filter) {
            switch ($testItem.Action) {
                'Create' {
                    $attributes = $testItem.Configuration | ConvertTo-PSFHashtable -Exclude $systemProperties
                    $newParam = @{
                        Name = $testItem.Configuration.Name
                        Path = ($testItem.Identity -replace '^.+?,')
                    }
                    if (0 -lt $attributes.Count) { $newParam.OtherAttributes = $attributes }
                    New-ADOrganizationalUnit @ldsParamLight @newParam
                }
                'Delete' {
                    Unprotect-OrganizationalUnit @ldsParam -Identity $testItem.ADObject.ObjectGUID
                    Remove-ADOrganizationalUnit @ldsParam -Identity $testItem.ADObject.ObjectGUID -Recursive -Confirm:$false
                }
                'Update' {
                    $update = @{ }
                    foreach ($change in $testItem.Change) {
                        $update[$change.Property] = $change.New
                    }
                    Set-ADObject @ldsParam -Identity $testItem.ADObject.ObjectGUID -Replace $update
                }
            }
        }
    }
}

function Test-LdsOrganizationalUnit {
    <#
    .SYNOPSIS
        Tests, whether the desired organizational units exist.
     
    .DESCRIPTION
        Tests, whether the desired organizational units exist.
     
    .PARAMETER Server
        The LDS Server to target.
     
    .PARAMETER Partition
        The Partition on the LDS Server to target.
     
    .PARAMETER Credential
        Credentials to use for the operation.
     
    .PARAMETER Delete
        Undo everything defined in configuration.
        Allows rolling back after deployment.
     
    .EXAMPLE
        PS C:\> Test-LdsOrganizationalUnit -Server lds1.contoso.com -Partition 'DC=fabrikam,DC=org'
 
        Tests, whether the desired organizational units exist in 'DC=fabrikam,DC=org' on lds1.contoso.com
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)]
        [string]
        $Server,

        [Parameter(Mandatory = $true)]
        [string]
        $Partition,

        [PSCredential]
        $Credential,

        [switch]
        $Delete
    )
    
    begin {
        Update-LdsConfiguration -LdsServer $Server -LdsPartition $Partition
        $ldsParam = $PSBoundParameters | ConvertTo-PSFHashtable -Include Server, Partition, Credential
        $systemProperties = 'ObjectClass', 'Path', 'Name'
    }
    process {
        foreach ($configurationItem in $script:content.organizationalUnit.Values) {
            $path = 'OU={0},{1}' -f $configurationItem.Name, ($configurationItem.Path -replace '%DomainDN%',$Partition)
            if ($path -notmatch ',DC=') { $path = $path, $Partition -join ',' }

            $resultDefaults = @{
                Type = 'OrganizationalUnit'
                Identity = $path
                Configuration = $configurationItem
            }

            $failed = $null
            $adObject = $null
            try { $adObject = Get-ADOrganizationalUnit @ldsParam -Identity $path -Properties * -ErrorAction SilentlyContinue -ErrorVariable failed }
            catch { $failed = $_ }
            if ($failed -and $failed.CategoryInfo.Category -ne 'ObjectNotFound') {
                foreach ($failure in $failed) { Write-Error $failure }
                continue
            }

            #region Cases
            # Case: Does not Exist
            if (-not $adObject) {
                if ($Delete) { continue }

                New-TestResult @resultDefaults -Action Create
                continue
            }

            # Case: Exists
            $resultDefaults.ADObject = $adObject
            if ($Delete) {
                New-TestResult @resultDefaults -Action Delete
                continue
            }

            $changes = foreach ($pair in $configurationItem.GetEnumerator()) {
                if ($pair.Key -in $systemProperties) { continue }
                if ($pair.Value -ne $adObject.$($pair.Key)) {
                    New-Change -Identity $path -Property $pair.Key -OldValue $adObject.$($pair.Key) -NewValue $pair.Value
                }
            }

            if ($changes) {
                New-TestResult @resultDefaults -Action Update -Change $changes
            }
            #endregion Cases
        }
    }
}

function Invoke-LdsSchemaAttribute {
    <#
    .SYNOPSIS
        Applies the intended schema attributes.
     
    .DESCRIPTION
        Applies the intended schema attributes.
     
    .PARAMETER Server
        The LDS Server to target.
     
    .PARAMETER Partition
        The Partition on the LDS Server to target.
     
    .PARAMETER Credential
        Credentials to use for the operation.
     
    .PARAMETER TestResult
        Result objects of the associated Test-Command.
        Allows cherry-picking which change to apply.
        If not specified, it will a test and apply all test results instead.
     
    .EXAMPLE
        PS C:\> Invoke-LdsSchemaAttribute -Server lds1.contoso.com -Partition 'DC=fabrikam,DC=org'
 
        Applies the intended schema attributes to 'DC=fabrikam,DC=org' on lds1.contoso.com.
    #>

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

        [Parameter(Mandatory = $true)]
        [string]
        $Partition,

        [PSCredential]
        $Credential,

        [Parameter(ValueFromPipeline = $true)]
        $TestResult
    )

    begin {
        Update-LdsConfiguration -LdsServer $Server -LdsPartition $Partition
        $ldsParam = $PSBoundParameters | ConvertTo-PSFHashtable -Include Server, Partition, Credential
        $ldsParamLight = $ldsParam | ConvertTo-PSFHashtable -Exclude Partition
        $systemProperties = 'ObjectClass', 'AttributeID', 'IsDeleted', 'Optional', 'MayContain'

        $rootDSE = Get-ADRootDSE @ldsParamLight
    }
    process {
        if (-not $TestResult) {
            $TestResult = Test-LdsSchemaAttribute @ldsParam
        }
        $testResultsSorted = $TestResult | Sort-Object {
            switch ($_.Action) {
                Create { 1 }
                Delete { 2 }
                Update { 3 }
                Add { 4 }
                Remove { 5 }
                Default { 6 }
            }
        }

        foreach ($testItem in $testResultsSorted) {
            switch ($testItem.Action) {
                'Create' {
                    $attributes = $testItem.Configuration | ConvertTo-PSFHashtable -Exclude $systemProperties
                    $attributes.AttributeID = $testItem.Configuration.AttributeID
                    $name = $testItem.Configuration.Name
                    if (-not $name) { $name = $testItem.Configuration.AdminDisplayName }
                    New-ADObject @ldsParamLight -Type attributeSchema -Name $name -Path $rootDSE.schemaNamingContext -OtherAttributes $attributes
                }
                'Delete' {
                    $testItem.ADObject | Set-ADObject @ldsParamLight -Replace @{ IsDeleted = $true }
                }
                'Update' {
                    $replacements = @{ }
                    foreach ($change in $testItem.Change) {
                        $replacements[$change.Property] = $change.New
                    }
                    $testItem.ADObject | Set-ADObject @ldsParamLight -Replace $replacements
                }
                'Add' {
                    $testItem.Change.Data | Set-ADObject @ldsParamLight -Add @{
                        mayContain = $testItem.ADObject.lDAPDisplayName
                    }
                }
                'Remove' {
                    $testItem.Change.Data | Set-ADObject @ldsParamLight -Remove @{
                        mayContain = $testItem.Identity
                    }
                }
                'Rename' {
                    $testItem.ADObject | Rename-ADObject @ldsParamLight -NewName @($testItem.Change.New)[0]
                }
            }
        }
    }
}

function Test-LdsSchemaAttribute {
    <#
    .SYNOPSIS
        Tests, whether the intended schema attributes have been applied.
     
    .DESCRIPTION
        Tests, whether the intended schema attributes have been applied.
     
    .PARAMETER Server
        The LDS Server to target.
     
    .PARAMETER Partition
        The Partition on the LDS Server to target.
     
    .PARAMETER Credential
        Credentials to use for the operation.
     
    .EXAMPLE
        PS C:\> Test-LdsSchemaAttribute -Server lds1.contoso.com -Partition 'DC=fabrikam,DC=org'
 
        Tests, whether the intended schema attributes have been applied to 'DC=fabrikam,DC=org' on lds1.contoso.com
    #>

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

        [Parameter(Mandatory = $true)]
        [string]
        $Partition,

        [PSCredential]
        $Credential
    )

    begin {
        Update-LdsConfiguration -LdsServer $Server -LdsPartition $Partition
        $ldsParam = $PSBoundParameters | ConvertTo-PSFHashtable -Include Server, Partition, Credential
        $ldsParamLight = $ldsParam | ConvertTo-PSFHashtable -Exclude Partition
        $systemProperties = 'ObjectClass', 'AttributeID', 'IsDeleted', 'Optional', 'MayContain'

        $rootDSE = Get-ADRootDSE @ldsParamLight
        $classes = Get-ADObject @ldsParamLight -SearchBase $rootDSE.schemaNamingContext -LDAPFilter '(objectClass=classSchema)' -Properties mayContain, adminDisplayName
    }
    process {
        foreach ($schemaSetting in $script:content.SchemaAttribute.Values) {
            $schemaObject = $null
            $schemaObject = Get-ADObject @ldsParamLight -LDAPFilter "(attributeID=$($schemaSetting.AttributeID))" -SearchBase $rootDSE.schemaNamingContext -ErrorAction Ignore -Properties *
            $resultDefaults = @{
                Type          = 'SchemaAttribute'
                Identity      = $schemaSetting.AdminDisplayName
                Configuration = $schemaSetting
            }

            if (-not $schemaObject) {
                # If we already want to disable the attribute, no need to create it
                if ($schemaSetting.IsDeleted) { continue }
                if ($schemaSetting.Optional) { continue }

                New-TestResult @resultDefaults -Action Create
                foreach ($entry in $schemaSetting.MayContain) {
                    if ($classes.AdminDisplayName -notcontains $entry) { continue }
                    New-TestResult @resultDefaults -Action Add -Change @(
                        New-Change -Identity $schemaSetting.AdminDisplayName -Property MayContain -NewValue $entry -Data ($classes | Where-Object AdminDisplayName -EQ $entry)
                    )
                }
                continue
            }

            $resultDefaults.ADObject = $schemaObject

            if ($schemaSetting.IsDeleted -and -not $schemaObject.isDeleted) {
                New-TestResult @resultDefaults -Action Delete -Change @(
                    New-Change -Identity $schemaSetting.AdminDisplayName -Property IsDeleted -OldValue $false -NewValue $true
                )
            }

            if ($schemaSetting.Name -and $schemaSetting.Name -cne $schemaObject.Name) {
                New-TestResult @resultDefaults -Action Rename -Change @(
                    New-Change -Identity $schemaSetting.AdminDisplayName -Property Name -OldValue $schemaObject.Name -NewValue $schemaSetting.Name
                )
            }

            $changes = foreach ($pair in $schemaSetting.GetEnumerator()) {
                if ($pair.Key -in $systemProperties) { continue }
                if ($pair.Value -cne $schemaObject.$($pair.Key)) {
                    New-Change -Identity $schemaSetting.AdminDisplayName -Property $pair.Key -OldValue $schemaObject.$($pair.Key) -NewValue $pair.Value
                }
            }
            if ($changes) {
                New-TestResult @resultDefaults -Action Update -Change $changes
            }

            $mayBeContainedIn = $schemaSetting.MayContain
            if ($schemaSetting.IsDeleted) { $mayBeContainedIn = @() }

            $classesMatch = $classes | Where-Object mayContain -Contains $schemaObject.LdapDisplayName
            foreach ($matchingclass in $classesMatch) {
                if ($matchingclass.AdminDisplayName -in $mayBeContainedIn) { continue }
                New-TestResult @resultDefaults -Action Remove -Change @(
                    New-Change -Identity $schemaSetting.AdminDisplayName -Property MayContain -OldValue $matchingclass.AdminDisplayName -DisplayStyle RemoveValue -Data $matchingClass
                )
            }
            foreach ($allowedClass in $mayBeContainedIn) {
                if ($classesMatch.AdminDisplayName -contains $allowedClass) { continue }
                New-TestResult @resultDefaults -Action Add -Change @(
                    New-Change -Identity $schemaSetting.AdminDisplayName -Property MayContain -NewValue $allowedClass -Data ($classes | Where-Object AdminDisplayName -EQ $allowedClass)
                )
            }
        }
    }
}

function Invoke-LdsUser {
    <#
    .SYNOPSIS
        Creates the intended user objects.
     
    .DESCRIPTION
        Creates the intended user objects.
     
    .PARAMETER Server
        The LDS Server to target.
     
    .PARAMETER Partition
        The Partition on the LDS Server to target.
     
    .PARAMETER Credential
        Credentials to use for the operation.
     
    .PARAMETER Delete
        Undo everything defined in configuration.
        Allows rolling back after deployment.
     
    .PARAMETER TestResult
        Result objects of the associated Test-Command.
        Allows cherry-picking which change to apply.
        If not specified, it will a test and apply all test results instead.
     
    .EXAMPLE
        PS C:\> Invoke-LdsUser -Server lds1.contoso.com -Partition 'DC=fabrikam,DC=org'
 
        Creates the intended user objects for 'DC=fabrikam,DC=org' on lds1.contoso.com.
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)]
        [string]
        $Server,

        [Parameter(Mandatory = $true)]
        [string]
        $Partition,

        [PSCredential]
        $Credential,

        [switch]
        $Delete,

        [Parameter(ValueFromPipeline = $true)]
        $TestResult
    )
    
    begin {
        Update-LdsConfiguration -LdsServer $Server -LdsPartition $Partition
        $ldsParam = $PSBoundParameters | ConvertTo-PSFHashtable -Include Server, Partition, Credential
        $ldsParamLight = $ldsParam | ConvertTo-PSFHashtable -Exclude Partition
        $systemProperties = 'ObjectClass', 'Path', 'Name', 'Enabled'
    }
    process {
        if (-not $TestResult) {
            $TestResult = Test-LdsUser @ldsParam -Delete:$Delete
        }
        foreach ($testItem in $TestResult) {
            switch ($testItem.Action) {
                'Create' {
                    $attributes = $testItem.Configuration | ConvertTo-PSFHashtable -Exclude $systemProperties
                    $newParam = @{
                        Name = $testItem.Configuration.Name
                        Path = ($testItem.Identity -replace '^.+?,')
                        OtherAttributes = $attributes
                    }
                    if ($testItem.Configuration.Enabled) {
                        $newParam += @{
                            Enabled = $true
                            AccountPassword = New-Password -AsSecureString
                        }
                    }
                    if (0 -eq $newParam.OtherAttributes.Count) { $newParam.Remove('OtherAttributes') }
                    New-ADUser @ldsParamLight @newParam
                }
                'Delete' {
                    Remove-ADUser @ldsParam -Identity $testItem.ADObject.ObjectGUID -Recursive -Confirm:$false
                }
                'Update' {
                    $update = @{ }
                    foreach ($change in $testItem.Change) {
                        $update[$change.Property] = $change.New
                    }
                    Set-ADObject @ldsParam -Identity $testItem.ADObject.ObjectGUID -Replace $update
                }
            }
        }
    }
}

function Test-LdsUser {
    <#
    .SYNOPSIS
        Tests, whether the desired users have already been created.
     
    .DESCRIPTION
        Tests, whether the desired users have already been created.
     
    .PARAMETER Server
        The LDS Server to target.
     
    .PARAMETER Partition
        The Partition on the LDS Server to target.
     
    .PARAMETER Credential
        Credentials to use for the operation.
     
    .PARAMETER Delete
        Undo everything defined in configuration.
        Allows rolling back after deployment.
     
    .EXAMPLE
        PS C:\> Test-LdsUser -Server lds1.contoso.com -Partition 'DC=fabrikam,DC=org'
 
        Tests, whether the desired users have already been created for 'DC=fabrikam,DC=org' on lds1.contoso.com
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)]
        [string]
        $Server,

        [Parameter(Mandatory = $true)]
        [string]
        $Partition,

        [PSCredential]
        $Credential,

        [switch]
        $Delete
    )
    
    begin {
        Update-LdsConfiguration -LdsServer $Server -LdsPartition $Partition
        $ldsParam = $PSBoundParameters | ConvertTo-PSFHashtable -Include Server, Partition, Credential
        $systemProperties = 'ObjectClass', 'Path', 'Name', 'Enabled'
    }
    process {
        foreach ($configurationItem in $script:content.user.Values) {
            if ($configurationItem.SamAccountName -and -not $configurationItem.Name) {
                $configurationItem.Name = $configurationItem.SamAccountName
            }
            $path = 'CN={0},{1}' -f $configurationItem.Name, ($configurationItem.Path -replace '%DomainDN%',$Partition)
            if ($path -notmatch ',DC=') { $path = $path, $Partition -join ',' }

            $resultDefaults = @{
                Type = 'User'
                Identity = $path
                Configuration = $configurationItem
            }

            $failed = $null
            $adObject = $null
            try { $adObject = Get-ADUser @ldsParam -Identity $path -Properties * -ErrorAction SilentlyContinue -ErrorVariable failed }
            catch { $failed = $_ }
            if ($failed -and $failed.CategoryInfo.Category -ne 'ObjectNotFound') {
                foreach ($failure in $failed) { Write-Error $failure }
                continue
            }

            #region Cases
            # Case: Does not Exist
            if (-not $adObject) {
                if ($Delete) { continue }

                New-TestResult @resultDefaults -Action Create
                continue
            }

            # Case: Exists
            $resultDefaults.ADObject = $adObject
            if ($Delete) {
                New-TestResult @resultDefaults -Action Delete
                continue
            }

            $changes = foreach ($pair in $configurationItem.GetEnumerator()) {
                if ($pair.Key -in $systemProperties) { continue }
                if ($pair.Value -ne $adObject.$($pair.Key)) {
                    New-Change -Identity $path -Property $pair.Key -OldValue $adObject.$($pair.Key) -NewValue $pair.Value
                }
            }

            if ($changes) {
                New-TestResult @resultDefaults -Action Update -Change $changes
            }
            #endregion Cases
        }
    }
}

<#
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 'ADLDSMF' -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 'ADLDSMF' -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 'ADLDSMF' -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."

<#
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 'ADLDSMF.ScriptBlockName' -Scriptblock {
     
}
#>


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


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


$script:content = @{
    user               = @{ }
    group              = @{ }
    organizationalUnit = @{ }
    groupmembership    = @{ }
    accessrule         = @{ }
    SchemaAttribute    = @{ }
}

$script:adrights = @{
    'FullControl'    = @(
        [System.DirectoryServices.ActiveDirectoryRights]::GenericAll
    )
    'Enumerate'      = @(
        [System.DirectoryServices.ActiveDirectoryRights]::ListChildren
        [System.DirectoryServices.ActiveDirectoryRights]::ListObject
    )
    'Read'           = @(
        [System.DirectoryServices.ActiveDirectoryRights]::GenericRead
    )
    'EditObject'     = @(
        [System.DirectoryServices.ActiveDirectoryRights]::Delete
        [System.DirectoryServices.ActiveDirectoryRights]::ReadProperty
        [System.DirectoryServices.ActiveDirectoryRights]::WriteProperty
    )
    'ManageChildren' = @(
        [System.DirectoryServices.ActiveDirectoryRights]::CreateChild
        [System.DirectoryServices.ActiveDirectoryRights]::DeleteChild
    )
    'Extended'       = @(
        [System.DirectoryServices.ActiveDirectoryRights]::ExtendedRight
    )
}

# Make ACL work again
$null = Get-Acl -Path . -ErrorAction Ignore

# Disable AD Connection Check
Set-PSFConfig -FullName 'ADSec.Connect.NoAssertion' -Value $true

# Load config if present
if (Test-Path -Path "$script:ModuleRoot\Config") {
    Import-LdsConfiguration -Path "$script:ModuleRoot\Config"
}

New-PSFLicense -Product 'ADLDSMF' -Manufacturer 'frweinma' -ProductVersion $script:ModuleVersion -ProductType Module -Name MIT -Version "1.0.0.0" -Date (Get-Date "2023-12-11") -Text @"
Copyright (c) 2023 frweinma
 
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.
"@

#endregion Load compiled code