Printout Header
RSS Feed

Searching for LDAP Directory Objects with ADO


If you know the LDAP path of an object you want to read or manipulate, it is no problem to access this object: This can be done with a simple bind operation. Even if you only know the OU or the container in which the object is stored, you can go quickly through the container content until you find your object.

It is by far more difficult to search objects in the directory

       - with specific search criteria and
       - recursively in all sub container under a certain base container in the directory.

This is for Active Directory and also for any other LDAP directory possible with no problems. All you need for this task is a particular search and access interface, the ActiveX Data Objects (ADO).

The following topics are available:


Activex Data Objects (ADO)
 
Preparing the directory search
Performing the directory search
Using the results
Attributes as search results
Searching the entire forest (GC search)
Paged Results - max number of result entries
 
Configuring the server pagesize (Active Directory)
Configuring the server pagesize (Exch55)
 
Example search - Active Directory
Example search - Exchange 5.5




Active Data Objects (ADO)


The term ADO (often referred to as ADODB) represents an programmers interface, which can be used to access databases. This access can be performed in VB scripts, but also in other programming languages. You can target almost every common database system, like Microsoft Access, SQL server, MySQL, Oracle or Informix - and directory databases as well, when they are accessible with LDAP.

ADO is a part of the Microsoft Data Access Components (MDAC), which can be used as an integral part of the operating system since Windows 2000. So you can test the samples in this topic without having to install special DLLs. Newer version of MDAC can be found here: Download page for Microsoft Data Access Components (MDAC).

The most important information about the ActiveX Data Objects can be found on the Microsoft web site, also:

MSDN: ADO Reference - ActiveX Data Objects
Technet: Searching Active Directory
MSDN: Searching the directory with ADO

Furthermore, the Microsoft knowledge base article Q183606 offers a quite good overview and a short FAQ list concerning ADO.

In ADO connections, the access to the different database types is realized with so-called ADO 'providers'. For the ADSI access to an LDAP directory, there is a special ADI provider named 'DSDSOObject'. This provider allows the user to pass the logon credentials and permits a filtered search with standard LDAP filters and offers above all the hierarchical search in directory substructures, for example in a complete OU subtree.

Because MDAC and therefore also ADO is an integral part of the operating system since Windows 2000, you can try the following examples easily without having to install some DLLs.


Preparing the directory search


First of all, we have to prepare an ADO object which uses the ADSSDOObject provider. With this object we can configure all the necessary logon credentials. The parameter 'Encrypt Password' ensures that a kerberos authentication with encrypted user name and password is performed against a domain controller wit Windows 2000 or above.


Set ado = CreateObject("ADODB.Connection") 'create new ADO connection ado.Provider = "ADSDSOObject" 'use the ADSI interface ado.Properties("User ID") = LogonName 'pass credentials - if you omit this, the search is performed.... ado.Properties("Password") = Password '... with the current credentials ado.Properties("Encrypt Password") = True 'only needed if you set "User ID" and "Password" ado.Open "connection-name" 'use any name for this connection

For a logon name you can use all kinds of notation which are described in the regarding topic in the SelfADSI tutorial about the LDAP bind operation.

If you omit the credentials in the parameters "User ID" and "Password" by simply dropping the appropriate lines in the script, the search will be performed with the currently logged on user ID - needless to say that you must have the permission on the regarding LDAP directory in this case.

If you use explicite logon credentials and omit "Encrypt Password" = True, then the user data and LDAP requests are sent unencrypted over the network - this is not advisable. However, if you perform a search in another LDAP directory than Active Directory, often a Simple Bind ("Encrypt Password = False resp. the row is omitted entirely) is the only way to communicate with the server in a VBScript.It is best to establish then the connection within an SSL tunnel (LDAPS on port 636).

For a connection name any label can be chosen - he is used for internal system identification purposes only.


< back to top


Performing the directory search


A filtered search is done with the method Execute of the ADO object we created in the last step:


Set objectList = ado.Execute("<LDAP://dc2.cerro.local/ou=test,dc=cerro,dc=local>; LDAPfilter;ADSPath;subtree")

In this search we have to specify the so-called search base: this is the container where the objects are searched for - including in its child containers. You can specified a Active Directory domain also. The search base has always to be a complete LDAP pathname.


Additionally, we have to configure an LDAP filter. This filter determines the criteria which are used for finding objects. The general rules for LDAP filters described here in the tutorial apply accordingly.


Furthermore, we need to specify the object property or attribute which we want to be returned by the search. We will use the property 'ADSPath' in most cases - as a result we receive the LDAP pathnames for the found objects then.


The last parameter 'subtree' effects the search to be recursive in all child containers. Possible values for this parameter would be:


     base:       The search only returns simply the object which was given as the search base (to check if the object exists)

     onelevel: The search only returns objects which are stored directly in the given search base container.

     subtree:   Recursive search in the given search base container and all child containers therein.


Using the results


The ADO object returns the result of the execute method in an array named 'Fields'. The properties we asked for in the search (in our case the LDAP pathname of the located objects) are provided as 'Value' in the each array element.


Now we could for example connect to each object with a standard bind operation and do something with them - doing some info output, changing objects attributes, deleting etc. etc.


WScript.Echo objectList.RecordCount While Not objectList.EOF 'here the requested property ADSPath is used Set object = GetObject(objectList.Fields(0).Value) ... ... ... objectList.MoveNext 'jump to the next search result entry Wend

To get the next search result entry, we have to use the command MoveNext.


If you get no results from your LDAP search against your expectations, and there is not even an error message, then maybe the search requested is limited on the server's side with a MaxPageSize limit. Please read the annotations for this in the following topic Paged Result - Maximal number of result entries.


Attributes as search results


In the 'Execute' call in the ADODB connection we pass as a parameter the property which we want to have returned in the search result. In the preceding explanations we followed the principle of having the LDAP pathname ('ADSPath') as the result parameter. With this LDAP pathname, we can establish a connection to each located object - this was necessary if we actually want to access these objects, for example if we want to change some attribute values.


However, when we only want to display object attributes and don't need to have any other direct access, we can pass all the necessary attributes directly in the "Execute" command. It's easy to request multiple attributes, just separate them with a comma in the execute call string.


An example, in which the display name and the email address of users in a domain is requested directly in the search:


Set objectList = ado.Execute("<LDAP://dc1.cerro.local/dc=cerro,dc=local>;(objectClass=user);displayName,mail;SubTree") While Not objectList.EOF 'now the displayame and mail attributes of the foun objects are shown WScript.Echo objectList.Fields("displayName") & " : " & objectList.Fields("mail") ... ... ... objectList.MoveNext 'jump to the next search result entry Wend

As you see we can use the attribute name as an array access index (objectList.Fields("displayName")) instead of the index number (objectList.Fields(0)).


Searching the entire AD forest


The previous approach for a ADO search for Active Directory objects was to do some LDAP requests to a domain controller over the ADO interface. You have to keep in mind that domain controllers in general store only objects from their own domain - apart from the schema and configuration partition. But if we want to search for 'normal' objects (users, groups, computers etc.) in the entire forest, we have to explicitly arrange for this. Two different approaches are possible:

  1. You get the domain information from the configuration partition. For each domain, you look for the nearest domain controller (with the help of DNS). Then you perform your search for each of these domain controllers separately. This method is certainly very elaborate, because you need to determine a domain controller of every domain in the forest first.

  2. A much easier way: A global catalog search. Objects from the entire forest can be retrieved from a global catalog with one fell swoop. All you need for this is to detect a domain controller which acts as a global catalog. But please be aware that there are not all attributes of the forest objects are available in a global catalog, because in the its database only a subset of important attributes is stored! An ADO search in the global catalog is performed by using the TCP port 3268 and passing the LDAP path of the root domain as a search base. The search scope has always to be 'SubTree'. In the search result, objects from the entire forest will be returned if they match the search criteria:
. . . 'GC search with root domain as search base: adoCmd.CommandText = "<LDAP://gc1.cerro.local:3268/dc=cerro,dc=local>;LDAP-Filter;ADSPath;subtree" Set objectList = adoCmd.Execute 'perform search
Vorsicht: Wenn Sie die Suche in einem Active Directory Forest durchführen, der mehrere DNS Namensräume ("Trees") enthält, dann erhalten sie mit dieser Variante nur diejenigen Objekte, die sich in Child-Domänen der Root aufhalten. Was tun, wenn man wirklich in allen Trees suchen will? In echten LDAP-Suchanfragen würde man hier einfach einen leeren String als Searchbase verwenden, aber in ADSI muß man auf folgenden Trick zurückgreifen (sie müssen dazu allerdings selbst als Benutzer des Forests angemeldet sein!):
Set aoi = CreateObject("ADSystemInfo") 'evaluate GC SearchBase gcBase = aoi.ForestDNSName . . . 'set GC search base as base DN for this search adoCmd.CommandText = "<GC://" & gcBase & ">;LDAP-Filter;Attribut(e);subtree" Set objectList = adoCmd.Execute 'perform search

Given the case that you just want to enumerate all the groups in the forest, then a GC search is exactly what you need. However, when you want to search for all users which have an certain logon script associated, then you have to choose the first method, because the logon script attribute is not stored in the global catalog and no information about it can be retrieved in a GC search.


SelfADSI-Tutorial Chapter regarding the Global Catalog
Microsoft-Annotations regarding LDAP searches in the global catalog (old but still valuable)

Paged Result - Maximal number of result entries


In an LDAP search request you have always to reckon with servers which enforce a limit for the count of search results for a single request. You perform for example a search request for all user objects in an entire OU structure, but you only receive 500 users in the servers answer although there must be over 2000 users in the regarding OU. The server always returns only a limited number of directory entries, no matter how often you perform the search and no matter what interface (for example ADO) you use for searching. Such a limitation on the server's side is called 'MaxPageSize'.


Especially Active Directory domain controllers and Exchange 5.5 servers have an active MaxPageSize limit by default (1000 for Active Directory, 100 for Exchange 5.5). In the next two topics you can read how these limit parameters can be changed centralized on the server side.


But here's another workaround for the MaxPageSize limitations: You can retrieve all possible results of a search even if the server has an active MaxPageSize restriction in place. To achieve this, you have to perform an LDAP search where the search property 'Paged Results' is configured. This is a parameter which instructs the server to give the results in packets, each of them not bigger than the parameters specifies, and that as long until really all the result entries are retrieved by the requesting client. This technique is handled directly within the LDAP protocol (specified in RFC 4511), in your script you only have to ensure that the Paged Result parameter is activated and set accordingly:

The script syntax for a Paged Result search looks like this:


Set ado = CreateObject("ADODB.Connection") 'create new ADO Connection ado.Provider = "ADSDSOObject" 'use the ADSI interface ado.Properties("User ID") = "Anmeldname" 'pass credentials - omit these 2 lines to use your current credentials! ado.Properties("Password") = "Passwort" ado.Properties("Encrypt Password") = True 'only needed if you set "User ID" and "Password" ado.Open "Verbindungs-Name" 'use any name for the connection Set adoCmd = CreateObject("ADODB.Command") 'create new ADO command adoCmd.ActiveConnection = ado 'assignment to an existing ADO connection adoCmd.Properties("Page Size") = 100 'now you can set special search parmeters adoCmd.Properties("Cache Results") = True adoCmd.CommandText = "<LDAP://dc2.cerro.local/ou=test,dc=cerro,dc=local>;LDAPFilter;ADSPath;subtree" Set objectList = adoCmd.Execute 'perform search

Configuring the maximal number of result entries (Active Directory)


In an directory search performed with ADO you have to keep in mind that a Windows domain controller only returns up to 1000 object in a search result per default. This behavior is designed to avoid a denial of service attack, in which normal users (which have read permissions in the directory by default) can overstress a domain controller with massive LDAP searches.


The maximum count of returned search results is configured with the server parameter 'MaxPageResult'. This parameter can be configured with the utility NTDSUTIL. The details are outlined in the Microsoft knowledge base article Q315071. You have to launch NTDSUTIL as an enterprise administrator on a domain controller and you have to enter the following commands then:


ldap policies
connections
connect to server <name of the local domain controller>
quit
set maxpagesize to <new maximum value for search results>
commit changes
quit

Don't forget to use the command commit changes, otherwise the changes don't become operative! By the way: This parameter is a global configuration and changes the behavior of all domain controllers in the entire forest (after AD synchronization took place). There is no reboot of any domain controller necessary.


NTDSUtil Screenshot

In our example i have additionally entered the command show values to check whether the new value was set correctly.


Did you know that these LDAP policies are stored directly in the configuration partition of the directory, namely in this object: cn=Default Query Policy,cn=Query-Policies,cn=Directory Service,cn=Windows NT,cn=Services,cn=Configuration,dc=Forest RootDomain. This object has an attribute named lDAPAdminLimits:


ADSIEdit Screenshot

As you can see, this attribute has the syntax of a multivalued string, in which the parameter values are just stored in a readable ASCII notation.


Configuring the maximal number of result entries (Exchange 5.5)


In an directory search performed with ADO you have to keep in mind that an Exchange 5.5 server only returns up to 100 object in a search result per default. This behavior is designed to avoid a denial of service attack, in which normal users (which have read permissions in the directory by default) can overstress an server with massive LDAP searches.


The maximum count of returned search results is specified with the Exchange admin tool in the configuration of the LDAP protocol (this can be found in the site configuration or in the properties of an server object):


Ex55Admin Screenshot

Example search in the Active Directory


This example searches for all user obejcts, which are Exchange mail recipients (this means: the Exchange alias name exists for this object as the attribute mailNickName) and which are hidden in the address book (attribute msExchHideFromAddressLists has a value of TRUE). The starting point for the search is the domain errotorre.de. We want the script to output the display name of each found user.


'you have to use names and credentials of your own environment here! serverName = "nadrash.cerrotorre.de" baseStr = "dc=cerrotorre,dc=de" userName = "philipp@cerrotorre.de" userPass = "secret" filterStr = "(&(objectclass=user)(mailNickName=*)(msExchHideFromAddressLists=TRUE))" Set ado = CreateObject("ADODB.Connection") 'create new ADO connection ado.Provider = "ADSDSOObject" 'use the ADSI interface ado.Properties("User ID") = userName 'pass credentials - omit these 2 lines to use your current credentials! ado.Properties("Password") = userPass ado.Properties("Encrypt Password") = True 'only needed if you set "User ID" and "Password" ado.Open "AD-Search" 'use any name for the connection Set adoCmd = CreateObject("ADODB.Command") 'create new ADO command adoCmd.ActiveConnection = ado 'assignment to an existing ADO connection adoCmd.Properties("Page Size") = 1000 'set the Paged Results value to 1000 (AD standard) adoCmd.Properties("Cache Results") = True adoCmd.CommandText = "<LDAP://" & serverName & "/" & baseStr & ">;" & filterStr & ";ADsPath;subtree" Set objectList = adoCmd.Execute 'perform search While Not objectList.EOF Set user = GetObject(objectList.Fields("ADsPath")) 'connect to the objects which were found WScript.Echo user.displayName 'output: displayname of these objects objectList.MoveNext 'jump to the next search result entry Wend

By using other LDAP filters you can adust your search acordingly:


. . . filterStr = "(&(objectCategory=person)(objectClass=user))" 'search for all users filterStr = "(objectClass=group)" 'search for all groups filterStr = "(&(objectCategory=person)(objectClass=contact))" 'search for all contacts filterStr = "(msExchangeHomeserverName = " & _ "/o=MAILOrg/ou=First Administrative Group/cn=Configuration/cn=Servers/cn=KUNGUR" 'search for all mailbox user on sServer 'KUNGUR' filterStr = "(msExchHideFromAddressLists=TRUE)" 'search for all hidden recipients filterStr = "(displayName=F*)" 'search for all recipients which displayname 'starts with 'F' . . .

A lot more examples for LDAP filters are described in the SelfADSI topic - Examples for LDAP Filters in AD Environments. Additional important information in the SelfADSI tutorial:

Topic 'LDAP Filters'
Topic 'LDAP Search Factory'
Topic 'Establishing a Connection to the Directory'
Topic 'Attributes for Active Directory Users'
Topic 'Attributes for Active Directory Groups'
Topic 'Attributes for Active Directory Contacts'


Example search in an Exchange 5.5 directory


This example searches for a mailbox, which has the SMTP address 'sandra@cerrotorre.de' as a primary or secondary mail address (the primary adress is the one which the mailbox uses as originators address when sending mail). The primary address is stored in the attribute mail by Exchange 5.5, the secondary addresses are stored in the attribute othermailbox. The starting point for the search is the Exchange 5.5 organisation named CERROMAIL. We want the script to output the display name of each found mailbox.


'you have to use names and credentials of your own environment here! serverName = "kailash.cerrotorre.de" baseStr = "o=CERROMAIL" userName = "CERROTORRE\philipp" userPass = "secret" filterStr = "(|(mail=sandra@cerrotorre.de)(othermailbox=smtp:sandra@cerrotorre.de))" Set ado = CreateObject("ADODB.Connection") 'create new ADO connection ado.Provider = "ADSDSOObject" 'use the ADSI interface ado.Properties("User ID") = userName 'pass credentials - omit these 2 lines to use your current credentials! ado.Properties("Password") = userPass ado.Properties("Encrypt Password") = True ado.Open "EX55-Search" 'use any name for the connection Set adoCmd = CreateObject("ADODB.Command") 'create new ADO command adoCmd.ActiveConnection = ado 'assignment to an existing ADO connection adoCmd.Properties("Page Size") = 99 'set the Paged Results value adoCmd.Properties("Cache Results") = True adoCmd.CommandText = "<LDAP://" & serverName & "/" & baseStr & ">;" & filterStr & ";ADsPath;subtree" Set objectList = adoCmd.Execute 'perform search While Not objectList.EOF Set user = GetObject(objectList.Fields("ADsPath")) 'connect to the objects which were found WScript.Echo mbx.name 'output: displayname of these objects objectList.MoveNext 'jump to the next search result entry Wend

By using other LDAP filters you can adust your search acordingly:

. . . filterStr = "(objectClass=organizationalPerson)" 'search for sall mailboxes filterStr = "(objectClass=groupOfNames)" 'search for sall distribution lists filterStr = "(objectClass=Remote-Address)" 'search for sall custom recipients filterStr = "(Home-MTA=cn=Microsoft MTA,cn=TRANGO,cn=Servers,cn=Configuration,ou=Site1,o=MAIL)" 'search for all MBXs on server 'TRANGO' in 'Site1' filterStr = "(Hide-From-Address-Book=TRUE)" 'search for all hidden recipients, 'needs logon with credentials like "cn=USER,dc=DOM,cn=admin" filterStr = "(cn=F*)" 'search for allrecipients which dsiplayname starts with 'F' . . .

A lot more examples for LDAP filters are described in the SelfADSI topic - Examples for LDAP Filters in Exchange 5.5 Environments. Additional important information in the SelfADSI tutorial:

Topic 'LDAP Filters'.
Topic 'Establishing a Connection to the Directory'.
Topic 'Attributes for Exchange 5.5 Mailboxes'