Ldap.psm1


function Expand-Collection {
    # Simple helper function to expand a collection into a PowerShell array.
    # The advantage to this is that if it's a collection with a single element,
    # PowerShell will automatically parse that as a single entry.
    [CmdletBinding()]
    param(
        [Parameter(Mandatory,
                   Position = 0,
                   ValueFromPipeline,
                   ValueFromRemainingArguments)]
        [ValidateNotNull()]
        [Object[]] $InputObject
    )

    process {
        foreach ($i in $InputObject) {
            ForEach-Object -InputObject $i -Process { Write-Output $_ }
        }
    }
}

function Send-LdapRequest {
    [CmdletBinding()]
    [OutputType([System.DirectoryServices.Protocols.DirectoryResponse])]
    param(
        [Parameter(Mandatory)]
        [ValidateNotNull()]
        [System.DirectoryServices.Protocols.LdapConnection] $LdapConnection
        ,

        [Parameter(Mandatory)]
        [ValidateNotNull()]
        [System.DirectoryServices.Protocols.DirectoryRequest] $Request
        ,

        [Parameter()]
        [System.TimeSpan] $Timeout
    )

    end {
        if ($Timeout) {
            $LdapConnection.SendRequest($Request, $Timeout) | Write-Output
        }
        else {
            $LdapConnection.SendRequest($Request)
        }
    }
}

function Get-LdapConnection {
    [CmdletBinding()]
    [OutputType([System.DirectoryServices.Protocols.LdapDirectoryIdentifier])]
    param(
        [Parameter(Mandatory)]
        [String] $Server,

        # LDAP port to use. Default is 389 for LDAP or 636 for LDAPS
        [Parameter()]
        [Int] $Port,

        # Do not use SSL
        [Parameter()]
        [Switch] $NoSsl,

        # Ignore certificate validation (use with self-signed certs)
        [Parameter()]
        [Switch] $IgnoreCertificate,

        # Version of the LDAP protocol to use
        [Parameter()]
        [Int] $ProtocolVersion,

        # Specify how the LDAP library follows referrals
        [Parameter()]
        [System.DirectoryServices.ReferralChasingOption] $ReferralChasing,

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

        [Parameter()]
        [System.DirectoryServices.Protocols.AuthType] $AuthType
    )

    process {
        $ldapIdentifier = New-Object -TypeName System.DirectoryServices.Protocols.LdapDirectoryIdentifier -ArgumentList $Server, $Port

        if ($Credential) {
            Write-Debug "[Get-LdapConnection] Creating authenticated LdapConnection for user $($Credential.UserName)"
            $ldap = New-Object -TypeName System.DirectoryServices.Protocols.LdapConnection -ArgumentList $ldapIdentifier, ($Credential.GetNetworkCredential())
            if (-not $AuthType) {
                Write-Debug "[Get-LdapConnection] AuthType was not specified; defaulting to Basic"
                $AuthType = [System.DirectoryServices.Protocols.AuthType]::Basic
            }
        }
        else {
            Write-Debug "[Get-LdapConnection] Creating anonymous LdapConnection"
            $ldap = New-Object -TypeName System.DirectoryServices.Protocols.LdapConnection -ArgumentList $ldapIdentifier
            if (-not $AuthType) {
                Write-Debug "[Get-LdapConnection] AuthType was not specified; defaulting to Anonymous"
                $AuthType = [System.DirectoryServices.Protocols.AuthType]::Anonymous
            }
        }

        $ldap.AuthType = $AuthType

        if ($NoSsl) {
            Write-Debug "[Get-LdapConnection] NoSsl was sent; not setting SSL"
        }
        else {
            $ldap.SessionOptions.SecureSocketLayer = $true
        }

        if ($IgnoreCertificate) {
            $ldap.SessionOptions.VerifyServerCertificate = { $true }
        }

        if ($ProtocolVersion) {
            $ldap.SessionOptions.ProtocolVersion = $ProtocolVersion
        }

        if ($ReferralChasing) {
            $ldap.SessionOptions.ReferralChasing = $ReferralChasing
        }

        Write-Output $ldap
    }
}

function Get-LdapObject {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [ValidateNotNull()]
        [System.DirectoryServices.Protocols.LdapConnection] $LdapConnection,

        [Parameter(ParameterSetName = 'DistinguishedName',
            Mandatory)]
        [String] $Identity,

        [Parameter(ParameterSetName = 'LdapFilter',
            Mandatory)]
        [Alias('Filter')]
        [String] $LdapFilter,

        [Parameter(ParameterSetName = 'LdapFilter',
            Mandatory)]
        [String] $SearchBase,

        [Parameter(ParameterSetName = 'LdapFilter')]
        [System.DirectoryServices.Protocols.SearchScope] $Scope = [System.DirectoryServices.Protocols.SearchScope]::Subtree,

        [Parameter()]
        [String[]] $Property,

        [Parameter()]
        [ValidateSet('String', 'ByteArray')]
        [String] $AttributeFormat = 'String',

        [Parameter()]
        [int] $PageSize,

        [Parameter()]
        [uint32] $TimeoutSeconds,

        # Do not attempt to clean up the LDAP output - provide the output as-is
        [Parameter()]
        [Switch] $Raw
    )

    begin {
        if ($AttributeFormat -eq 'String') {
            $attrType = [string]
        }
        else {
            $attrType = [byte[]]
        }
    }

    process {
        $request = New-Object -TypeName System.DirectoryServices.Protocols.SearchRequest

        if ($PSCmdlet.ParameterSetName -eq 'DistinguishedName') {
            $request.DistinguishedName = $Identity
        }
        else {
            $request.Filter = $LdapFilter
            $request.DistinguishedName = $SearchBase
        }

        if (-not $Property -or $Property -contains '*') {
            Write-Debug "[Get-LdapObject] Returning all properties"
        }
        else {
            foreach ($p in $Property) {
                [void] $request.Attributes.Add($p)
            }
        }

        # Declare this outside of the below If block to support Strict mode
        $pageControl = $null
        if ($PageSize) {
            $pageControl = New-Object -TypeName System.DirectoryServices.Protocols.PageResultRequestControl -ArgumentList $PageSize
            [void] $request.Controls.Add($pageControl)
        }

        Write-Debug "[Get-LdapObject] Sending LDAP request"
        $splat = @{
            'LdapConnection' = $LdapConnection
            'Request'        = $request
        }

        if ($TimeoutSeconds) {
            $splat['Timeout'] = [System.TimeSpan]::FromSeconds($TimeoutSeconds)
        }

        # Again, we need to define the variable outside the scope of the do/until loop
        # or else Strict mode will yell at us
        $hasMore = $false
        do {
            # Stop after this run unless we explicitly say otherwise
            $hasMore = $false

            # Since we are sending a SearchRequest, we will get a SearchResponse back
            [System.DirectoryServices.Protocols.SearchResponse] $response = Send-LdapRequest @splat

            if (-not $response) {
                Write-Verbose "No response was returned from the LDAP server."
            }
            elseif ($response.ResultCode -ne 'Success') {
                Write-Warning "The LDAP server returned response code $($response.ResultCode) instead of Success"
                Write-Output $response
            }
            else {
                if ($Raw) {
                    Write-Debug "[Get-LdapObject] -Raw was passed; outputting raw directory entries"
                    Write-Output ($response.Entries)
                }
                else {
                    # Convert results to a PSCustomObject.
                    $response.Entries | ForEach-Object {
                        $hash = [Ordered] @{
                            PSTypeName        = 'LdapObject'
                            DistinguishedName = $_.DistinguishedName
                        }

                        # Attributes are returned as an instance of the class
                        # System.DirectoryServices.Protocols.SearchResultAttributeCollection,
                        # which is a collection of DirctoryAttribute objects.
                        #
                        # DirectoryAttribute extends CollectionBase, which PowerShell can iterate
                        # through using ForEach-Object, but PowerShell doesn't automatically
                        # expand them the way it handles IEnumerables.
                        #
                        # The ForEach-Object here iterates through the values in the
                        # DirectoryEntry and converts them to an IEnumerable instance, which is
                        # much more PowerShell-friendly.

                        foreach ($a in $_.Attributes.Keys | Sort-Object) {
                            $hash[$a] = $_.Attributes[$a].GetValues($attrType) | ForEach-Object { $_ }
                        }

                        [PSCustomObject] $hash
                    } | Write-Output
                }

                # If we're paging, see if we need to return another page
                if ($PageSize) {
                    [System.DirectoryServices.Protocols.PageResultResponseControl] $pageResult = $response.Controls |
                        Where-Object {$_ -is [System.DirectoryServices.Protocols.PageResultResponseControl]} |
                        Select-Object -First 1   # There should only be one, but this is defensive programming

                    if (-not $pageResult) {
                        Write-Warning "No paging controls were returned from the LDAP server. Results may be incomplete."
                    }
                    elseif ($pageResult.Cookie.Length -eq 0) {
                        # Length of 0 indicates that we've returned all results
                        Write-Debug "[Get-LdapObject] Paging cookie with length of 0 returned; completed paging"
                    }
                    else {
                        # Update the page control in the request with the new paging info in the response
                        Write-Debug "[Get-LdapObject] More paging information was provided ($($pageResult.Cookie))"
                        $pageControl.Cookie = $pageResult.Cookie
                        $hasMore = $true
                    }
                }
            }
        }
        while ($hasMore)
    }
}

function Remove-LdapConnection {
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory,
            Position = 0,
            ValueFromPipeline = $true)]
        [System.DirectoryServices.Protocols.LdapConnection[]] $LdapConnection
        ,

        [Parameter()]
        [Switch] $Force
    )

    process {
        foreach ($l in $LdapConnection) {
            if ($l) {
                if (-not ($Force -or $PSCmdlet.ShouldProcess($l, "Close LDAP connection"))) {
                    Write-Debug "[Remove-LdapConnection] WhatIf mode or user denied prompt; not closing connection [[ $l ]"
                }
                else {
                    Write-Debug "[Remove-LdapConnection] Disposing LdapConnection [$l]"
                    $l.Dispose()
                }
            }
        }
    }
}

Set-StrictMode -Version Latest