ADUser.psm1

Set-StrictMode -Version Latest
$ErrorActionPreference = [Management.Automation.ActionPreference]::Stop
. $PSScriptRoot\Shared\Variables.ps1
Add-Type -AssemblyName 'System.DirectoryServices.Protocols'

function Get-ADUser {
    <#
    .SYNOPSIS
        Retrieves an Active Directory user.
    .DESCRIPTION
        Retrieves an Active Directory user by their identity, which can be a
        distinguished name, GUID, SID, or sAMAccountName.
    .OUTPUTS
        [PSCustomObject], $null if not found.
    #>

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

        # The identity of the user 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 'User' @PSBoundParameters -ObjectPropertyConverter ${function:Convert-ADUserPropertyTable}
    }
}


function New-ADUser {
    <#
    .SYNOPSIS
        Creates a new Active Directory user.
    .DESCRIPTION
        Creates a new Active Directory user with the specified name.
    .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 user.
        [Parameter(Mandatory)]
        [string] $Name,

        # The path to create the user in. Optional, by default wil create it in
        # "CN=Users,DC=<your domain etc>"
        [Parameter()]
        [string] $Path,

        # Whether or not the user is enabled. Defaults to "True" if not set.
        [Parameter()]
        [Nullable[bool]] $Enabled,

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

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

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

        [switch] $PassThru
    )
    begin {
        # default to enabled. Everywhere else Enabled is nullable and null means
        # "no change" except here.
        if ($Null -eq $Enabled) {
            $Enabled = $true
        }
        $commonParams = @{
            WhatIf = $WhatIfPreference
            Verbose = $VerbosePreference
        }
    }
    process {
        $entry = New-ADObject 'User' 'CN' $Name `
            -Path $Path `
            -DefaultRelativePath 'CN=Users' `
            -ObjectPropertyConverter ${function:Convert-ADUserPropertyTable} `
            -Server $Server `
            -Credential $Credential `
            -DoSAMAccountName `
            -PassThru `
            @commonParams

        if (($null -ne $Enabled) -or $OtherAttributes) {
            $entry = Set-ADUser -Identity $entry.DistinguishedName -Enabled $Enabled -Replace $OtherAttributes -Server $Server -Credential $Credential -PassThru @commonParams
        }
        
        if ($PassThru) {
            # output
            $entry
        }
    }
}


function Set-ADUser {
    <#
    .SYNOPSIS
        Modifies an Active Directory user.
    .DESCRIPTION
        Modifies an Active Directory user with the specified properties.
    .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 user to modify.
        [Parameter(Mandatory, ValueFromPipeline)]
        [string] $Identity,

        [Parameter()]
        [Nullable[bool]] $Enabled,

        # 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-ADUser -Identity $Identity -Server $Server -Credential $Credential
        if (($null -ne $Enabled) -or $Add -or $Remove -or $Replace) {
            Set-ADUserEntry $entry -Enabled $Enabled @commonParams

            # clone $Replace so we can modify it here
            $replacementsTable = if ($Replace) {
                $Replace.Clone()
            } else {
                @{}
            }
            # using Add to force error if "replace" already has GroupType.
            $replacementsTable.Add('userAccountControl', $entry.Properties['userAccountControl'])

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

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


function Remove-ADUser {
    <#
    .SYNOPSIS
        Removes an Active Directory user.
    .DESCRIPTION
        Removes an Active Directory user by their identity.
    .OUTPUTS
        None
    #>

    [Diagnostics.CodeAnalysis.SuppressMessage("PSShouldProcess","",Scope="Function")] # -WhatIf passed through to ADObject func
    [CmdletBinding(SupportsShouldProcess)]
    param (
        [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 'User' @PSBoundParameters
    }
}


function Test-ADUser {
    <#
    .SYNOPSIS
        Tests if an Active Directory user exists.
    .DESCRIPTION
        Tests if an Active Directory user exists by their identity.
    .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
    )
    process {
        Test-ADObject 'User' @PSBoundParameters
    }
}


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

    [Diagnostics.CodeAnalysis.SuppressMessage(
        'PSShouldProcess','',Scope='Function',Justification='-WhatIf passed through to LDAPEntry func'
    )]
    [CmdletBinding(SupportsShouldProcess)]
    param (
        [Parameter(Mandatory, ValueFromPipeline)]
        [PSCustomObject] $Entry
    )
    begin {
        $commonParams = @{
            WhatIf = $WhatIfPreference
            Verbose = $VerbosePreference
        }
    }
    process {
        Update-LDAPEntryFlag $Entry userAccountControl $UserAccountControl_ACCOUNT_DISABLED -NotePropertyName Enabled -TrueValue $false -FalseValue $true @commonParams
        Update-ADObjectEntry $Entry -ObjectPropertyConverter ${function:Convert-ADUserPropertyTable} @commonParams
    }
}


function Set-ADUserEntry {
    <#
    .SYNOPSIS
        Set the members of an AD User directory entry PSCustomObject
    #>

    [Diagnostics.CodeAnalysis.SuppressMessage("PSShouldProcess","",Scope="Function")] # -WhatIf passed through to ADObject func
    [CmdletBinding(SupportsShouldProcess)]
    param (
        [Parameter(Mandatory, ValueFromPipeline)]
        [PSCustomObject] $Entry,

        [Parameter()]
        [Nullable[bool]] $Enabled
    )
    begin {
        $commonParams = @{
            WhatIf = $WhatIfPreference
            Verbose = $VerbosePreference
        }
    }
    process {
        if ($null -ne $Enabled) {
            Set-LDAPEntryFlag $Entry userAccountControl $UserAccountControl_ACCOUNT_DISABLED -Value (-not $Enabled) @commonParams
        }
        Update-ADObjectEntry $Entry -ObjectPropertyConverter ${function:Convert-ADUserPropertyTable}
    }
}


function Convert-ADUserPropertyTable {
    <#
    .SYNOPSIS
        Takes a table of raw LDAP properties and converts them into a table of
        object properties for an ADObject.
    .NOTES
        Adapted from https://learn.microsoft.com/en-us/archive/technet-wiki/12037.active-directory-get-aduser-default-band-extended-properties
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory, ValueFromPipeline)]
        [hashtable] $LdapAttributeTable,

        [Parameter()]
        [hashtable] $ObjectPropertyTable
    )
    process {
        if(-not $ObjectPropertyTable) {
            $ObjectPropertyTable = @{}
        }

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

        # regex used to initially create this
        # (\w+)\t([a-zA-Z0-9 ()]+)\t(\w+)\t(.*)
        # $ObjectPropertyTable['$1'] = $LdapAttributeTable['$4']
        $ObjectPropertyTable['AccountExpirationDate'] = Convert-ADDateTime $LdapAttributeTable['accountExpires']
        $ObjectPropertyTable['AccountLockoutTime'] = Convert-ADDateTime $LdapAttributeTable['lockoutTime']
        $ObjectPropertyTable['AccountNotDelegated'] = [bool] ($LdapAttributeTable['userAccountControl'] -band $UserAccountControl_NOT_DELEGATED)
        $ObjectPropertyTable['AllowReversiblePasswordEncryption'] = [bool] ($LdapAttributeTable['userAccountControl'] -band $UserAccountControl_ENCRYPTED_TEXT_PWD_ALLOWED)
        $ObjectPropertyTable['BadLogonCount'] = $LdapAttributeTable['badPwdCount']
        $ObjectPropertyTable['CannotChangePassword'] = $LdapAttributeTable['nTSecurityDescriptor']
        $ObjectPropertyTable['Certificates'] = $LdapAttributeTable['userCertificate']
        $ObjectPropertyTable['ChangePasswordAtLogon'] = $LdapAttributeTable['pwdLastSet'] -eq 0
        $ObjectPropertyTable['City'] = $LdapAttributeTable['l']
        $ObjectPropertyTable['Company'] = $LdapAttributeTable['company']
        $ObjectPropertyTable['Country'] = $LdapAttributeTable['c'] # (2 character abbreviation)
        $ObjectPropertyTable['Department'] = $LdapAttributeTable['department']
        $ObjectPropertyTable['Division'] = $LdapAttributeTable['division']
        $ObjectPropertyTable['DoesNotRequirePreAuth'] = [bool] ($LdapAttributeTable['userAccountControl'] -band $UserAccountControl_DONT_REQ_PREAUTH)
        $ObjectPropertyTable['EmailAddress'] = $LdapAttributeTable['mail']
        $ObjectPropertyTable['EmployeeID'] = $LdapAttributeTable['employeeID']
        $ObjectPropertyTable['EmployeeNumber'] = $LdapAttributeTable['employeeNumber']
        $ObjectPropertyTable['Enabled'] = -not ($LdapAttributeTable['userAccountControl'] -band $UserAccountControl_ACCOUNT_DISABLED)
        $ObjectPropertyTable['Fax'] = $LdapAttributeTable['facsimileTelephoneNumber']
        $ObjectPropertyTable['GivenName'] = $LdapAttributeTable['givenName']
        $ObjectPropertyTable['HomeDirectory'] = $LdapAttributeTable['homeDirectory']
        $ObjectPropertyTable['HomedirRequired'] = [bool] ($LdapAttributeTable['userAccountControl'] -band $UserAccountControl_HOMEDIR_REQUIRED)
        $ObjectPropertyTable['HomeDrive'] = $LdapAttributeTable['homeDrive']
        $ObjectPropertyTable['HomePage'] = $LdapAttributeTable['wWWHomePage']
        $ObjectPropertyTable['HomePhone'] = $LdapAttributeTable['homePhone']
        $ObjectPropertyTable['Initials'] = $LdapAttributeTable['initials']
        $ObjectPropertyTable['LastBadPasswordAttempt'] = Convert-ADDateTime $LdapAttributeTable['badPasswordTime']
        $ObjectPropertyTable['LastLogonDate'] = Convert-ADDateTime $LdapAttributeTable['lastLogonTimeStamp']
        $ObjectPropertyTable['LockedOut'] = [bool] ($LdapAttributeTable['msDS-User-Account-Control-Computed'] -band $UserAccountControl_LOCKOUT)
        $ObjectPropertyTable['LogonWorkstations'] = $LdapAttributeTable['userWorkstations']
        $ObjectPropertyTable['Manager'] = $LdapAttributeTable['manager']
        $ObjectPropertyTable['MemberOf'] = $LdapAttributeTable['memberOf']
        $ObjectPropertyTable['MNSLogonAccount'] = [bool] ($LdapAttributeTable['userAccountControl'] -band $UserAccountControl_MNS_LOGON_ACCOUNT)
        $ObjectPropertyTable['MobilePhone'] = $LdapAttributeTable['mobile']
        $ObjectPropertyTable['Office'] = $LdapAttributeTable['physicalDeliveryOfficeName']
        $ObjectPropertyTable['OfficePhone'] = $LdapAttributeTable['telephoneNumber']
        $ObjectPropertyTable['Organization'] = $LdapAttributeTable['o']
        $ObjectPropertyTable['OtherName'] = $LdapAttributeTable['middleName']
        $ObjectPropertyTable['PasswordExpired'] = [bool] ($LdapAttributeTable['msDS-User-Account-Control-Computed'] -band $UserAccountControl_PASSWORD_EXPIRED) # see note 1
        $ObjectPropertyTable['PasswordLastSet'] = Convert-ADDateTime $LdapAttributeTable['pwdLastSet']
        $ObjectPropertyTable['PasswordNeverExpires'] = [bool] ($LdapAttributeTable['userAccountControl'] -band $UserAccountControl_DONT_EXPIRE_PASSWORD)
        $ObjectPropertyTable['PasswordNotRequired'] = [bool] ($LdapAttributeTable['userAccountControl'] -band $UserAccountControl_PASSWD_NOTREQD)
        $ObjectPropertyTable['POBox'] = $LdapAttributeTable['postOfficeBox']
        $ObjectPropertyTable['PostalCode'] = $LdapAttributeTable['postalCode']
        $ObjectPropertyTable['PrimaryGroup'] = $LdapAttributeTable['Group with primaryGroupToken']
        $ObjectPropertyTable['ProfilePath'] = $LdapAttributeTable['profilePath']
        $ObjectPropertyTable['SamAccountName'] = $LdapAttributeTable['sAMAccountName']
        $ObjectPropertyTable['ScriptPath'] = $LdapAttributeTable['scriptPath']
        $ObjectPropertyTable['ServicePrincipalNames'] = $LdapAttributeTable['servicePrincipalName']
        $ObjectPropertyTable['SID'] = [string] $LdapAttributeTable['objectSID']
        $ObjectPropertyTable['SIDHistory'] = $LdapAttributeTable['sIDHistory']
        $ObjectPropertyTable['SmartcardLogonRequired'] = [bool] ($LdapAttributeTable['userAccountControl'] -band $UserAccountControl_SMARTCARD_REQUIRED)
        $ObjectPropertyTable['State'] = $LdapAttributeTable['st']
        $ObjectPropertyTable['StreetAddress'] = $LdapAttributeTable['streetAddress']
        $ObjectPropertyTable['Surname'] = $LdapAttributeTable['sn']
        $ObjectPropertyTable['Title'] = $LdapAttributeTable['title']
        $ObjectPropertyTable['TrustedForDelegation'] = [bool] ($LdapAttributeTable['userAccountControl'] -band $UserAccountControl_TRUSTED_FOR_DELEGATION)
        $ObjectPropertyTable['TrustedToAuthForDelegation'] = [bool] ($LdapAttributeTable['userAccountControl'] -band $UserAccountControl_TRUSTED_TO_AUTH_FOR_DELEGATION)
        $ObjectPropertyTable['UseDESKeyOnly'] = [bool] ($LdapAttributeTable['userAccountControl'] -band $UserAccountControl_USE_DES_KEY_ONLY)
        $ObjectPropertyTable['UserPrincipalName'] = $LdapAttributeTable['userPrincipalName']

        #output
        $ObjectPropertyTable
    }
}
#endregion