DSCResources/MSFT_xADGroup/MSFT_xADGroup.psm1

$script:resourceModulePath = Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent
$script:modulesFolderPath = Join-Path -Path $script:resourceModulePath -ChildPath 'Modules'

$script:localizationModulePath = Join-Path -Path $script:modulesFolderPath -ChildPath 'xActiveDirectory.Common'
Import-Module -Name (Join-Path -Path $script:localizationModulePath -ChildPath 'xActiveDirectory.Common.psm1')

$script:localizedData = Get-LocalizedData -ResourceName 'MSFT_xADGroup'

function Get-TargetResource
{
    [CmdletBinding()]
    [OutputType([System.Collections.Hashtable])]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $GroupName,

        [Parameter()]
        [ValidateSet('DomainLocal', 'Global', 'Universal')]
        [System.String]
        $GroupScope = 'Global',

        [Parameter()]
        [ValidateSet('Security', 'Distribution')]
        [System.String]
        $Category = 'Security',

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $Path,

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

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $Description,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $DisplayName,

        [Parameter()]
        [ValidateNotNull()]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.CredentialAttribute()]
        $Credential,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $DomainController,

        [Parameter()]
        [System.String[]]
        $Members,

        [Parameter()]
        [System.String[]]
        $MembersToInclude,

        [Parameter()]
        [System.String[]]
        $MembersToExclude,

        [Parameter()]
        [ValidateSet('SamAccountName', 'DistinguishedName', 'SID', 'ObjectGUID')]
        [System.String]
        $MembershipAttribute = 'SamAccountName',

        # This must be the user's DN
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $ManagedBy,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $Notes,

        [Parameter()]
        [ValidateNotNull()]
        [System.Boolean]
        $RestoreFromRecycleBin
    )

    Assert-Module -ModuleName 'ActiveDirectory'

    $adGroupParams = Get-ADCommonParameters @PSBoundParameters

    try
    {
        $adGroup = Get-ADGroup @adGroupParams -Property Name, GroupScope, GroupCategory, DistinguishedName, Description, DisplayName, ManagedBy, Info

        Write-Verbose -Message ($script:localizedData.RetrievingGroupMembers -f $MembershipAttribute)

        # Retrieve the current list of members, returning the specified membership attribute
        [System.Array]$adGroupMembers = (Get-ADGroupMember @adGroupParams).$MembershipAttribute

        $targetResource = @{
            GroupName           = $adGroup.Name
            GroupScope          = $adGroup.GroupScope
            Category            = $adGroup.GroupCategory
            Path                = Get-ADObjectParentDN -DN $adGroup.DistinguishedName
            Description         = $adGroup.Description
            DisplayName         = $adGroup.DisplayName
            Members             = $adGroupMembers
            MembersToInclude    = $MembersToInclude
            MembersToExclude    = $MembersToExclude
            MembershipAttribute = $MembershipAttribute
            ManagedBy           = $adGroup.ManagedBy
            Notes               = $adGroup.Info
            Ensure              = 'Absent'
        }

        if ($adGroup)
        {
            $targetResource['Ensure'] = 'Present'
        }
    }
    catch [Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException]
    {
        Write-Verbose -Message ($script:localizedData.GroupNotFound -f $GroupName)

        $targetResource = @{
            GroupName           = $GroupName
            GroupScope          = $GroupScope
            Category            = $Category
            Path                = $Path
            Description         = $Description
            DisplayName         = $DisplayName
            Members             = @()
            MembersToInclude    = $MembersToInclude
            MembersToExclude    = $MembersToExclude
            MembershipAttribute = $MembershipAttribute
            ManagedBy           = $ManagedBy
            Notes               = $Notes
            Ensure              = 'Absent'
        }
    }

    return $targetResource
} #end function Get-TargetResource

function Test-TargetResource
{
    [CmdletBinding()]
    [OutputType([System.Boolean])]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $GroupName,

        [Parameter()]
        [ValidateSet('DomainLocal', 'Global', 'Universal')]
        [System.String]
        $GroupScope = 'Global',

        [Parameter()]
        [ValidateSet('Security', 'Distribution')]
        [System.String]
        $Category = 'Security',

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $Path,

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

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $Description,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $DisplayName,

        [Parameter()]
        [ValidateNotNull()]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.CredentialAttribute()]
        $Credential,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $DomainController,

        [Parameter()]
        [System.String[]]
        $Members,

        [Parameter()]
        [System.String[]]
        $MembersToInclude,

        [Parameter()]
        [System.String[]]
        $MembersToExclude,

        [Parameter()]
        [ValidateSet('SamAccountName', 'DistinguishedName', 'SID', 'ObjectGUID')]
        [System.String]
        $MembershipAttribute = 'SamAccountName',

        # This must be the user's DN
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $ManagedBy,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $Notes,

        [Parameter()]
        [ValidateNotNull()]
        [System.Boolean]
        $RestoreFromRecycleBin
    )

    # Validate parameters before we even attempt to retrieve anything
    $assertMemberParameters = @{ }

    if ($PSBoundParameters.ContainsKey('Members') -and -not [system.string]::IsNullOrEmpty($Members))
    {
        $assertMemberParameters['Members'] = $Members
    }

    if ($PSBoundParameters.ContainsKey('MembersToInclude') -and -not [system.string]::IsNullOrEmpty($MembersToInclude))
    {
        $assertMemberParameters['MembersToInclude'] = $MembersToInclude
    }

    if ($PSBoundParameters.ContainsKey('MembersToExclude') -and -not [system.string]::IsNullOrEmpty($MembersToExclude))
    {
        $assertMemberParameters['MembersToExclude'] = $MembersToExclude
    }

    Assert-MemberParameters @assertMemberParameters -ModuleName 'xADDomain' -ErrorAction Stop

    $targetResource = Get-TargetResource @PSBoundParameters

    $targetResourceInCompliance = $true

    if ($PSBoundParameters.ContainsKey('GroupScope') -and $targetResource.GroupScope -ne $GroupScope)
    {
        Write-Verbose -Message ($script:localizedData.NotDesiredPropertyState -f 'GroupScope', $GroupScope, $targetResource.GroupScope)
        $targetResourceInCompliance = $false
    }

    if ($PSBoundParameters.ContainsKey('Category') -and $targetResource.Category -ne $Category)
    {
        Write-Verbose -Message ($script:localizedData.NotDesiredPropertyState -f 'Category', $Category, $targetResource.Category)
        $targetResourceInCompliance = $false
    }

    if ($Path -and ($targetResource.Path -ne $Path))
    {
        Write-Verbose -Message ($script:localizedData.NotDesiredPropertyState -f 'Path', $Path, $targetResource.Path)
        $targetResourceInCompliance = $false
    }

    if ($Description -and ($targetResource.Description -ne $Description))
    {
        Write-Verbose -Message ($script:localizedData.NotDesiredPropertyState -f 'Description', $Description, $targetResource.Description)
        $targetResourceInCompliance = $false
    }

    if ($DisplayName -and ($targetResource.DisplayName -ne $DisplayName))
    {
        Write-Verbose -Message ($script:localizedData.NotDesiredPropertyState -f 'DisplayName', $DisplayName, $targetResource.DisplayName)
        $targetResourceInCompliance = $false
    }

    if ($ManagedBy -and ($targetResource.ManagedBy -ne $ManagedBy))
    {
        Write-Verbose -Message ($script:localizedData.NotDesiredPropertyState -f 'ManagedBy', $ManagedBy, $targetResource.ManagedBy)
        $targetResourceInCompliance = $false
    }

    if ($Notes -and ($targetResource.Notes -ne $Notes))
    {
        Write-Verbose -Message ($script:localizedData.NotDesiredPropertyState -f 'Notes', $Notes, $targetResource.Notes)
        $targetResourceInCompliance = $false
    }

    # Test group members match passed membership parameters
    if (-not (Test-Members @assertMemberParameters -ExistingMembers $targetResource.Members))
    {
        Write-Verbose -Message $script:localizedData.GroupMembershipNotDesiredState
        $targetResourceInCompliance = $false
    }

    if ($targetResource.Ensure -ne $Ensure)
    {
        Write-Verbose -Message ($script:localizedData.NotDesiredPropertyState -f 'Ensure', $Ensure, $targetResource.Ensure)
        $targetResourceInCompliance = $false
    }

    return $targetResourceInCompliance
} #end function Test-TargetResource

function Set-TargetResource
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $GroupName,

        [Parameter()]
        [ValidateSet('DomainLocal', 'Global', 'Universal')]
        [System.String]
        $GroupScope = 'Global',

        [Parameter()]
        [ValidateSet('Security', 'Distribution')]
        [System.String]
        $Category = 'Security',

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $Path,

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

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $Description,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $DisplayName,

        [Parameter()]
        [ValidateNotNull()]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.CredentialAttribute()]
        $Credential,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $DomainController,

        [Parameter()]
        [System.String[]]
        $Members,

        [Parameter()]
        [System.String[]]
        $MembersToInclude,

        [Parameter()]
        [System.String[]]
        $MembersToExclude,

        [Parameter()]
        [ValidateSet('SamAccountName', 'DistinguishedName', 'SID', 'ObjectGUID')]
        [System.String]
        $MembershipAttribute = 'SamAccountName',

        # This must be the user's DN
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $ManagedBy,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $Notes,

        [Parameter()]
        [ValidateNotNull()]
        [System.Boolean]
        $RestoreFromRecycleBin

    )

    Assert-Module -ModuleName 'ActiveDirectory'

    $adGroupParams = Get-ADCommonParameters @PSBoundParameters

    try
    {
        if ($MembershipAttribute -eq 'DistinguishedName')
        {
            $allMembers = $Members + $MembersToInclude + $MembersToExclude

            $groupMemberDomains = @()

            foreach ($member in $allMembers)
            {
                $groupMemberDomains += Get-ADDomainNameFromDistinguishedName -DistinguishedName $member
            }

            $uniqueGroupMemberDomainCount = $groupMemberDomains |
                Select-Object -Unique

            $GroupMemberDomainCount = $uniqueGroupMemberDomainCount.count

            if ($GroupMemberDomainCount -gt 1 -or ($groupMemberDomains -ine (Get-DomainName)).Count -gt 0)
            {
                Write-Verbose -Message ($script:localizedData.GroupMembershipMultipleDomains -f $GroupMemberDomainCount)
                $MembersInMultipleDomains = $true
            }
        }

        $adGroup = Get-ADGroup @adGroupParams -Property Name, GroupScope, GroupCategory, DistinguishedName, Description, DisplayName, ManagedBy, Info

        if ($Ensure -eq 'Present')
        {
            $setADGroupParams = $adGroupParams.Clone()
            $setADGroupParams['Identity'] = $adGroup.DistinguishedName

            # Update existing group properties
            if ($PSBoundParameters.ContainsKey('Category') -and $Category -ne $adGroup.GroupCategory)
            {
                Write-Verbose -Message ($script:localizedData.UpdatingGroupProperty -f 'Category', $Category)
                $setADGroupParams['GroupCategory'] = $Category
            }

            if ($PSBoundParameters.ContainsKey('GroupScope') -and $GroupScope -ne $adGroup.GroupScope)
            {
                # Cannot change DomainLocal to Global or vice versa directly. Need to change them to a Universal group first!
                Set-ADGroup -Identity $adGroup.DistinguishedName -GroupScope Universal
                Write-Verbose -Message ($script:localizedData.UpdatingGroupProperty -f 'GroupScope', $GroupScope)
                $setADGroupParams['GroupScope'] = $GroupScope
            }

            if ($Description -and ($Description -ne $adGroup.Description))
            {
                Write-Verbose -Message ($script:localizedData.UpdatingGroupProperty -f 'Description', $Description)
                $setADGroupParams['Description'] = $Description
            }

            if ($DisplayName -and ($DisplayName -ne $adGroup.DisplayName))
            {
                Write-Verbose -Message ($script:localizedData.UpdatingGroupProperty -f 'DisplayName', $DisplayName)
                $setADGroupParams['DisplayName'] = $DisplayName
            }

            if ($ManagedBy -and ($ManagedBy -ne $adGroup.ManagedBy))
            {
                Write-Verbose -Message ($script:localizedData.UpdatingGroupProperty -f 'ManagedBy', $ManagedBy)
                $setADGroupParams['ManagedBy'] = $ManagedBy
            }

            if ($Notes -and ($Notes -ne $adGroup.Info))
            {
                Write-Verbose -Message ($script:localizedData.UpdatingGroupProperty -f 'Notes', $Notes)
                $setADGroupParams['Replace'] = @{ Info = $Notes }
            }

            Write-Verbose -Message ($script:localizedData.UpdatingGroup -f $GroupName)

            Set-ADGroup @setADGroupParams

            # Move group if the path is not correct
            if ($Path -and ($Path -ne (Get-ADObjectParentDN -DN $adGroup.DistinguishedName)))
            {
                Write-Verbose -Message ($script:localizedData.MovingGroup -f $GroupName, $Path)

                $moveADObjectParams = $adGroupParams.Clone()
                $moveADObjectParams['Identity'] = $adGroup.DistinguishedName

                Move-ADObject @moveADObjectParams -TargetPath $Path
            }

            Write-Verbose -Message ($script:localizedData.RetrievingGroupMembers -f $MembershipAttribute)

            $adGroupMembers = (Get-ADGroupMember @adGroupParams).$MembershipAttribute

            if (-not (Test-Members -ExistingMembers $adGroupMembers -Members $Members -MembersToInclude $MembersToInclude -MembersToExclude $MembersToExclude))
            {
                <#
                    The fact that we're in the Set method, there is no need to
                    validate the parameter combination as this was performed in
                    the Test method.
                #>

                if ($PSBoundParameters.ContainsKey('Members') -and -not [system.string]::IsNullOrEmpty($Members))
                {
                    # Remove all existing first and add explicit members
                    $Members = Remove-DuplicateMembers -Members $Members

                    # We can only remove members if there are members already in the group!
                    if ($adGroupMembers.Count -gt 0)
                    {
                        Write-Verbose -Message ($script:localizedData.RemovingGroupMembers -f $adGroupMembers.Count, $GroupName)

                        Remove-ADGroupMember @adGroupParams -Members $adGroupMembers -Confirm:$false
                    }

                    Write-Verbose -Message ($script:localizedData.AddingGroupMembers -f $Members.Count, $GroupName)

                    Add-ADCommonGroupMember -Parameter $adGroupParams -Members $Members -MembersInMultipleDomains:$MembersInMultipleDomains
                }

                if ($PSBoundParameters.ContainsKey('MembersToInclude') -and -not [system.string]::IsNullOrEmpty($MembersToInclude))
                {
                    $MembersToInclude = Remove-DuplicateMembers -Members $MembersToInclude

                    Write-Verbose -Message ($script:localizedData.AddingGroupMembers -f $MembersToInclude.Count, $GroupName)

                    Add-ADCommonGroupMember -Parameter $adGroupParams -Members $MembersToInclude -MembersInMultipleDomains:$MembersInMultipleDomains
                }

                if ($PSBoundParameters.ContainsKey('MembersToExclude') -and -not [system.string]::IsNullOrEmpty($MembersToExclude))
                {
                    $MembersToExclude = Remove-DuplicateMembers -Members $MembersToExclude

                    Write-Verbose -Message ($script:localizedData.RemovingGroupMembers -f $MembersToExclude.Count, $GroupName)

                    Remove-ADGroupMember @adGroupParams -Members $MembersToExclude -Confirm:$false
                }
            }
        }
        elseif ($Ensure -eq 'Absent')
        {
            # Remove existing group
            Write-Verbose -Message ($script:localizedData.RemovingGroup -f $GroupName)

            Remove-ADGroup @adGroupParams -Confirm:$false
        }
    }
    catch [Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException]
    {
        # The AD group doesn't exist
        if ($Ensure -eq 'Present')
        {
            Write-Verbose -Message ($script:localizedData.GroupNotFound -f $GroupName)

            $adGroupParams = Get-ADCommonParameters @PSBoundParameters -UseNameParameter

            if ($Description)
            {
                $adGroupParams['Description'] = $Description
            }

            if ($DisplayName)
            {
                $adGroupParams['DisplayName'] = $DisplayName
            }

            if ($ManagedBy)
            {
                $adGroupParams['ManagedBy'] = $ManagedBy
            }

            if ($Path)
            {
                $adGroupParams['Path'] = $Path
            }

            # Create group. Try to restore account first if it exists.
            if ($RestoreFromRecycleBin)
            {
                Write-Verbose -Message ($script:localizedData.RestoringGroup -f $GroupName)

                $restoreParams = Get-ADCommonParameters @PSBoundParameters

                $adGroup = Restore-ADCommonObject @restoreParams -ObjectClass Group -ErrorAction Stop
            }

            if (-not $adGroup)
            {
                Write-Verbose -Message ($script:localizedData.AddingGroup -f $GroupName)

                $adGroup = New-ADGroup @adGroupParams -GroupCategory $Category -GroupScope $GroupScope -PassThru
            }

            <#
                Only the New-ADGroup cmdlet takes a -Name parameter. Refresh
                the parameters with the -Identity parameter rather than -Name.
            #>

            $adGroupParams = Get-ADCommonParameters @PSBoundParameters

            if ($Notes)
            {
                # Can't set the Notes field when creating the group
                Write-Verbose -Message ($script:localizedData.UpdatingGroupProperty -f 'Notes', $Notes)

                $setADGroupParams = $adGroupParams.Clone()
                $setADGroupParams['Identity'] = $adGroup.DistinguishedName

                Set-ADGroup @setADGroupParams -Add @{ Info = $Notes }
            }

            # Add the required members
            if ($PSBoundParameters.ContainsKey('Members') -and -not [system.string]::IsNullOrEmpty($Members))
            {
                $Members = Remove-DuplicateMembers -Members $Members

                Write-Verbose -Message ($script:localizedData.AddingGroupMembers -f $Members.Count, $GroupName)

                Add-ADCommonGroupMember -Parameter $adGroupParams -Members $Members -MembersInMultipleDomains:$MembersInMultipleDomains
            }
            elseif ($PSBoundParameters.ContainsKey('MembersToInclude') -and -not [system.string]::IsNullOrEmpty($MembersToInclude))
            {
                $MembersToInclude = Remove-DuplicateMembers -Members $MembersToInclude

                Write-Verbose -Message ($script:localizedData.AddingGroupMembers -f $MembersToInclude.Count, $GroupName)

                Add-ADCommonGroupMember -Parameter $adGroupParams -Members $MembersToInclude -MembersInMultipleDomains:$MembersInMultipleDomains
            }
        }
    } #end catch
} #end function Set-TargetResource

Export-ModuleMember -Function *-TargetResource