Powershell LDAP module: member;range=0-1499 results

Decorative title image PowerShell and Microsoft SCCM logos with servers and arrows representing content cloning between them

All product names, logos, and brands used in this post are property of their respective owners.

I recently stumbled across a nifty PowerShell LDAP module that allows you to programmatically query directories (OpenLDAP, Active Directory, etc.) via the LDAP protocol. The description states “non-AD LDAP environments,” but in the absence of the AD PowerShell module (for example, PowerShell Core on macOS, Linux, Azure Cloud Shell, etc.), the LDAP module happily queries Active Directory as well. And as it turns out, I spend more time reading ADDS than writing to it.

A common thread in my Active Directory data mining is group membership (members) - especially huge AD groups with many members (tens of thousands of users). With the Active Directory PowerShell module (RSAT tools), I use the following:

1
2
3
# Specify a large AD group to query
$group="All-Contoso-Users-Global"
$allmembers = Get-ADGroup $group -Properties member | select-object -expand member

The LDAP module can produce identical results (albeit a little slower and with a caveat) as follows. The snippet is also available as a Gist.

Install the LDAP module

1
Install-Module -Name Ldap

Create an LDAP connection using the Get-LdapConnection cmdlet

1
2
3
4
5
6
7
# Create an Active Directory connection via LDAP, replacing 'CN=binduser,OU=Accounts,DC=ad,DC=contoso,DC=com'
# with a real user in the directory and specifying the user's password when prompted
# See https://github.com/replicaJunction/Ldap/blob/master/docs/en-US/Get-LdapConnection.md
$binduser = 'CN=binduser,OU=Accounts,DC=ad,DC=contoso,DC=com'
$connection = Get-LdapConnection -Server 'ad.contoso.com' -Port 636 -Credential `
              (New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList `
	          $binduser,(Read-Host -AsSecureString -Prompt "Enter password"))

Enumerate members of the group using the Get-LdapObject cmdlet

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# Specify a large AD group and search base to query
$group="All-Contoso-Users-Global"
$searchBase = "DC=ad,DC=contoso,DC=com"
$allmembers = @()

# Get AD group members using LDAP, specifically Get-LdapObject
# See https://github.com/replicaJunction/Ldap/blob/master/docs/en-US/Get-LdapObject.md
$members = Get-LdapObject -LdapConnection $connection -LdapFilter `
           "(&(objectClass=group)(samaccountname=$group))" -SearchBase $searchBase `
	       -Property "member"

This is where things get interesting. The caveat I mentioned is how the LDAP module returns the member attribute for groups with more than 1,500 members. You will notice the module returns an additional member attribute (something like member;range=0-1499) for large AD groups.

PS C:\Temp> Get-LdapObject -LdapConnection $connection -LdapFilter "(&(objectClass=group)(samaccountname=$group))" -SearchBase $searchBase -Property "member"

DistinguishedName                                                                       member member;range=0-1499
-----------------                                                                       ------ -------------------
CN=All-Contoso-Users-Global,OU=Groups,DC=ad,DC=contoso,DC=com        			       {CN=someuser1,OU=Use...

That is a result of the “range tag” feature of the LDAP protocol, documented here. The Microsoft code sample is C++, which was not particularly useful for a PowerShell user like me. But it was not too difficult to implement something similar with PowerShell (using a Do…While loop):

Enumerate ALL members of the group using the Get-LdapObject cmdlet (and range tag)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# Specify a large AD group and search base to query
$group="All-Contoso-Users-Global"
$searchBase = "DC=ad,DC=contoso,DC=com"
$allmembers = @()

# Define default/initial range values, which may vary based on your 
# Active Directory configuration (default is 1500, so 0-1499)
[string]$rangeStart = "0"
[string]$rangeEnd = "1499"
[string]$range = "$rangeStart-$rangeEnd"
[string]$rangeFinal = "$rangeStart-*"

# Get AD group members using LDAP, specifically Get-LdapObject
# See https://github.com/replicaJunction/Ldap/blob/master/docs/en-US/Get-LdapObject.md
$members = Get-LdapObject -LdapConnection $connection -LdapFilter `
           "(&(objectClass=group)(samaccountname=$group))" -SearchBase $searchBase `
	       -Property "member;range=$range"

# Use a loop to increment and enumerate range-tagged results
do {
	# If the range tag member property exists, add the current range to the allmembers array
	if($members."member;range=$range") {
		$allmembers += $members."member;range=$range"
	}
	# If this is the final range, add the current range to the allmembers array and break
	if($members."member;range=$rangeFinal") {
		$allmembers += $members."member;range=$rangeFinal"
		break
	}
	# Increment the range tag and retrieve the next range with Get-LdapObject
	[string]$rangeStart = [int]$rangeEnd+1
	[string]$rangeEnd = [int]$rangeStart+1499
	[string]$range = "$rangeStart-$rangeEnd"
	[string]$rangeFinal = "$rangeStart-*"
	$members = Get-LdapObject -LdapConnection $connection -LdapFilter "(&(objectClass=group)(samaccountname=$group))" `
	           -SearchBase $searchBase -Property "member;range=$range"
} while ($members."member;range=$range" -or $members."member;range=$rangeFinal")

With $allmembers populated, perform whatever tasks you need to

1
2
$allmembers | Sort | Select -First 5
$allmembers | Measure-Object
PS C:\Temp> $allmembers | Sort | Select -First 5
CN=someuser1,OU=Users,DC=ad,DC=contoso,DC=com
CN=someuser2,OU=Users,DC=ad,DC=contoso,DC=com
CN=someuser3,OU=Users,DC=ad,DC=contoso,DC=com
CN=someuser4,OU=Users,DC=ad,DC=contoso,DC=com
CN=someuser5,OU=Users,DC=ad,DC=contoso,DC=com

PS C:\Temp> $allmembers | Measure-Object

Count    : 2371
Average  :
Sum      :
Maximum  :
Minimum  :
Property :

If nothing else, I hope this post serves as a PowerShell example of “range tag” enumeration of LDAP objects. However, the LDAP module itself is worth a look. Especially if you need to query Active Directory from a non-Windows device (Mac, Linux, Cloud Shell, etc.). You can even create functions that mimic the common Get- AD PowerShell module cmdlets (like Get-ADUser, Get-ADGroup, etc.) using LDAP searches…