ADObject.psm1

Import-Module "$PSScriptRoot\Shared\ADHelpers.psm1" -Verbose:$false
Set-StrictMode -Version Latest
$ErrorActionPreference = [Management.Automation.ActionPreference]::Stop


function Get-ADObject {
    <#
    .SYNOPSIS
        Retrieves an LDAP DirectoryEntry.
    .DESCRIPTION
        Retrieves an LDAP DirectoryEntry by their identity, which can be a
        distinguished name, GUID, SID, or sAMAccountName.
    .OUTPUTS
        [System.DirectoryServices.DirectoryEntry]
        # $null if not found.
    #>

    [OutputType([DirectoryServices.DirectoryEntry])]
    [CmdletBinding(DefaultParameterSetName='Filter')]
    param (
        # The ObjectClass to search for.
        [Parameter(Position=0)]
        [string] $Type,

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

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

        # The base path to search within on the given server
        [Parameter()]
        [string] $SearchBase,

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

        # Credentials for the domain controller.
        [Parameter()]
        [PSCredential] $Credential
    )
    begin {
        $searcher = Get-LdapSearcher -SearchBase $SearchBase -Server $Server -Credential $Credential -Verbose:$VerbosePreference
    }
    process {
        if ($Identity) {
            $LDAPFilter = Convert-ADIdentityToFilter -Identity $Identity
        }

        if ($Type) {
            $searcher.Filter = "(&(objectClass=$Type)($LDAPFilter))"
        } else {
            $searcher.Filter = "($LDAPFilter)"
        }
        Write-Verbose "Searching for '$($searcher.Filter)'"
        $searchResult = $searcher.FindAll()
        if ($Identity) {
            $resultCount = $searchResult | Measure-Object | Select-Object -ExpandProperty Count
            if ($resultCount -gt 1) {
                throw [InvalidOperationException]::new("Identity value '$Identity' returned multiple values of class '$Type', which isn't supposed to be possible.")
            }
        }
        foreach ($resultItem in $searchResult) {
            # output
            $resultItem.GetDirectoryEntry()
        }
    }
}


function New-ADObject {
    <#
    .SYNOPSIS
        Creates a new LDAP DirectoryEntry.
    .DESCRIPTION
        Creates a new LDAP DirectoryEntry with the specified name.
    .OUTPUTS
        [System.DirectoryServices.DirectoryEntry] when Passthru is enabled.
    #>

    [OutputType([DirectoryServices.DirectoryEntry])]
    [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName='Path')]
    param (
        # The ObjectClass of the type to create.
        [Parameter(Mandatory, Position=0)]
        [string] $Type,

        # The type of the DistinguishedName component for this new object.
        # Should be CN or OU. Defaults to CN.
        [ValidateSet('CN', 'OU')]
        [Parameter(Position=1)]
        [string] $DistinguishedComponenentType = 'CN',

        # The name of the new entry.
        [Parameter(Mandatory, Position=2, ValueFromPipeline)]
        [string] $Name,

        # Path of the OU or container where the new object is created, in DN form.
        [Parameter()]
        [string] $Path,

        # Path of the OU or container where the new object is created, in DN
        # form *without* the DC components. Used if -Path is not provided.
        [Parameter()]
        [string] $DefaultRelativePath,

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

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

        # Should set sAM Account Name? If not set will default to a GUID.
        [Switch] $DoSAMAccountName
    )
    begin {
        $searcher = Get-LdapSearcher -Server $Server -Credential $Credential -SearchBase $Path -DefaultRelativeBase $DefaultRelativePath -Verbose:$VerbosePreference
        $baseEntry = $searcher.SearchRoot
    }
    process {
        if (-not $baseEntry.distinguishedName) {
            Write-Error "Parent container node '$(if ($Path) { $Path } else { $DefaultRelativePath })' not found."
        }

        $targetSummary = "$Type '$Name' in container '$($baseEntry.distinguishedName)'"
        if ($PSCmdlet.ShouldProcess($targetSummary)) {
            Write-Verbose "$($MyInvocation.MyCommand): $targetSummary"
            $newEntry = $baseEntry.Children.Add("$DistinguishedComponenentType=$Name", $Type)
            if ($DoSAMAccountName) {
                $existing = Get-ADObject -LDAPFilter "sAMAccountName=$Name" -Server $Server -Credential $Credential
                if (($existing | Measure-Object).Count) {
                    # objectClass contains the full class inheritance hierarchy so we only want the final, most-specific entry.
                    $existingClass = $existing.objectClass | Select-Object -Last 1
                    Write-Error "There is already an existing entry '$($existing.distinguishedName)' of type '$($existingClass)'."
                } else {
                    $newEntry.Properties['sAMAccountName'].Value = $Name
                }
            }
            $newEntry.CommitChanges();
            
            #output
            $newEntry
        }
    }
}


function Set-ADObject {
    <#
    .SYNOPSIS
        Modifies an LDAP DirectoryEntry.
    .DESCRIPTION
        Modifies an LDAP DirectoryEntry with the specified properties.
    .OUTPUTS
        [System.DirectoryServices.DirectoryEntry] when Passthru is enabled.
    #>

    [OutputType([DirectoryServices.DirectoryEntry])]
    [CmdletBinding(SupportsShouldProcess)]
    param (
        # The ObjectClass to modify.
        [Parameter(Position=0)]
        [string] $Type,

        # The identity of the LDAP DirectoryEntry to modify.
        [Parameter(Mandatory, ValueFromPipeline, Position=1)]
        [string] $Identity,

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

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

        # Credentials for the domain controller.
        [Parameter()]
        [PSCredential] $Credential = $null
    )
    process {
        if ($PSCmdlet.ShouldProcess($Identity, "Modifying $Type")) {
            $entry = Get-ADObject $Type -Identity $Identity -Server $Server -Credential $Credential
            if ($OtherAttributes) {
                Set-DirectoryEntryPropertyTable $entry $OtherAttributes
                $entry.CommitChanges()
            }
            
            # output
            $entry
        }
    }
}


function Remove-ADObject {
    <#
    .SYNOPSIS
        Removes an LDAP DirectoryEntry.
    .DESCRIPTION
        Removes an LDAP DirectoryEntry by their identity.
    .OUTPUTS
        None
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param (
        # The ObjectClass of the entry to modify.
        [Parameter(Position=0)]
        [string] $Type,

        # The identity of the LDAP DirectoryEntry to remove.
        [Parameter(Mandatory, ValueFromPipeline, Position=1)]
        [string] $Identity,

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

        # Credentials for the domain controller.
        [Parameter()]
        [PSCredential] $Credential
    )
    process {
        if ($PSCmdlet.ShouldProcess($Identity, "Removing $Type")) {
            $entry = Get-ADObject $Type -Identity $Identity -Server $Server -Credential $Credential
            if (($entry | Measure-Object).Count -eq 1) {
                Write-Verbose "Removing $Type '$($entry.distinguishedName)'."
                $entry.DeleteTree()

            } elseif (-not $entry) {
                Write-Error "Could not find $Type '$Identity', cannot remove."
            } else {
                Write-Error "Multiple entries of type $Type found matching identity '$Identity', cannot remove."
            }
        }
    }
}


function Test-ADObject {
    <#
    .SYNOPSIS
        Tests if an LDAP DirectoryEntry exists.
    .DESCRIPTION
        Tests if an LDAP DirectoryEntry exists by their identity.
    .OUTPUTS
        [bool]
    #>

    [OutputType([bool])]
    [CmdletBinding()]
    param (
        # The ObjectClass of the entry to test.
        [Parameter(Position=0)]
        [string] $Type,

        # The identity of the LDAP DirectoryEntry to test.
        [Parameter(Mandatory, ValueFromPipeline, Position=1)]
        [string] $Identity,

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

        # Credentials for the domain controller.
        [Parameter()]
        [PSCredential] $Credential = $null
    )
    process {
        $entry = Get-ADObject $Type -Identity $Identity -Server $Server -Credential $Credential
        
        # output
        $null -ne $entry
    }
}