<#
Implementatation notes

Managing Disposable objects
    The types PrincipalContext, Principal, and DirectoryEntry are used througout the code
    and all are disposable.  However, in many cases, disposing the object immediately
    causes subsequent operations to fail or duplicate dispose calls to occur.
    To simplify management of these disposables, each public entry point defines a
    $disposables ArrayList variable and passes it to secondary functions that may
    need to create disposable objects.  The public entry point is then required
    to dispose the contents of the list in a finally block.

Managing PrincipalContext instances.
    To use the AccountManagement APIs to connect to the local machine or a domain,
    a PrincipalContext is needed.

    For the local groups and users, a PrincipalContext reflecting the current user
    can be created.

    For the default domain; the domain where the machine is joined, explicit credentials
    are needed since the default  user context is SYSTEM which has no rights to the
    domain.

    Additional PrincipalContext intsances may be needed when the machine is in a domain that is
    part of a multi-domain forest.  For example, Microsoft uses a multi-domain forest that
    includes domains such as ntdev, redmond, wingroup and a group may have members that
    span multiple domains.  Unless the enterprise implements the Global Catalog;
    something that Microsoft does not do; a unique PrincipalContext is needed to resolve
    accounts in each of the domains.

    To manage the use of PrincipalContext across domains, public entry points define
    a $principalContexts hashtable and pass it to support functions that need to
    resolve a group or group member.  Consumers of a PrincipalContext call
    GetPrincipalContext with a scope (domain name or machine name). GetPrincipalContext
    returns an existing hashtable entry or creates a new entry.  Note that a PrincipalContext
    to a target domain requires connecting to the domain, the hash table avoids subsequent
    connection calls.  Also note that GetPrincipalContext takes a Credential parameter for the
    case where a new PrincipalContext is needed. The implicit assumption is the credentials
    provided for the primary domain also has rights to resolve accounts in any of the other domains.

Resolving Group Members
    The original implementation assumed that group members could be resolved using the machine
    PrincipalContext or the logged on user.  In practice this is not reliable since
    the resource is typically run under the system account and this account is not guaranteed
    to have rights to resolve domain accounts. Additionally, the API's for enumerating group members
    does not provide a facility for passing additional credentials resulting in domain members failing
    to resolve.
    To address this, group members are enumerated by first converting the GroupPrincipal to a
    DirectoryEntry and enumerating its child members. The returned DirectoryEntry instances
    are then resolved to Principal objects using a PrincipalContext appropriate for the
    target domain.  See ResolveGroupMembersToPrincipals for details.

Handling Stale Group members
    A group may have stale members if the machine was moved from one domain to a another,
    foreign domain or when accounts are deleted (domain or local).
    At this point, members that were defined in the original domain or were deleted are
    now stale and cannot be resolved using Principal::FindByIdentity.  The original
    implementation failed at this point preventing any operations against the group.  The
    current implementation calls Write-Warning with the associated SID of the member that cannot
    be resolved then continues the operation.

#>

# A global variable that contains localized messages.
data LocalizedData
{
# culture="en-US"
ConvertFrom-StringData @'
GroupWithName=Group: {0}
RemoveOperation=Remove
AddOperation=Add
SetOperation=Set
GroupCreated=Group {0} created successfully.
GroupUpdated=Group {0} properties updated successfully.
GroupRemoved=Group {0} removed successfully.
NoConfigurationRequired=Group {0} exists on this node with the desired properties. No action required.
NoConfigurationRequiredGroupDoesNotExist=Group {0} does not exist on this node. No action required.
CouldNotFindPrincipal=Could not find a principal with the provided name [{0}]
MembersAndIncludeExcludeConflict=The {0} and {1} and/or {2} parameters conflict. The {0} parameter should not be used in any combination with the {1} and {2} parameters.
MembersIsNull=The Members parameter value is null. The {0} parameter must be provided if neither {1} nor {2} is provided.
MembersIsEmpty=The Members parameter is empty.  At least one group member must be provided.
MemberNotValid=The group member does not exist or cannot be resolved: {0}.
IncludeAndExcludeConflict=The principal {0} is included in both {1} and {2} parameter values. The same principal must not be included in both {1} and {2} parameter values.
IncludeAndExcludeAreEmpty=The MembersToInclude and MembersToExclude are either both null or empty.  At least one member must be specified in one of these parameters"
InvalidGroupName=The name {0} cannot be used. Names may not consist entirely of periods and/or spaces, or contain these characters: {1}
GroupExists=A group with the name {0} exists.
GroupDoesNotExist=A group with the name {0} does not exist.
PropertyMismatch=The value of the {0} property is expected to be {1} but it is {2}.
MembersNumberMismatch=Property {0}. The number of provided unique group members {1} is different from the number of actual group members {2}.
MembersMemberMismatch=At least one member {0} of the provided {1} parameter does not have a match in the existing group {2}.
MemberToExcludeMatch=At least one member {0} of the provided {1} parameter has a match in the existing group {2}.
ResolvingLocalAccount=Resolving {0} as a local account.
ResolvingDomainAccount=Resolving {0} in the {1} domain.
ResolvingDomainAccountWithTrust=Resolving {0} with domain trust.
DomainCredentialsRequired=Credentials are required to resolve the domain account {0}.
UnableToResolveAccount=Unable to resolve account '{0}'. Failed with message: {1} (error code={2})
'@
}

Import-Module "$PSScriptRoot\..\RunAsHelper.psm1"

Import-LocalizedData LocalizedData -FileName MSFT_GroupResource.strings.psd1

if (-not (IsNanoServer))
{
    Add-Type -AssemblyName 'System.DirectoryServices.AccountManagement'
}

<#
.Synopsis
The Get-TargetResource cmdlet.
#>
function Get-TargetResource
{
    param
    (
        [parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $GroupName,

        [System.Management.Automation.PSCredential]
        $Credential = $null
    )

    if (IsNanoServer)
    {
        return Get-TargetResourceOnNanoServer @PSBoundParameters
    }
    else
    {
        return Get-TargetResourceOnFullSKU @PSBoundParameters
    }

}

<#
.Synopsis
The Set-TargetResource cmdlet.
#>
function Set-TargetResource
{
    [CmdletBinding(SupportsShouldProcess=$true)]
    param
    (
        [parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $GroupName,

        [ValidateSet("Present", "Absent")]
        [System.String]
        $Ensure = "Present",

        [System.String]
        $Description,

        [System.String[]]
        $Members,

        [System.String[]]
        $MembersToInclude,

        [System.String[]]
        $MembersToExclude,

        [ValidateNotNullOrEmpty()]
        [System.Management.Automation.PSCredential]
        $Credential
    )

    if (IsNanoServer)
    {
        Set-TargetResourceOnNanoServer @PSBoundParameters
    }
    else
    {
        Set-TargetResourceOnFullSKU @PSBoundParameters
    }
}

<#
.Synopsis
The Test-TargetResource cmdlet is used to validate if the resource is in a state as expected in the instance document.
#>
function Test-TargetResource
{
    param
    (
        [parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $GroupName,

        [ValidateSet("Present", "Absent")]
        [System.String]
        $Ensure = "Present",

        [System.String]
        $Description,

        [System.String[]]
        $Members,

        [System.String[]]
        $MembersToInclude,

        [System.String[]]
        $MembersToExclude,

        [ValidateNotNullOrEmpty()]
        [System.Management.Automation.PSCredential]
        $Credential
    )

    if (IsNanoServer)
    {
        return Test-TargetResourceOnNanoServer @PSBoundParameters
    }
    else
    {
        return Test-TargetResourceOnFullSKU @PSBoundParameters
    }
}

<#
.Synopsis
The Get-TargetResource cmdlet for Full SKU images.
#>
function Get-TargetResourceOnFullSKU
{
    param
    (
        [parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $GroupName,

        [System.Management.Automation.PSCredential]
        $Credential = $null
    )

    Set-StrictMode -Version Latest

    ValidateGroupName -GroupName $GroupName

    # store disposable objects in a list for cleanup later.
    # This is needed for the case where utility functions need to create
    # disposable objects (Principal and PrincipalContext) and the
    # object's life time is longer than the function. The $disposables
    # collection is passed to these functions for storing the disposable objects
    # and this function disposes the contents prior to returning.
    # See references to DisposeAll for details.
    $disposables = New-Object System.Collections.ArrayList

    # hash table of scope to PrincipalContext. This is used for
    # cases where the group membership contains entries that span the machine
    # and one or more domains. The hashtable's key is the machine or domain
    # name (scope) while the value is the PrincipalContext to use to resolve accounts
    # within the named scope.
    $principalContexts = @{}

    try
    {
        [System.DirectoryServices.AccountManagement.GroupPrincipal] $group = GetGroup -groupName $GroupName -principalContexts $principalContexts -disposables $disposables
        if($group -ne $null)
        {
            $null = $disposables.Add($group)

            # The group is found. Enumerate all group members.
            $members = [String[]]@(EnumerateMembersOnFullSKU -Group $group -principalContexts $principalContexts -disposables $disposables -credential $Credential)

            # Return all group properties and Ensure="Present".
            $returnValue = @{
                                GroupName = $group.Name;
                                Ensure = "Present";
                                Description = $group.Description;
                                Members = [System.String[]] $members;
                            }

            return $returnValue
        }

        # The group is not found. Return Ensure=Absent.
        return @{
                    GroupName = $GroupName;
                    Ensure = "Absent";
                }
    }
    finally
    {
        DisposeAll $disposables
    }
}

<#
.Synopsis
The Set-TargetResource cmdlet for Full SKU images.
#>
function Set-TargetResourceOnFullSKU
{
    [CmdletBinding(SupportsShouldProcess=$true)]
    param
    (
        [parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $GroupName,

        [ValidateSet("Present", "Absent")]
        [System.String]
        $Ensure = "Present",

        [System.String]
        $Description,

        [System.String[]]
        $Members,

        [System.String[]]
        $MembersToInclude,

        [System.String[]]
        $MembersToExclude,

        [ValidateNotNullOrEmpty()]
        [System.Management.Automation.PSCredential]
        $Credential
    )

    Set-StrictMode -Version Latest

    ValidateGroupName -GroupName $GroupName

    # store disposable objects in a list for cleanup later.
    # This is needed for the case where utility functions need to create
    # disposable objects (Principal and PrincipalContext) and the
    # object's life time is longer than the function. The $disposables
    # collection is passed to these functions for storing the disposable objects
    # and this function disposes the contents prior to returning.
    # See references to DisposeAll for details.
    $disposables = New-Object System.Collections.ArrayList

    # hash table of scope to PrincipalContext. This is used for
    # cases where the group membership contains entries that span the machine
    # and one or more domains. The hashtable's key is the machine or domain
    # name (scope) while the value is the PrincipalContext to use to resolve accounts
    # within the named scope.
    $principalContexts = @{}

    try
    {
        # Try to find a group by its name.
        [System.DirectoryServices.AccountManagement.GroupPrincipal] $group = GetGroup -groupName $GroupName -principalContexts $principalContexts -disposables $disposables
        [bool] $groupExists = $false

        if ($group -ne $null)
        {
            $groupExists = $true
        }

        if($Ensure -eq "Present")
        {
            [System.DirectoryServices.AccountManagement.Principal[]] $membersToIncludePrincipals = $null
            [System.DirectoryServices.AccountManagement.Principal[]] $membersToExcludePrincipals = $null

            if ($group -ne $null)
            {
                $null = $disposables.Add($group)
            }

            # Ensure is set to "Present".

            [bool] $whatIfShouldProcess = $true
            [bool] $saveChanges = $false

            if(-not $groupExists)
            {
                # A group does not exist. Check WhatIf for adding a group.
                $whatIfShouldProcess = $pscmdlet.ShouldProcess(($LocalizedData.GroupWithName -f $GroupName), $LocalizedData.AddOperation)
            }
            else
            {
                # Check WhatIf for setting a group.
                $whatIfShouldProcess = $pscmdlet.ShouldProcess(($LocalizedData.GroupWithName -f $GroupName), $LocalizedData.SetOperation)
            }

            if($whatIfShouldProcess)
            {
                if(-not $groupExists)
                {
                    # NOTE: The PrincipalContext for the local machine is populated above in the call to GetGroup
                    $localPrincipalContext = $principalContexts[$env:COMPUTERNAME]

                    # The group with the provided name does not exist. Add a new group.
                    $group = New-Object System.DirectoryServices.AccountManagement.GroupPrincipal($localPrincipalContext)
                    $null = $disposables.Add($group)

                    $group.Name = $GroupName
                    $saveChanges = $true
                }

                # Set group properties.

                if($PSBoundParameters.ContainsKey('Description') -and ((-not $groupExists) -or ($Description -ne $group.Description)))
                {
                    $group.Description = $Description
                    $saveChanges = $true
                }

                # NOTE: Group members can be updated in two ways..
                # 1: Supplying the Members parameter - this causes the membership to be replaced with the members defined in Members.
                #    NOTE: If Members is empty, the group membership is cleared.
                # 2: Providing MembersToInclude and/or MembersToExclude - this adds/removes members from the list.
                #    If Members is mutually exclusive with MembersToInclude and MembersToExclude
                #    If Members is not defined then MembersToInclude or MembersToExclude must contain at least one entry.

                if($PSBoundParameters.ContainsKey('Members'))
                {
                    if($PSBoundParameters.ContainsKey('MembersToInclude') -or $PSBoundParameters.ContainsKey('MembersToExclude'))
                    {
                        # If Members are provided, Include and Exclude are not allowed.
                        ThrowInvalidArgumentError -ErrorId "GroupTestCmdlet_MembersPlusIncludeOrExcludeConflict" -ErrorMessage ($LocalizedData.MembersAndIncludeExcludeConflict -f "Members","MembersToInclude","MembersToExclude")
                    }

                    if($Members -eq $null)
                    {
                        ThrowInvalidArgumentError -ErrorId "GroupTestCmdlet_MembersIsNull" -ErrorMessage ($LocalizedData.MembersIsNull -f "Members","MembersToInclude","MembersToExclude")
                    }

                    if ($Members.Count -eq 0)
                    {
                        $group.Members.Clear()
                        $saveChanges = $true
                    }
                    else
                    {
                        # Remove duplicate names as strings.
                        $Members = [String[]]@(RemoveDuplicates -Members $Members)

                        # Resolve the names to actual principal objects.
                        [System.DirectoryServices.AccountManagement.Principal[]]$expectedPrincipals = ResolveNamesToPrincipals -principalContexts $principalContexts -Disposables $disposables -credential $Credential -ObjectNames $Members

                        if ($expectedPrincipals.Length -gt 0)
                        {
                            $group.Members.Clear()
                            # Set the contents of the group
                            if ((AddGroupMembers -Group $group -Principals $expectedPrincipals) -eq $true)
                            {
                                $saveChanges = $true
                            }
                        }
                        else
                        {
                            #ISSUE: Is an empty $Members parameter valid?
                            ThrowInvalidArgumentError -ErrorId "GroupSetCmdlet_MembersEmpty" -ErrorMessage ($LocalizedData.MembersIsEmpty)
                        }
                    }
                }
                else
                {
                    [System.DirectoryServices.AccountManagement.Principal[]] $membersToIncludePrincipals = $null
                    [System.DirectoryServices.AccountManagement.Principal[]] $membersToExcludePrincipals = $null

                    if($PSBoundParameters.ContainsKey('MembersToInclude'))
                    {
                        $MembersToInclude = [String[]]@(RemoveDuplicates -Members $MembersToInclude)

                        # Resolve the names to actual principal objects.
                        $membersToIncludePrincipals = ResolveNamesToPrincipals -principalContexts $principalContexts -Disposables $disposables -credential $Credential -ObjectNames $MembersToInclude
                    }

                    if($PSBoundParameters.ContainsKey('MembersToExclude'))
                    {
                        $MembersToExclude = [String[]]@(RemoveDuplicates -Members $MembersToExclude)

                        # Resolve the names to actual principal objects.
                        $membersToExcludePrincipals = ResolveNamesToPrincipals -principalContexts $principalContexts -Disposables $disposables -credential $Credential -ObjectNames $MembersToExclude
                    }

                    if($membersToIncludePrincipals -ne $null -and $membersToExcludePrincipals -ne $null)
                    {
                        # Both MembersToInclude and MembersToExlude were provided. Check if they have common principals.
                        foreach($includePrincipal in $membersToIncludePrincipals)
                        {
                            foreach($excludePrincipal in $membersToExcludePrincipals)
                            {
                                if($includePrincipal -eq $excludePrincipal)
                                {
                                    ThrowInvalidArgumentError -ErrorId "GroupSetCmdlet_IncludeAndExcludeConflict" -ErrorMessage ($LocalizedData.IncludeAndExcludeConflict -f $includePrincipal.SamAccountName,"MembersToInclude", "MembersToExclude")
                                }
                            }
                        }
                        if ($membersToIncludePrincipals.Length -eq 0 -and $membersToExcludePrincipals.Length -eq 0)
                        {
                            ThrowInvalidArgumentError -ErrorId "GroupSetCmdlet_EmptyIncludeAndExclude" -ErrorMessage ($LocalizedData.IncludeAndExcludeAreEmpty)
                        }
                    }


                    if ((RemoveGroupMembers -Group $group -Principals $membersToExcludePrincipals) -eq $true)
                    {
                        $saveChanges = $true
                    }

                    if ((AddGroupMembers -Group $group -Principals $membersToIncludePrincipals) -eq $true)
                    {
                        $saveChanges = $true
                    }
                }

                if($saveChanges)
                {
                    $group.Save()

                    # Send an operation success verbose message.
                    if($groupExists)
                    {
                        Write-Verbose -Message ($LocalizedData.GroupUpdated -f $GroupName)
                    }
                    else
                    {
                        Write-Verbose -Message ($LocalizedData.GroupCreated -f $GroupName)
                    }
                }
                else
                {
                    Write-Verbose -Message ($LocalizedData.NoConfigurationRequired -f $GroupName)
                }
            }
        }
        else
        {
            # Ensure is set to "Absent".
            if($groupExists -eq $true)
            {
                # The group exists.
                if($pscmdlet.ShouldProcess(($LocalizedData.GroupWithName -f $GroupName), $LocalizedData.RemoveOperation))
                {
                    # Remove the group by the provided name.
                    # NOTE: Don't add to $disposables since Delete also disposes.
                    $group.Delete()
                    Write-Verbose -Message ($LocalizedData.GroupRemoved -f $GroupName)
                }
                else
                {
                    $null = $disposables.Add($group)
                }
            }
            else
            {
                Write-Verbose -Message ($LocalizedData.NoConfigurationRequiredGroupDoesNotExist -f $GroupName)
            }
        }
    }
    finally
    {
        DisposeAll $disposables
    }
}

<#
.Synopsis
The Test-TargetResource cmdlet for Full SKU images is used to validate if the resource is in a state as expected in the instance document.
#>
function Test-TargetResourceOnFullSKU
{
    param
    (
        [parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $GroupName,

        [ValidateSet("Present", "Absent")]
        [System.String]
        $Ensure = "Present",

        [System.String]
        $Description,

        [System.String[]]
        $Members,

        [System.String[]]
        $MembersToInclude,

        [System.String[]]
        $MembersToExclude,

        [ValidateNotNullOrEmpty()]
        [System.Management.Automation.PSCredential]
        $Credential
    )

    Set-StrictMode -Version Latest

    ValidateGroupName -GroupName $GroupName

    # store disposable objects in a list for cleanup later.
    # This is needed for the case where utility functions need to create
    # disposable objects (Principal and PrincipalContext) and the
    # object's life time is longer than the function. The $disposables
    # collection is passed to these functions for storing the disposable objects
    # and this function disposes the contents prior to returning.
    # See references to DisposeAll for details.
    $disposables = New-Object System.Collections.ArrayList

    # hash table of scope to PrincipalContext. This is used for
    # cases where the group membership contains entries that span the machine
    # and one or more domains. The hashtable's key is the machine or domain
    # name (scope) while the value is the PrincipalContext to use to resolve accounts
    # within the named scope.
    $principalContexts = @{}

    try
    {
        [System.DirectoryServices.AccountManagement.GroupPrincipal] $group = GetGroup -groupName $GroupName -principalContexts $principalContexts -disposables  $disposables
        if($group -eq $null)
        {
            # A group with the provided name does not exist.
            Write-Log -Message ($LocalizedData.GroupDoesNotExist -f $GroupName)

            if($Ensure -eq "Absent")
            {
                return $true
            }
            else
            {
                return $false
            }
        }
        $null = $disposables.Add($group)

        # A group with the provided name exists.
        Write-Log -Message ($LocalizedData.GroupExists -f $GroupName)

        # Validate separate properties.
        if($Ensure -eq "Absent")
        {
            Write-Log -Message ($LocalizedData.PropertyMismatch -f "Ensure", "Absent", "Present")
            return $false # The Ensure property does not match. Return $false
        }

        if($PSBoundParameters.ContainsKey('GroupName') -and $GroupName -ne $group.SamAccountName -and $GroupName -ne $group.Sid.Value)
        {
            return $false # The Name property does not match. Return $false
        }

        if($PSBoundParameters.ContainsKey('Description') -and $Description -ne $group.Description)
        {
            Write-Log -Message ($LocalizedData.PropertyMismatch -f "Description", $Description, $group.Description)
            return $false # The Description property does not match. Return $false
        }

        if($PSBoundParameters.ContainsKey('Members'))
        {
            if($PSBoundParameters.ContainsKey('MembersToInclude') -or $PSBoundParameters.ContainsKey('MembersToExclude'))
            {
                # If Members are provided, Include and Exclude are not allowed.
                ThrowInvalidArgumentError -ErrorId "GroupTestCmdlet_MembersPlusIncludeOrExcludeConflict" -ErrorMessage ($LocalizedData.MembersAndIncludeExcludeConflict -f "Members","MembersToInclude","MembersToExclude")
            }

            if($Members -eq $null)
            {
                ThrowInvalidArgumentError -ErrorId "GroupTestCmdlet_MembersIsNull" -ErrorMessage ($LocalizedData.MembersIsNull -f "Members","MembersToInclude","MembersToExclude")
            }

            if ($Members.Count -eq 0)
            {
                if ($group.Members.Count -eq 0)
                {
                    return $true
                }
                else
                {
                    return $false
                }
            }
            else
            {
                # Remove duplicate names as strings.
                $Members = [String[]]@(RemoveDuplicates -Members $Members)

                # Resolve the names to actual principal objects.
                [System.DirectoryServices.AccountManagement.Principal[]] $expectedMembers = ResolveNamesToPrincipals -principalContexts $principalContexts -Disposables $disposables -credential $Credential -ObjectNames $Members

                if($expectedMembers.Length -ne $group.Members.Count)
                {
                    Write-Log -Message ($LocalizedData.MembersNumberMismatch -f "Members", $expectedMembers.Length, $group.Members.Count)
                    return $false; # The number of provided unique group members is different from the number of actual group members. Return $false.
                }

                [System.DirectoryServices.AccountManagement.Principal[]] $actualMembers = ResolveGroupMembersToPrincipals -group $group -principalContexts $principalContexts -disposables $disposables -credential $Credential

                # Compare two members lists.
                foreach ($expectedMember in $expectedMembers)
                {
                    $matchFound = $false

                    foreach($groupMember in $actualMembers)
                    {
                        if($expectedMember -eq $groupMember)
                        {
                            $matchFound = $true
                            break;
                        }
                    }

                    if(!$matchFound)
                    {
                        Write-Log -Message ($LocalizedData.MembersMemberMismatch -f $expectedMember.SamAccountName, "Members", $group.SamAccountName)
                        return $false # At least one element does not have a match. Return $false
                    }
                }
            }
        }
        else
        {
            [System.DirectoryServices.AccountManagement.Principal[]] $actualMembers = ResolveGroupMembersToPrincipals -group $group -principalContexts $principalContexts -disposables $disposables -credential $Credential

            if($PSBoundParameters.ContainsKey('MembersToInclude'))
            {
                $MembersToInclude = [String[]]@(RemoveDuplicates -Members $MembersToInclude)

                # Resolve the names to actual principal objects.
                [System.DirectoryServices.AccountManagement.Principal[]] $membersToIncludePrincipals = ResolveNamesToPrincipals -principalContexts $principalContexts -Disposables $disposables -credential $Credential -ObjectNames $MembersToInclude

                # Check if every element in $membersToIncludePrincipals has a match in $group.Members.
                # Compare two members lists.
                foreach($expectedMember in $membersToIncludePrincipals)
                {
                    $matchFound = $false

                    foreach($groupMember in $actualMembers)
                    {
                        if($expectedMember -eq $groupMember)
                        {
                            $matchFound = $true
                            break
                        }
                    }

                    if(!$matchFound)
                    {
                        Write-Log -Message ($LocalizedData.MembersMemberMismatch -f $expectedMember.SamAccountName, "MembersToInclude", $group.SamAccountName)
                        return $false # At least one element from $MembersToInclude does not have a match. Return $false
                    }
                }
            }

            if($PSBoundParameters.ContainsKey('MembersToExclude'))
            {
                $MembersToExclude = [String[]]@(RemoveDuplicates -Members $MembersToExclude);

                # Resolve the names to actual principal objects.
                [System.DirectoryServices.AccountManagement.Principal[]] $membersToExcludePrincipals = ResolveNamesToPrincipals -principalContexts $principalContexts -Disposables $disposables -credential $Credential -ObjectNames $MembersToExclude

                foreach($expectedMember in $membersToExcludePrincipals)
                {
                    foreach($groupMember in $actualMembers)
                    {
                        if($expectedMember -eq $groupMember)
                        {
                            Write-Log -Message ($LocalizedData.MemberToExcludeMatch -f $expectedMember.SamAccountName, "MembersToExclude", $group.SamAccountName)
                            return $false  # At least one element from $MembersToExclude has a match. Return $false
                        }
                    }
                }
            }
        }
    }
    finally
    {
        DisposeAll $disposables
    }

    # All properties match. Return $true.
    return $true;
}

<#
.Synopsis
The Get-TargetResource cmdlet for Nano Server images.
#>
function Get-TargetResourceOnNanoServer
{
    param
    (
        [parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $GroupName,

        [System.Management.Automation.PSCredential]
        $Credential = $null
    )

    Set-StrictMode -Version Latest

    ValidateGroupName -GroupName $GroupName

    try
    {
        [Microsoft.PowerShell.Commands.LocalGroup] $group = Get-LocalGroup -Name $GroupName -ErrorAction Stop
    }
    catch [System.Exception]
    {
        if ($_.CategoryInfo.Reason -eq 'GroupNotFoundException')
        {
            # The group is not found. Return Ensure=Absent.
            return @{
                        GroupName = $GroupName;
                        Ensure = "Absent";
                    }
        }
        Throw-TerminatingError -ErrorRecord $_
    }

    # The group is found. Enumerate all group members.
    $members = [String[]](EnumerateMembersOnNanoServer -Group $group)

    # Return all group properties and Ensure="Present".
    $returnValue = @{
                        GroupName = $group.Name;
                        Ensure = "Present";
                        Description = $group.Description;
                        Members = [System.String[]] $members;
                    }
    
    return $returnValue
}

<#
.Synopsis
The Set-TargetResource cmdlet for Nano Server images.
#>
function Set-TargetResourceOnNanoServer
{
    [CmdletBinding(SupportsShouldProcess=$true)]
    param
    (
        [parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $GroupName,

        [ValidateSet("Present", "Absent")]
        [System.String]
        $Ensure = "Present",

        [System.String]
        $Description,

        [System.String[]]
        $Members,

        [System.String[]]
        $MembersToInclude,

        [System.String[]]
        $MembersToExclude,

        [ValidateNotNullOrEmpty()]
        [System.Management.Automation.PSCredential]
        $Credential
    )

    Set-StrictMode -Version Latest

    ValidateGroupName -GroupName $GroupName
    
    # Try to find a group by its name.
    [bool] $groupExists = $false
    try
    {
        [Microsoft.PowerShell.Commands.LocalGroup] $group = Get-LocalGroup -Name $GroupName -ErrorAction Stop
        $groupExists = $true
    }
    catch [System.Exception]
    {
        if ($_.CategoryInfo.Reason -eq 'GroupNotFoundException')
        {
            # A group with the provided name does not exist.
            Write-Log -Message ($LocalizedData.GroupDoesNotExist -f $GroupName)
        }
        else
        {
            Throw-TerminatingError -ErrorRecord $_
        }
    }

    if($Ensure -eq "Present")
    {
        # Ensure is set to "Present".
        if(-not $groupExists)
        {
            # The group with the provided name does not exist. Add a new group.
            New-LocalGroup -Name $GroupName
            Write-Verbose -Message ($LocalizedData.GroupCreated -f $GroupName)
        }
    
        # Set group properties.
        
        if($PSBoundParameters.ContainsKey('Description') -and ((-not $groupExists) -or ($Description -ne $group.Description)))
        {
            Set-LocalGroup -Name $GroupName -Description $Description
        }
        
        # NOTE: Group members can be updated in two ways..
        # 1: Supplying the Members parameter - this causes the membership to be replaced with the members defined in Members.
        #    NOTE: If Members is empty, the group membership is cleared.
        # 2: Providing MembersToInclude and/or MembersToExclude - this adds/removes members from the list.
        #    If Members is mutually exclusive with MembersToInclude and MembersToExclude
        #    If Members is not defined then MembersToInclude or MembersToExclude must contain at least one entry.
        
        if($PSBoundParameters.ContainsKey('Members'))
        {
            if($PSBoundParameters.ContainsKey('MembersToInclude') -or $PSBoundParameters.ContainsKey('MembersToExclude'))
            {
                # If Members are provided, Include and Exclude are not allowed.
                ThrowInvalidArgumentError -ErrorId "GroupTestCmdlet_MembersPlusIncludeOrExcludeConflict" -ErrorMessage ($LocalizedData.MembersAndIncludeExcludeConflict -f "Members","MembersToInclude","MembersToExclude")
            }
        
            if($Members -eq $null)
            {
                ThrowInvalidArgumentError -ErrorId "GroupTestCmdlet_MembersIsNull" -ErrorMessage ($LocalizedData.MembersIsNull -f "Members","MembersToInclude","MembersToExclude")
            }
        
            # Remove duplicate names as strings.
            $ExpectedMembers = [String[]]@(RemoveDuplicates -Members $Members)
        
            if ($ExpectedMembers.Length -gt 0)
            {
                # Get current members
                $CurrentMembers = EnumerateMembersOnNanoServer -Group $group

                # Remove the current members of the group
                Remove-LocalGroupMember -Group $GroupName -Member $CurrentMembers

                # Add the list of expected members to the group
                Add-LocalGroupMember -Group $GroupName -Member $ExpectedMembers
            }
            else
            {
                ThrowInvalidArgumentError -ErrorId "GroupSetCmdlet_MembersEmpty" -ErrorMessage ($LocalizedData.MembersIsEmpty)
            }
        }
        else
        {
            if($PSBoundParameters.ContainsKey('MembersToInclude'))
            {
                $MembersToInclude = [String[]]@(RemoveDuplicates -Members $MembersToInclude)
            }
       
            if($PSBoundParameters.ContainsKey('MembersToExclude'))
            {
                $MembersToExclude = [String[]]@(RemoveDuplicates -Members $MembersToExclude)
            }
       
            if($PSBoundParameters.ContainsKey('MembersToInclude') -and $PSBoundParameters.ContainsKey('MembersToExclude'))
            {
                # Both MembersToInclude and MembersToExlude were provided. Check if they have common principals.
                foreach($includeMember in $MembersToInclude)
                {
                    foreach($excludeMember in $MembersToExclude)
                    {
                        if($includeMember -eq $excludeMember)
                        {
                            ThrowInvalidArgumentError -ErrorId "GroupSetCmdlet_IncludeAndExcludeConflict" -ErrorMessage ($LocalizedData.IncludeAndExcludeConflict -f $includeMember ,"MembersToInclude", "MembersToExclude")
                        }
                    }
                }
                if ($MembersToInclude.Length -eq 0 -and $MembersToExclude.Length -eq 0)
                {
                    ThrowInvalidArgumentError -ErrorId "GroupSetCmdlet_EmptyIncludeAndExclude" -ErrorMessage ($LocalizedData.IncludeAndExcludeAreEmpty)
                }
            }
            
            if($PSBoundParameters.ContainsKey('MembersToInclude'))
            {
                foreach($includeMember in $MembersToInclude)
                {
                    try
                    {
                        Add-LocalGroupMember -Group $GroupName -Member $includeMember -ErrorAction Stop
                    }
                    catch [System.Exception]
                    {
                        if ($_.CategoryInfo.Reason -ne 'MemberExistsException')
                        {
                            throw $_.Exception
                        }
                    }
                }
            }
       
            if($PSBoundParameters.ContainsKey('MembersToExclude'))
            {
                foreach($excludeMember in $MembersToExclude)
                {
                    try
                    {
                        Remove-LocalGroupMember -Group $GroupName -Member $excludeMember -ErrorAction Stop
                    }
                    catch [System.Exception]
                    {
                        if ($_.CategoryInfo.Reason -ne 'MemberNotFoundException')
                        {
                            Throw-TerminatingError -ErrorRecord $_
                        }
                    }
                }
            }
        }
    }
    else
    {
        # Ensure is set to "Absent".
        if($groupExists -eq $true)
        {
            # The group exists. Remove the group by the provided name.
            Remove-LocalGroup -Name $GroupName
            Write-Verbose -Message ($LocalizedData.GroupRemoved -f $GroupName)
        }
        else
        {
            Write-Verbose -Message ($LocalizedData.NoConfigurationRequiredGroupDoesNotExist -f $GroupName)
        }
    }
}

<#
.Synopsis
The Test-TargetResource cmdlet for Nano Server images is used to validate if the resource is in a state as expected in the instance document.
#>
function Test-TargetResourceOnNanoServer
{
    param
    (
        [parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $GroupName,

        [ValidateSet("Present", "Absent")]
        [System.String]
        $Ensure = "Present",

        [System.String]
        $Description,

        [System.String[]]
        $Members,

        [System.String[]]
        $MembersToInclude,

        [System.String[]]
        $MembersToExclude,

        [ValidateNotNullOrEmpty()]
        [System.Management.Automation.PSCredential]
        $Credential
    )

    Set-StrictMode -Version Latest

    ValidateGroupName -GroupName $GroupName

    try
    {
        [Microsoft.PowerShell.Commands.LocalGroup] $group = Get-LocalGroup -Name $GroupName -ErrorAction Stop
    }
    catch [System.Exception]
    {
        if ($_.CategoryInfo.Reason -eq 'GroupNotFoundException')
        {
            # A group with the provided name does not exist.
            Write-Log -Message ($LocalizedData.GroupDoesNotExist -f $GroupName)
        
            if($Ensure -eq "Absent")
            {
                return $true
            }
            else
            {
                return $false
            }
        }
        Throw-TerminatingError -ErrorRecord $_
    }

    # A group with the provided name exists.
    Write-Log -Message ($LocalizedData.GroupExists -f $GroupName)

    # Validate separate properties.
    if($Ensure -eq "Absent")
    {
        Write-Log -Message ($LocalizedData.PropertyMismatch -f "Ensure", "Absent", "Present")
        return $false # The Ensure property does not match. Return $false
    }
    
    if($PSBoundParameters.ContainsKey('Description') -and $Description -ne $group.Description)
    {
        Write-Log -Message ($LocalizedData.PropertyMismatch -f "Description", $Description, $group.Description)
        return $false # The Description property does not match. Return $false
    }
    
    if($PSBoundParameters.ContainsKey('Members'))
    {
        Write-Verbose "Testing members..."
        if($PSBoundParameters.ContainsKey('MembersToInclude') -or $PSBoundParameters.ContainsKey('MembersToExclude'))
        {
            # If Members are provided, Include and Exclude are not allowed.
            ThrowInvalidArgumentError -ErrorId "GroupTestCmdlet_MembersPlusIncludeOrExcludeConflict" -ErrorMessage ($LocalizedData.MembersAndIncludeExcludeConflict -f "Members","MembersToInclude","MembersToExclude")
        }
    
        if($Members -eq $null)
        {
            ThrowInvalidArgumentError -ErrorId "GroupTestCmdlet_MembersIsNull" -ErrorMessage ($LocalizedData.MembersIsNull -f "Members","MembersToInclude","MembersToExclude")
        }
    
        # Remove duplicate names as strings.
        $ExpectedMembers = [String[]]@(RemoveDuplicates -Members $Members)

        # Get current members
        $CurrentMembers = EnumerateMembersOnNanoServer -Group $group
        
        if($ExpectedMembers.Length -ne $CurrentMembers.Length)
        {
            Write-Log -Message ($LocalizedData.MembersNumberMismatch -f "Members", $ExpectedMembers.Length, $CurrentMembers.Length)
            return $false; # The number of provided unique group members is different from the number of actual group members. Return $false.
        }
    
        # Compare two members lists.
        foreach ($ExpectedMember in $ExpectedMembers)
        {
            $matchFound = $false
        
            foreach($groupMember in $CurrentMembers)
            {
                if($ExpectedMember -eq $groupMember)
                {
                    $matchFound = $true
                    break;
                }
            }
        
            if(-not $matchFound)
            {
                Write-Log -Message ($LocalizedData.MembersMemberMismatch -f $expectedMember, "Members", $group.Name)
                return $false # At least one element does not have a match. Return $false
            }
        }
    }
    else
    {
        # Get current members
        $CurrentMembers = EnumerateMembersOnNanoServer -Group $group

        if($PSBoundParameters.ContainsKey('MembersToInclude'))
        {
            $MembersToInclude = [String[]]@(RemoveDuplicates -Members $MembersToInclude)
    
            # Check if every element in $membersToIncludePrincipals has a match in $group.Members.
            # Compare two members lists.
            foreach($expectedMember in $MembersToInclude)
            {
                $matchFound = $false
    
                foreach($groupMember in $CurrentMembers)
                {
                    if($expectedMember -eq $groupMember)
                    {
                        $matchFound = $true
                        break
                    }
                }
    
                if(-not $matchFound)
                {
                    Write-Log -Message ($LocalizedData.MembersMemberMismatch -f $expectedMember, "MembersToInclude", $group.Name)
                    return $false # At least one element from $MembersToInclude does not have a match. Return $false
                }
            }
        }
    
        if($PSBoundParameters.ContainsKey('MembersToExclude'))
        {
            $MembersToExclude = [String[]]@(RemoveDuplicates -Members $MembersToExclude);
    
            foreach($expectedMember in $MembersToExclude)
            {
                foreach($groupMember in $CurrentMembers)
                {
                    if($expectedMember -eq $groupMember)
                    {
                        Write-Log -Message ($LocalizedData.MemberToExcludeMatch -f $expectedMember, "MembersToExclude", $group.Name)
                        return $false  # At least one element from $MembersToExclude has a match. Return $false
                    }
                }
            }
        }
    }

    # All properties match. Return $true.
    return $true;
}

function RemoveDuplicates
{
    param
    (
        [System.String[]] $Members
    )

    Set-StrictMode -Version Latest

    $destIndex = 0;
    for([int] $sourceIndex = 0 ; $sourceIndex -lt $Members.Count; $sourceIndex++)
    {
        $matchFound = $false
        for([int] $matchIndex = 0; $matchIndex -lt $destIndex; $matchIndex++)
        {
            if($Members[$sourceIndex] -eq $Members[$matchIndex])
            {
                # A duplicate is found. Discard the duplicate.
                $matchFound = $true
                continue
            }
        }

        if(!$matchFound)
        {
            $Members[$destIndex++] = $Members[$sourceIndex].ToLowerInvariant();
        }
    }

    # Create the output array.
    $destination = New-Object System.String[] -ArgumentList $destIndex

    # Copy only distinct elements from the original array to the destination array.
    [System.Array]::Copy($Members, $destination, $destIndex);

    if ($destIndex -gt 0)
    {
        return $destination
    }
    return [System.String[]]@()
}

function EnumerateMembersOnNanoServer
{
    [OutputType([System.String[]])]
    param
    (
        [parameter(Mandatory = $true)]
        [ValidateNotNull()]
        [Microsoft.PowerShell.Commands.LocalGroup]
        $Group
    )

    Set-StrictMode -Version Latest
    [System.Collections.ArrayList] $members = New-Object System.Collections.ArrayList

    # Get the group members.
    $groupmembers = Get-LocalGroupMember -Group $Group

    foreach($member in $groupmembers)
    {
        if ($member.PrincipalSource -eq "Local")
        {
            $null = $members.Add($member.Name.Substring($member.Name.IndexOf("\")+1))
        }
        else
        {
            Write-Verbose "$($member.Name) is not a local user (PrincipalSource = $($member.PrincipalSource))"
        }
    }

    if ($members.Count -gt 0)
    {
        return $members.ToArray()
    }
    return ,([System.String[]]@())
}

function EnumerateMembersOnFullSKU
{
    [OutputType([System.String[]])]
    param
    (
        [parameter(Mandatory = $true)]
        [ValidateNotNull()]
        [System.DirectoryServices.AccountManagement.GroupPrincipal]
        $group,

        [Parameter(Mandatory = $true)]
        [ValidateNotNull()]
        $principalContexts,

        [Parameter(Mandatory = $true)]
        [ValidateNotNull()]
        [System.Collections.ArrayList]
        $disposables,

        [System.Net.NetworkCredential]
        $credential = $null
    )

    Set-StrictMode -Version Latest
    [System.Collections.ArrayList] $members = New-Object System.Collections.ArrayList

    # Get the group members as Principal objects.
    [System.DirectoryServices.AccountManagement.Principal[]] $principals = ResolveGroupMembersToPrincipals -group $group -principalContexts $principalContexts -disposables  $disposables -credential $credential

    foreach($principal in $principals)
    {
        if($principal.ContextType -eq [System.DirectoryServices.AccountManagement.ContextType]::Domain)
        {
            # Select only the first part of the full domain name.
            [String]$domainName = $principal.Context.Name;
            [int] $separatorIndex = $domainName.IndexOf('.')
            if ($separatorIndex -ne -1)
            {
                $domainName = $domainName.Substring(0, $separatorIndex)
            }

            if($principal.StructuralObjectClass -eq "computer")
            {
                $null = $members.Add($domainName+'\'+$principal.Name)
            }
            else
            {
                $null = $members.Add($domainName+'\'+$principal.SamAccountName)
            }
        }
        else
        {
            $null = $members.Add($principal.Name)
        }
    }

    return $members.ToArray()
}

<#
.Synopsis
    Resolves the members of a group to Principal instances.
#>
function ResolveGroupMembersToPrincipals
{
    [OutputType([System.DirectoryServices.AccountManagement.Principal[]])]
    param
    (
        [parameter(Mandatory = $true)]
        [ValidateNotNull()]
        [System.DirectoryServices.AccountManagement.GroupPrincipal]
        $group,

        [Parameter(Mandatory = $true)]
        [ValidateNotNull()]
        $principalContexts,

        [Parameter(Mandatory = $true)]
        [ValidateNotNull()]
        [System.Collections.ArrayList]
        $disposables,

        [System.Net.NetworkCredential]
        $credential = $null
    )
    Set-StrictMode -Version latest

    [System.Collections.ArrayList] $principals = New-Object System.Collections.ArrayList

    # NOTE: This logic enumerates the group members using the underlying DirectoryEntry API.
    # The reason this is needed is due to the fact that enumerating the group
    # members as principal instances causes a resolve to occur. Since there is no
    # facility for passing credentials to perform the resolution, any members that
    # cannot be resolved using the current user will fail; such as when the DSC
    # Group resource runs as system.  Dropping down to the underyling API
    # allows us to access the account's SID which can then be used to
    # resolve the associated principal using explicit credentials.
    [System.DirectoryServices.DirectoryEntry] $groupDe = $group.GetUnderlyingObject()

    $enum = $groupDe.Invoke("Members")
    foreach ($item in $enum)
    {
        [string] $scope = $null
        [string] $accountName = $null
        [string] $machineName = $env:COMPUTERNAME
        [System.DirectoryServices.AccountManagement.Principal] $principal = $null

        # extract the objectSid from the underlying DirectoryEntry
        [System.DirectoryServices.DirectoryEntry] $entry = New-Object System.DirectoryServices.DirectoryEntry($item)
        [byte[]] $sidBytes = $entry.Properties["objectSid"].Value

        $null = $disposables.Add($entry)

        [string[]] $parts = $entry.Path.Split("/")

        if ($parts.Count -eq 4)
        {
            # parsing WinNT://domainname/accountname
            # or WinNT://machinename/accountname
            $scope = $parts[2]
            $accountName = $parts[3]
        }
        elseif ($parts.Count -eq 5)
        {
            # parsing WinNT://domainname/machinename/accountname
            $scope = $parts[3]
            $accountName = $parts[4]
        }
        else
        {
            # the account is stale either becuase it was deleted or
            # the machine was moved to a new domain without removing
            # the domain members from the group.
            # If we consider this a fatal error, the group is no longer
            # managable by the DSC resource.  Writing a warning allows
            # the operation to complete while leaving the stale member in the
            # group.
            Write-Warning -Message ($LocalizedData.MemberNotValid -f  $entry.Path)
            continue
        }


        [bool] $isLocalMachine = [System.String]::CompareOrdinal($scope, $machineName) -eq 0

        $principalContext = GetPrincipalContext -principalContexts $principalContexts -disposables $disposables -scope $scope -credential $credential

        # if local machine qualified, get the PrincipalContext for the local machine
        if ($isLocalMachine -eq $true)
        {
            Write-Verbose -Message ($LocalizedData.ResolvingLocalAccount -f $accountName)
        }
        # the account is domain qualified - credentials required to resolve it.
        elseif ($credential -ne $null  -or $principalContext -ne $null)
        {
            Write-Verbose -Message ($LocalizedData.ResolvingDomainAccount -f  $scope, $accountName)
        }
        else
        {
            # The provided name is not scoped to the local machine and no credentials were provided.
            # This is an unsupported use case; credentials are required to resolve off-box.
            ThrowInvalidArgumentError -ErrorId "PrincipalNotFoundNoCredential" -ErrorMessage ($LocalizedData.DomainCredentialsRequired -f $accountName)
        }

        # create a sid to enable comparison againt the expected member's sid.
        [System.Security.Principal.SecurityIdentifier] $sid = New-Object System.Security.Principal.SecurityIdentifier($sidBytes, 0)

        $principal = ResolveSidToPrincipal -principalContext $principalContext -sid $sid -isLocalMachineQualified $isLocalMachine
        $null = $principals.Add($principal)
        $null = $disposables.Add($principal)
    }

    return $principals.ToArray()
}

<#
.Synopsis
    Resolves an array of object names to Principal instances.
#>
function ResolveNamesToPrincipals
{
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNull()]
        [String[]] $objectNames,

        [Parameter(Mandatory = $true)]
        [ValidateNotNull()]
        [System.Collections.ArrayList] $disposables,

        [Parameter(Mandatory = $true)]
        [ValidateNotNull()]
        $principalContexts,

        [System.Net.NetworkCredential]
        $credential = $null
    )
    Set-StrictMode -Version Latest

    [System.Collections.ArrayList] $principals = New-Object System.Collections.ArrayList
    $keys = @{}

    foreach($objectName in $objectNames)
    {
        $principal = ResolveNameToPrincipal -principalContexts $principalContexts -credential $credential -disposables $disposables -objectName $objectName
        if ($principal -ne $null)
        {
            [string] $key = $null
            # handle duplicate entries
            if ($principal.ContextType -eq [System.DirectoryServices.AccountManagement.ContextType]::Domain)
            {
                $key = $principal.DistinguishedName
            }
            else
            {
                $key = $principal.SamAccountName
            }
            if ($keys.ContainsKey($key) -eq $false)
            {
                $keys.Add($key, $null)
                $null = $principals.Add($principal)
            }
        }
    }

    $keys.Clear()
    return $principals.ToArray()
}

<#
.Synopsis
    resolves an object name to a Principal
#>
function ResolveNameToPrincipal
{
    [OutputType([System.DirectoryServices.AccountManagement.Principal])]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNull()]
        $principalContexts,

        [Parameter(Mandatory = $true)]
        [ValidateNotNull()]
        $disposables,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string] $objectName,

        [System.Net.NetworkCredential]
        $credential = $null

    )
    Set-StrictMode -Version Latest

    [string] $accountName = $null

    # the scope of the the object name when in the form of scope\name, UPN, or DN
    [string] $scope = Parse-Scope -fullName $objectName -accountName ([ref] $accountName)

    # check for an object qualified to the local machine
    [bool] $isLocalMachine = IsLocalMachine $scope

    [System.DirectoryServices.AccountManagement.PrincipalContext] $principalContext = $null
    [bool] $UseDomainTrust = $false

    # if local machine qualified, get the PrincipalContext for the local machine
    if ($isLocalMachine -eq $true)
    {
        Write-Verbose -Message ($LocalizedData.ResolvingLocalAccount -f $objectName)
    }
    # the account is domain qualified - credentials provided to resolve it.
    elseif ($credential -ne $null)
    {
        Write-Verbose -Message ($LocalizedData.ResolvingDomainAccount -f  $ObjectName, $scope)
    }
    # no credentials provided to resolve account name, so try with domain trust
    else
    {
        # The provided name is not scoped to the local machine and no credentials were provided.
        # If the object is a domain qualified name, we can try to resolve the user with domain trust, if setup.
        $UseDomainTrust = $true
        Write-Verbose -Message ($LocalizedData.ResolvingDomainAccountWithTrust -f $objectName)
    }

    # Get a PrincipalContext to use to resolve the object
    $principalContext = GetPrincipalContext -principalContexts $principalContexts -disposables $disposables -scope $scope -credential $credential
    
    if ($UseDomainTrust)
    {
        # When using domain trust, we use the object name to resolve. Object name can be in different formats such as a domain 
        # qualified name, UPN, or a distinguished name for the scope
        $account = $objectName
    }
    else
    {
        $account = $accountName
    }

    try
    {
        [System.DirectoryServices.AccountManagement.Principal] $principal = [System.DirectoryServices.AccountManagement.Principal]::FindByIdentity($principalContext, $account)
    }
    catch [System.Runtime.InteropServices.COMException]
    {
        ThrowInvalidArgumentError -ErrorId "PrincipalNotFound" -ErrorMessage ( $LocalizedData.UnableToResolveAccount -f $objectName, $_.Exception.Message, $_.Exception.HResult )
    }

    if ($principal -eq $null)
    {
        [string] $errorId = $null
        if ($isLocalMachine)
        {
            $errorId = "PrincipalNotFound_LocalMachine"
        }
        else
        {
            $errorId = "PrincipalNotFound_ProvidedCredential"
        }

        ThrowInvalidArgumentError -ErrorId $errorId -ErrorMessage ($LocalizedData.CouldNotFindPrincipal -f $objectName)
    }

    return $principal
}

<#
.Synopsis
    Resolves a SID to a principal
#>
function ResolveSidToPrincipal
{
    [OutputType([System.DirectoryServices.AccountManagement.Principal])]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNull()]
        [System.DirectoryServices.AccountManagement.PrincipalContext] $principalContext,

        [Parameter(Mandatory = $true)]
        [ValidateNotNull()]
        [System.Security.Principal.SecurityIdentifier] $sid,

        [Parameter(Mandatory = $true)]
        [bool] $isLocalMachineQualified
    )
    Set-StrictMode -Version Latest

    [string] $sidValue = $Sid.Value

    # Try to find a matching principal.
    $principal = [System.DirectoryServices.AccountManagement.Principal]::FindByIdentity($principalContext, [System.DirectoryServices.AccountManagement.IdentityType]::Sid, $sidValue)

    if ($principal -eq $null)
    {
        [string] $errorId = $null
        if ($isLocalMachineQualified)
        {
            $errorId = "PrincipalNotFound_LocalMachine"
        }
        else
        {
            $errorId = "PrincipalNotFound_ProvidedCredential"
        }

        ThrowInvalidArgumentError -ErrorId $errorId -ErrorMessage ($LocalizedData.CouldNotFindPrincipal -f $sid.ToString())
    }

    return $principal
}

<#
.Synopsis
    Gets a PrincipalContext to use to resolve an object in the specified $scope
    $principalContexts is a hashtable of scope to PrincipalContext. This is used to
    cache PrincipalContext instances for cases where it is used multiple times.
    disposables is an array of disposable objects. When a new PrincipalContext is
    created, it is added to the disposable list as well as the hashtable.
    $credential is used when a new PrincipalContext needs to be created with explicit
    credentials to a target domain
#>
function GetPrincipalContext
{
    [OutputType([System.DirectoryServices.AccountManagement.PrincipalContext])]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNull()]
        $principalContexts,

        [Parameter(Mandatory = $true)]
        [ValidateNotNull()]
        $disposables,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [object] $scope,

        [System.Net.NetworkCredential]
        $credential = $null
    )

    # The PrincipalContext to use to resolve the account
    [System.DirectoryServices.AccountManagement.PrincipalContext] $principalContext = $null

    # check for an object qualified to the local machine
    [bool] $isLocalMachine = [System.String]::Compare($env:COMPUTERNAME, $scope) -eq 0

    if ($isLocalMachine)
    {
        # check for a cached PrincipalContext for the local machine.
        if ($principalContexts.ContainsKey($env:COMPUTERNAME))
        {
            $principalContext = $principalContexts[$env:COMPUTERNAME]
        }
        else
        {
            # Create a PrincipalContext for the local machine
            $principalContext = New-Object System.DirectoryServices.AccountManagement.PrincipalContext([System.DirectoryServices.AccountManagement.ContextType]::Machine)

            # Cache the PrincipalContext for this scope for subsequent calls.
            $principalContexts.Add($env:COMPUTERNAME, $principalContext)
            $null = $disposables.Add($principalContext)
        }
    }
    elseif ($principalContexts.ContainsKey($scope))
    {
        $principalContext = $principalContexts[$scope]
    }
    elseif ($credential -ne $null)
    {
        # Create a PrincipalContext targeing $scope using the network credentials that were passed in.

        $name = [System.String]::Format("{0}\{1}", $credential.Domain, $credential.UserName)
        $principalContext = New-Object System.DirectoryServices.AccountManagement.PrincipalContext([System.DirectoryServices.AccountManagement.ContextType]::Domain, $scope, $name, $credential.Password)

        # Cache the PrincipalContext for this scope for subsequent calls.
        $principalContexts.Add($scope, $principalContext)
        $null = $disposables.Add($principalContext)
    }
    else
    {
        # Get a PrincipalContext for the current user in the target domain (even for local System account).
        $principalContext = New-Object System.DirectoryServices.AccountManagement.PrincipalContext([System.DirectoryServices.AccountManagement.ContextType]::Domain, $scope)

        # Cache the PrincipalContext for this scope for subsequent calls.
        $principalContexts.Add($scope, $principalContext)
        $null = $disposables.Add($principalContext)
    }

    return $principalContext
}


<#
.Synopsis
    Adds the entries defined in $Principals from $Group.Members.
    Returns $true if the members changed (i.e., members were added);
    otherwise, $false if all of the entries in $Principals were already present
    in $Group.Members
#>function AddGroupMembers
{
    [OutputType([bool])]
    param
    (
        [System.DirectoryServices.AccountManagement.GroupPrincipal] $Group,

        [System.DirectoryServices.AccountManagement.Principal[]] $Principals
    )
    Set-StrictMode -Version Latest
    [bool] $updated = $false

    if ($Principals -ne $null)
    {
        # Make changes to the group.
        foreach($principal in $Principals)
        {
            if ($group.Members.Contains($principal))
            {
                continue
            }
            $group.Members.Add($principal)
            # indicate a change was made to $Group.Members
            $updated = $true
        }
    }
    return $updated
}

<#
.Synopsis
    Removes the entries defined in $Principals from $Group.Members.
    Returns $true if the members changed (i.e., members were removed);
    otherwise, $false if none of the entries in $Principals were present
    in $Group.Members
#>
function RemoveGroupMembers
{
    [OutputType([bool])]
    param
    (
        [System.DirectoryServices.AccountManagement.GroupPrincipal] $Group,

        [System.DirectoryServices.AccountManagement.Principal[]] $Principals
    )
    Set-StrictMode -Version Latest
    [bool] $updated = $false

    if ($Principals -ne $null)
    {
        # Make changes to the group.
        foreach($principal in $Principals)
        {
            if ($group.Members.Remove($principal) -eq $true)
            {
                # indicated a change was made to the members.
                $updated = $true
            }
        }
    }

    return $updated
}

#region Utilities

<#
.Synopsis
    Determines if a scope represents the current machine.
#>
function IsLocalMachine
{
    [OutputType([bool])]
    param
    (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $scope
    )
    Set-StrictMode -Version latest

    if ($scope -eq ".")
    {
        return $true
    }

    if ($scope -eq $env:COMPUTERNAME)
    {
        return $true
    }

    if ($scope -eq "localhost")
    {
        return $true
    }

    if ($scope.Contains("."))
    {
        if ($scope -eq "127.0.0.1")
        {
            return $true
        }

        # Determine if we have an ip address that matches an ip address on one of the
        # network adapters.
        # NOTE: This is likely overkill; consider removing it.
        $items = @(Get-WmiObject Win32_NetworkAdapterConfiguration)
        foreach ($item in $items)
        {
            if ($item.IPaddress -ne $null)
            {
                foreach ($addr in $item.IPaddress)
                {
                    if ($addr -eq $scope)
                    {
                        return $true
                    }
                }
            }
        }
    }
    return $false
}

<#
.Synopsis
    Determines if a specified domain is the same as the domain defined in a PrincipalContext.
    This is used to determine if a new connection (PrincipalContext) needs to be created.

.Notes
    This method uses simple string compare and simple parsing to perform the match and
    should only be used to determine if a new connection is required since some
    comparisons may returned $false due to formatting differences.
#>
function IsSameDomain
{
    [OutputType([bool])]
    param
    (
        [Parameter(Mandatory=$true)]
        [ValidateNotNull()]
        [System.DirectoryServices.AccountManagement.PrincipalContext] $principalContext,

        [Parameter(Mandatory=$true)]
        [ValidateNotNull()]
        [String] $domain
    )
    Set-StrictMode -Version latest

    # Compare against Name of $principalContext - typically the undecorated domain name
    [bool] $isSameDomain = [System.String]::Compare($domain, $principalContext.Name, [System.StringComparison]::OrdinalIgnoreCase) -eq 0

    if ($isSameDomain -eq $false)
    {
        $dotIndex = $principalContext.ConnectedServer.IndexOf(".")
        if ($dotIndex -ne -1)
        {
            $principalDomain = $principalContext.ConnectedServer.Substring($dotIndex + 1)
            $isSameDomain = [System.String]::Compare($domain, $principalDomain, [System.StringComparison]::OrdinalIgnoreCase) -eq 0
        }
    }

    return $isSameDomain
}

<#
.Synopsis
    Parses various object name formats to extract the machine or domain scope.

    The returned $scope is used to determine where to perform the resolution,
    the local machine or a target domain while $accountName is the name
    of the account to resolve.

    The following details the formats that are handled as well as how the
    values are determined.

    Domain qualified names (domainname\username)

    The value is split on the first '\' character with the left hand side
    returned as the scope and the right hand side is returned as the account name.

    UPN: (username@domainname)

    The value is split on the first '@' character with the left hand side
    returned as the account name and the right hand side returned
    as the scope

    DistinguishedName:

    The value at the first occurance of "DC=" is used to extract the unqualified
    domain name.  The incoming string is returned, as is, for the account
    name.

    Unqualified account names:

    The incoming string is returned as the account name and the local
    machine name is returned as the scope. Note that values that do
    not fall into the above categories are interpreted as unqualified
    account names.

.Notes
    ResolveNameToPrincipal will fail if a machine name is specified
    as domainname\machinename. It will succeed if the machine name
    is specified as the SAM name (domainname\machinename$) or
    as the unqualified machine name.
    Parse-Scope splits the scope and account name to avoid
    the problem.
#>
function Parse-Scope
{
    [OutputType([string])]
    param
    (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $fullName,

        [Parameter(Mandatory=$true)]
        [AllowEmptyString()]
        [AllowNull()]
        [ref] $accountName
    )
    Set-StrictMode -Version latest

    # assume no scope is defined or $fullName is a DistinguishedName
    $accountName.Value = $fullName

    # parse domain or machine qualified account name
    [int] $separatorIndex = $fullName.IndexOf("\")
    if ($separatorIndex -ne -1)
    {
        $scope = $fullName.Substring(0, $separatorIndex)
        if (IsLocalMachine $scope)
        {
            $scope = $env:COMPUTERNAME
        }
        $accountName.Value = $fullName.Substring($separatorIndex+1)
        return $scope
    }

    # parse UPN for the scope
    $separatorIndex = $fullName.IndexOf("@")
    if ($separatorIndex -ne -1)
    {
        $scope = $fullName.Substring($separatorIndex + 1)
        $accountName.Value = $fullName.Substring(0,$separatorIndex)
        return $scope
    }

    # parse distinguished name for the scope
    $separatorIndex = $fullName.IndexOf("DC=", [System.StringComparison]::OrdinalIgnoreCase)
    if ($separatorIndex -ne -1)
    {
        # NOTE: For distinguished name formats, the DistinguishedName is
        # returned as the account name. See the initialization of $accountName
        # above.
        $startIndex = $separatorIndex + 3
        $endIndex = $fullName.IndexOf(",", $startIndex)
        if ($endIndex -gt $startIndex)
        {
            $length = $endIndex - $separatorIndex - 3
            $scope = $fullName.Substring($startIndex, $length)
            return $scope
        }
    }
    return $env:COMPUTERNAME
}

<#
.Synopsis
    Disposes the contents of an array list containing IDisposable objects.
#>
function DisposeAll
{
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNull()]
        [AllowEmptyCollection()]
        [System.Collections.ArrayList]
        $disposables
    )
    Set-StrictMode -Version latest

    if ($disposables.Count -gt 0)
    {
        foreach ($disposable in $disposables)
        {
            if ($disposable -is [System.IDisposable])
            {
                $disposable.Dispose()
            }
        }
    }
}

<#
.Synopsis
    Gets a local windows group

.Notes
    The returned value is NOT added to the $disposables list.
#>
function GetGroup
{
    [OutputType([System.DirectoryServices.AccountManagement.GroupPrincipal])]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string] $groupName,

        [Parameter(Mandatory = $true)]
        [ValidateNotNull()]
        [System.Collections.ArrayList]
        [AllowEmptyCollection()]
        $disposables,

        [Parameter(Mandatory = $true)]
        [AllowEmptyCollection()]
        $principalContexts
    )

    [System.DirectoryServices.AccountManagement.PrincipalContext] $principalContext = GetPrincipalContext -principalContexts $principalContexts -disposables $disposables -scope $env:COMPUTERNAME -credential $null
    [System.DirectoryServices.AccountManagement.GroupPrincipal] $group = [System.DirectoryServices.AccountManagement.GroupPrincipal]::FindByIdentity($principalContext, $groupName)
    # NOTE: $group is not automatically added to $disposables because the caller
    # may need to call $group.Delete() which also disposes it.
    return $group
}

<#
.Synopsis
Validates the Group name for invalid characters.
#>
function ValidateGroupName
{
    param
    (
        [parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $GroupName
    )

    # Check if the name consists of only periods and/or white spaces.
    [bool] $wrongName = $true

    for($i = 0; $i -lt $GroupName.Length; $i++)
    {
        if(-not [Char]::IsWhiteSpace($GroupName, $i) -and $GroupName[$i] -ne '.')
        {
            $wrongName = $false
            break
        }
    }

    $invalidChars = @('\','/','"','[',']',':','|','<','>','+','=',';',',','?','*','@')

    if($wrongName)
    {
        ThrowInvalidArgumentError -ErrorId "GroupNameHasOnlyWhiteSpacesAndDots" -ErrorMessage ($LocalizedData.InvalidGroupName -f $GroupName, [string]::Join(" ", $invalidChars))
    }

    if($GroupName.IndexOfAny($invalidChars) -ne -1)
    {
        ThrowInvalidArgumentError -ErrorId "GroupNameHasInvalidCharachter" -ErrorMessage ($LocalizedData.InvalidGroupName -f $GroupName, [string]::Join(" ", $invalidChars))
    }
}

<#
.Synopsis
Throws an argument error.
#>
function ThrowInvalidArgumentError
{
    [CmdletBinding()]
    param
    (

        [parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $ErrorId,

        [parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $ErrorMessage
    )

    $errorCategory=[System.Management.Automation.ErrorCategory]::InvalidArgument
    $exception = New-Object System.ArgumentException $ErrorMessage;
    $errorRecord = New-Object System.Management.Automation.ErrorRecord $exception, $ErrorId, $errorCategory, $null
    throw $errorRecord
}

Function Throw-TerminatingError
{
    param(
        [string] $Message,
        [System.Management.Automation.ErrorRecord] $ErrorRecord,
        [string] $ExceptionType
    )
    
    $exception = new-object "System.InvalidOperationException" $Message,$ErrorRecord.Exception
    $errorRecord = New-Object System.Management.Automation.ErrorRecord $exception,"MachineStateIncorrect","InvalidOperation",$null
    throw $errorRecord
}

<#
.Synopsis
Writes either to Verbose or ShouldProcess channel.
#>
function Write-Log
{
    [CmdletBinding(SupportsShouldProcess=$true)]
    param
    (
        [parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $Message
    )

    if ($PSCmdlet.ShouldProcess($Message, $null, $null))
    {
        Write-Verbose $Message
    }
}

#endregion

Export-ModuleMember -function Get-TargetResource, Set-TargetResource, Test-TargetResource
