ADGroup.psm1

Set-StrictMode -Version Latest
$ErrorActionPreference = [Management.Automation.ActionPreference]::Stop
. $PSScriptRoot\Shared\Variables.ps1


function Get-ADGroup {
    <#
    .SYNOPSIS
        Retrieves an Active Directory group.
    .DESCRIPTION
        Retrieves an Active Directory group using System.DirectoryServices.Protocols
    .OUTPUTS
        [PSCustomObject], none if not found.
    #>

    [OutputType([PSCustomObject])]
    [CmdletBinding(DefaultParameterSetName='Filter')]
    param (
        # The filter to search for groups. Uses normal LDAP Search syntax, *not*
        # PS ActiveDirectory search.
        [Parameter(Mandatory, ValueFromPipeline, ParameterSetName='Filter')]
        [string] $LDAPFilter,

        # The identity of the group to retrieve. Can be sAMAcountName, SID, LDAP
        # path, or distinguished name.
        [Parameter(Mandatory, ValueFromPipeline, ParameterSetName='Identity')]
        [string] $Identity,

        # The domain controller to query.
        [Parameter()]
        [string] $Server = $null,

        # Credentials for the domain controller.
        [Parameter()]
        [PSCredential] $Credential = $null
    )
    process {
        Get-ADObject 'Group' -ObjectPropertyConverter ${function:Convert-ADGroupPropertyTable} @PSBoundParameters
    }
}


function New-ADGroup {
    <#
    .SYNOPSIS
        Creates a new Active Directory group.
    .DESCRIPTION
        Creates a new Active Directory group using System.DirectoryServices.Protocols
    .OUTPUTS
        [PSCustomObject] if PassThru is enabled.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessage(
        'PSShouldProcess','',Scope='Function',Justification='-WhatIf passed through to ADObject func'
    )]
    [OutputType([PSCustomObject])]
    [CmdletBinding(SupportsShouldProcess)]
    param (
        # The name of the new group.
        [Parameter(Mandatory, ValueFromPipeline)]
        [string] $Name,

        # DistinguishedName path of the parent container. If not provided will
        # parent directly to the default Domain.
        [Parameter()]
        [string] $Path,

        [ValidateSet('', 'Distribution', 'Security')]
        [Parameter()]
        [string] $GroupCategory,

        [ValidateSet('', 'Global', 'DomainLocal', 'Universal')]
        [Parameter()]
        [string] $GroupScope,

        # A hashtable of LDAP attributes to set on the object.
        [Parameter()]
        [hashtable] $OtherAttributes,

        # The domain controller to query.
        [string] $Server,

        # Credentials for the domain controller.
        [PSCredential] $Credential,

        [switch] $PassThru
    )
    begin {
        $commonParams = @{
            WhatIf = $WhatIfPreference
            Verbose = $VerbosePreference
        }
    }
    process {
        $entry = New-ADObject 'Group' 'CN' $Name `
            -Path $Path `
            -DefaultRelativePath 'CN=Users' `
            -ObjectPropertyConverter ${function:Convert-ADGroupPropertyTable} `
            -OtherAttributes $OtherAttributes `
            -Server $Server `
            -Credential $Credential `
            -PassThru `
            -DoSAMAccountName `
            @commonParams

        if ($GroupCategory -or $GroupScope -or $OtherAttributes) {
            $entry = Set-ADGroup $entry.DistinguishedName -GroupCategory $GroupCategory -GroupScope $GroupScope -Server $Server -Credential $Credential @commonParams
        }

        if ($PassThru) {
            # output
            $entry
        }
    }
}


function Set-ADGroup {
    <#
    .SYNOPSIS
        Modifies an Active Directory group.
    .DESCRIPTION
        Modifies an Active Directory group using System.DirectoryServices.Protocols
    .OUTPUTS
        [PSCustomObject] if PassThru is enabled.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessage(
        'PSShouldProcess','',Scope='Function',Justification='-WhatIf passed through to ADObject func'
    )]
    [OutputType([PSCustomObject])]
    [CmdletBinding(SupportsShouldProcess)]
    param (
        # The identity of the group to alter. Can be sAMAcountName, SID, LDAP
        # path, or distinguished name.
        [Parameter(Mandatory, ValueFromPipeline)]
        [string] $Identity,

        [ValidateSet('', 'Distribution', 'Security')]
        [Parameter()]
        [string] $GroupCategory,

        [ValidateSet('', 'Global', 'DomainLocal', 'Universal')]
        [Parameter()]
        [string] $GroupScope,

        # A hashtable of LDAP properties to add on the entry.
        [Parameter()]
        [hashtable] $Add,

        # A hashtable of LDAP properties to remove from the entry.
        [Parameter()]
        [hashtable] $Remove,

        # A hashtable of LDAP properties to replace on the entry.
        [Parameter()]
        [hashtable] $Replace,

        # The domain controller to query.
        [Parameter()]
        [string] $Server,

        # Credentials for the domain controller.
        [Parameter()]
        [PSCredential] $Credential,

        [switch] $PassThru
    )
    begin {
        $commonParams = @{
            WhatIf = $WhatIfPreference
            Verbose = $VerbosePreference
        }
    }
    process {
        $entry = Get-ADGroup -Identity $Identity -Server $Server -Credential $Credential
        if ($GroupCategory -or $GroupScope -or $Add -or $Remove -or $Replace) {

            # clone $Replace so we can modify it here
            $replacementsTable = if ($OtherAttributes) {
                $Replace.Clone()
            } else {
                @{}
            }

            # using Add to force error if "replace" already has GroupType.
            $replacementsTable.Add('groupType', $entry.Properties['groupType'])

            Set-ADObject 'Group' -Identity $Identity -Add $Add -Remove $Remove -Replace $replacementsTable -Server $Server -Credential $Credential @commonParams
            Set-ADObjectEntry $Entry -Add $Add -Remove $Remove -Replace $replacementsTable @commonParams
            Update-ADGroupEntry $entry
        } else {
            Write-Warning "Can't update group '$Identity', nothing to do."
        }

        if ($PassThru) {
            # output
            $entry
        }
    }
}


function Remove-ADGroup {
    <#
    .SYNOPSIS
        Removes an Active Directory group.
    .DESCRIPTION
        Removes an Active Directory group using System.DirectoryServices.Protocols
    .OUTPUTS
        None
    #>

    [Diagnostics.CodeAnalysis.SuppressMessage(
        'PSShouldProcess','',Scope='Function',Justification='-WhatIf passed through to ADObject func'
    )]
    [CmdletBinding(SupportsShouldProcess)]
    param (
        # The identity of the group to alter. Can be sAMAcountName, SID, LDAP
        # path, or distinguished name.
        [Parameter(Mandatory, ValueFromPipeline)]
        [string] $Identity,

        # The domain controller to query.
        [Parameter()]
        [string] $Server,

        # Credentials for the domain controller.
        [Parameter()]
        [PSCredential] $Credential = $null
    )
    process {
        Remove-ADObject 'Group' @PSBoundParameters
    }
}


function Test-ADGroup {
    <#
    .SYNOPSIS
        Tests the existence of an Active Directory group.
    .DESCRIPTION
        Tests the existence of an Active Directory group using System.DirectoryServices.Protocols
    .OUTPUTS
        [bool]
    #>

    [OutputType([bool])]
    [CmdletBinding()]
    param (
        # The identity of the group to test. Can be sAMAcountName, SID, LDAP
        # path, or distinguished name.
        [Parameter(Mandatory, ValueFromPipeline)]
        [string] $Identity,

        # The domain controller to query.
        [Parameter()]
        [string] $Server,

        # Credentials for the domain controller.
        [Parameter()]
        [PSCredential] $Credential = $null
    )
    process {
        Test-ADObject 'Group' @PSBoundParameters
    }
}


#private
function Update-ADGroupEntry {
    <#
    .SYNOPSIS
        Recalculate the local properties of a directory entry PSCustomObject representing an AD group.
    #>

    param (
        [Parameter(Mandatory, ValueFromPipeline)]
        [PSCustomObject] $Entry
    )
    begin {
        $commonParams = @{
            WhatIf = $WhatIfPreference
            Verbose = $VerbosePreference
        }
    }
    process {
        Update-LDAPEntryFlag $Entry GroupType $GroupType_ACCOUNT_GROUP -NotePropertyName GroupScope -TrueValue Global @commonParams
        Update-LDAPEntryFlag $Entry GroupType $GroupType_RESOURCE_GROUP -NotePropertyName GroupScope -TrueValue DomainLocal @commonParams
        Update-LDAPEntryFlag $Entry GroupType $GroupType_UNIVERSAL_GROUP -NotePropertyName GroupScope -TrueValue Universal @commonParams

        Update-LDAPEntryFlag $Entry GroupType $GroupType_SECURITY_ENABLED -NotePropertyName GroupCategory -TrueValue Security -FalseValue Distribution @commonParams

        Update-ADObjectEntry $Entry -ObjectPropertyConverter ${function:Convert-ADGroupPropertyTable} @commonParams
    }
}


#private
function Set-ADGroupEntry {
    [Diagnostics.CodeAnalysis.SuppressMessage(
        'PSShouldProcess','',Scope='Function',Justification='-WhatIf passed through to LDAPEntry func'
    )]
    [CmdletBinding(SupportsShouldProcess)]
    param (
        [Parameter(Mandatory, ValueFromPipeline)]
        [PSCustomObject] $Entry,

        [ValidateSet('', 'Distribution', 'Security')]
        [Parameter()]
        [string] $GroupCategory,

        [ValidateSet('', 'Global', 'DomainLocal', 'Universal')]
        [Parameter()]
        [string] $GroupScope
    )
    begin {
        $commonParams = @{
            WhatIf = $WhatIfPreference
            Verbose = $VerbosePreference
        }
    }
    process {
        [nullable[bool]] $securityEnabled = if ($GroupCategory -eq 'Security') {
            $true
        } elseif ($GroupCategory -eq 'Distribution') {
            $false
        } else {
            $null
        }
        if ($null -ne $securityEnabled) {
            Set-LDAPEntryFlag $Entry GroupType $GroupType_SECURITY_ENABLED -Value $securityEnabled @commonParams
        }

        if ($GroupScope -eq 'Global') {
            Set-LDAPEntryFlag $Entry GroupType $GroupType_ACCOUNT_GROUP -Value $true @commonParams
            Set-LDAPEntryFlag $Entry GroupType $GroupType_RESOURCE_GROUP -Value $false @commonParams
            Set-LDAPEntryFlag $Entry GroupType $GroupType_UNIVERSAL_GROUP -Value $false @commonParams
        } elseif ($GroupScope -eq 'DomainLocal') {
            Set-LDAPEntryFlag $Entry GroupType $GroupType_ACCOUNT_GROUP -Value $false @commonParams
            Set-LDAPEntryFlag $Entry GroupType $GroupType_RESOURCE_GROUP -Value $true @commonParams
            Set-LDAPEntryFlag $Entry GroupType $GroupType_UNIVERSAL_GROUP -Value $false @commonParams
        } elseif ($GroupScope -eq 'Universal') {
            Set-LDAPEntryFlag $Entry GroupType $GroupType_ACCOUNT_GROUP -Value $false @commonParams
            Set-LDAPEntryFlag $Entry GroupType $GroupType_RESOURCE_GROUP -Value $false @commonParams
            Set-LDAPEntryFlag $Entry GroupType $GroupType_UNIVERSAL_GROUP -Value $true @commonParams
        }
    }
}


function Convert-ADGroupPropertyTable {
    <#
    .SYNOPSIS
        Takes a table of raw LDAP properties and converts them into a table of
        object properties for an ADGroup.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory, ValueFromPipeline)]
        [hashtable] $LdapAttributeTable,
        [hashtable] $ObjectPropertyTable
    )
    process {
        if(-not $ObjectPropertyTable) {
            $ObjectPropertyTable = @{}
        }

        Convert-ADObjectPropertyTable -LdapAttributeTable $LdapAttributeTable -ObjectPropertyTable $ObjectPropertyTable | Out-Null

        $ObjectPropertyTable['GroupCategory'] = if($LdapAttributeTable['groupType'] -band $GroupType_SECURITY_ENABLED) {
            'Security'
        } else {
            'Distribution'
        }

        $ObjectPropertyTable['GroupScope'] = if ($LdapAttributeTable['groupType'] -band $GroupType_ACCOUNT_GROUP) {
            'Global'
        } elseif ($LdapAttributeTable['groupType'] -band $GroupType_RESOURCE_GROUP) {
            'DomainLocal'
        } elseif ($LdapAttributeTable['groupType'] -band $GroupType_UNIVERSAL_GROUP) {
            'Universal'
        } # (bit mask 1, 2, 4, or 8)']
        $ObjectPropertyTable['HomePage'] = $LdapAttributeTable['wWWHomePage']
        $ObjectPropertyTable['ManagedBy'] = $LdapAttributeTable['managedBy']
        $ObjectPropertyTable['MemberOf'] = $LdapAttributeTable['memberOf']
        $ObjectPropertyTable['Members'] = $LdapAttributeTable['member']
        $ObjectPropertyTable['SamAccountName'] = $LdapAttributeTable['sAMAccountName']
        $ObjectPropertyTable['SID'] = $LdapAttributeTable['objectSID'].ToString()
        $ObjectPropertyTable['SIDHistory'] = $LdapAttributeTable['sidHistory']
        
        #output
        $ObjectPropertyTable
    }
}