ADO is an acronym for ActiveX Data Objects. ADO provides Active Directory query technology to VBScript (and VB) using the ADSI OLE-DB provider. Searches using ADO are only allowed in the LDAP namespace.

Active Directory searches using ADO are very efficient. The provider retrieves records matching your query criteria in one operation, without the need to bind to many objects. However, the resulting recordset is read-only, so ADO cannot be used to modify Active Directory objects directly. If you need to modify attribute values, you will have to bind to the object.

ADO returns a recordset. Each record in the recordset is a collection of the values of the attributes requested. The attribute values are from the objects that meet the conditions specified by an ADO query. The ADO query string can use either SQL or LDAP syntax. This page only covers the LDAP syntax. See the first link below for examples of SQL syntax queries. See the other links below for alternatives and related topics.

How to use SQL syntax with ADO queries, with some examples.

Alternative methods of using ADO.

How to specify alternate credentials with ADO.

Sort and Filter with disconnected ADO recordsets.

Add Active Directory as a linked server in an SQL Server instance and use the OPENQUERY SQL statement to query Active Directory.

Use Ambiguous Name Resolution in filter clauses to query Active Directory.

Use PowerShell scripts to query Active Directory.

The LDAP query string includes up to 5 clauses, separated by semicolons. The clauses are:

The search base - The ADsPath to start the search, enclosed in angle brackets. For example, to start the search in the Sales OU of the MyDomain.com domain you might use a search base as follows:

"<LDAP://ou=Sales,dc=MyDomain,dc=com>"

The ADsPath can use either the LDAP or GC providers. You would use the GC provider to search for information in other trusted domains, but only attributes replicated to the Global Catalog are available.

The search filter - A clause that specifies the conditions that must be met for records to be included in the resulting recordset. The attribute values for all objects meeting the conditions are included in the recordset. The syntax of the search filter is explained below. An example to filter for all user objects would be:

"(&(objectCategory=person)(objectClass=user))"

The attributes to return - A list of Active Directory attributes separated by commas. Use the LDAP display names of the attributes. An example would be:

"sAMAccountName,displayName,description"

Note that most property methods cannot be returned by ADO. For example, "LastName" is a property method whose value cannot be returned by ADO. The only property methods that can be returned by ADO are "Name" and "ADsPath". Also, the "tokenGroups", "tokenGroupsGlobalAndUniversal", and "tokenGroupsNoGCAcceptable" attributes cannot be retrieved by ADO. These are the only SID syntax operational attributes. If any of these attributes are listed in the attribute clause, the resulting recordset is empty. Other operational attributes, such as "canonicalName" and "modifyTimeStamp" can be retrieved using ADO. For the "tokenGroups" attributes, you must retrieve the "distinguishedName", bind to the corresponding object, then use the GetInfoEx method to load the attribute values into the local property cache.

The search scope - This can be one of three values. "Base" means that only the object represented by the search base is included in the search. No child containers, OU's, or objects (like users) are included. This is used to check for the existence of the base object. You might assign the ADsPath of a user object as the base of a search and use a scope of "base" to check for existence of the user. "OneLevel" means the search only includes immediate children of the base, like the users in an OU. If the base of the search is the ADsPath of an OU, and the filter is to return only organizational unit objects, then a scope of "OneLevel" will return all child OU's of the base, but not the base OU. "Subtree" (the default) means the search includes the base, all children of the base, and the entire Active Directory structure below the search base.

The Range Limits - Specifies which records in a multi-valued attribute are to be returned. This clause is optional, but if it is used, it must be the fourth clause in the query string - between the attribute list and the search scope. As an example, to include records indexed by 0 through 999, you would use:

"Range=0-999"

An example query string, with no Range Limits, would be:

"<LDAP://ou=Sales,dc=MyDomain,dc=com>;(objectCategory=computer)" _
    & ";sAMAccountName;Subtree"

An example with Range Limits would be:

"<LDAP://cn=Users,dc=MyDomain,dc=com>;(objectCategory=group)" _
    & ";member;Range=0-999;Base"

Only the Base and Attribute clauses are required. If there is no Filter clause, use two semicolons between the Base and Attribute clauses. The recordset will include all objects specified by the Base and Scope clauses. If there is no Scope clause, the search scope defaults to Subtree. A simple query string to return the Distinguished Names of all objects in Active Directory would be:

"<LDAP://dc=MyDomain,dc=com>;;distinguishedName"

The only part of the query string that is case sensitive is the LDAP or GC provider name, which must be in all capitals, and any Boolean values (either TRUE or  FALSE), which must also be in all capitals. The query string is assigned to the "CommandText" property of the ADO Command object. An ADO Connection object specifies the provider used to connect to Active Directory. The Execute method of the Command object executes the query and returns a Recordset object. See the link above for alternative methods to retrieve recordsets using ADO.

Several properties of the ADO command object can be assigned values to make the query more efficient. In particular, you can assign a value to the "Page Size" property. This specifies the number of rows of the Recordset object that are retrieved at one time. If no value is assigned, a maximum of 1000 rows will be retrieved. You can assign any value to  "Page Size" up to 1000. This turns on paging, which means that ADO retrieves the number of rows you specify repeatedly until all rows are retrieved, no matter how many there are. It has been found that it makes very little difference what value you assign, as long as you assign a value so that paging is enabled.

You enumerate the records in the Recordset object in a loop. For example, a complete program to retrieve the sAMAccountName and cn attributes of all user objects in the domain is shown below. To make this example more generic, the RootDSE object is used to retrieve the default naming context, which is the DNS name of the domain the computer has authenticated to. You could hard code the Distinguished Name of the domain instead.

Option Explicit
Dim adoCommand, adoConnection, strBase, strFilter, strAttributes
Dim objRootDSE, strDNSDomain, strQuery, adoRecordset, strName, strCN

' Setup ADO objects.
Set adoCommand = CreateObject("ADODB.Command")
Set adoConnection = CreateObject("ADODB.Connection")
adoConnection.Provider = "ADsDSOObject"
adoConnection.Open "Active Directory Provider"
Set adoCommand.ActiveConnection = adoConnection

' Search entire Active Directory domain.
Set objRootDSE = GetObject("LDAP://RootDSE")

strDNSDomain = objRootDSE.Get("defaultNamingContext")
strBase = "<LDAP://" & strDNSDomain & ">"

' Filter on user objects.
strFilter = "(&(objectCategory=person)(objectClass=user))"

' Comma delimited list of attribute values to retrieve.
strAttributes = "sAMAccountName,cn"

' Construct the LDAP syntax query.
strQuery = strBase & ";" & strFilter & ";" & strAttributes & ";subtree"
adoCommand.CommandText = strQuery
adoCommand.Properties("Page Size") = 100
adoCommand.Properties("Timeout") = 30
adoCommand.Properties("Cache Results") = False

' Run the query.
Set adoRecordset = adoCommand.Execute

' Enumerate the resulting recordset.
Do Until adoRecordset.EOF
    ' Retrieve values and display.
    strName = adoRecordset.Fields("sAMAccountName").Value
    strCN = adoRecordset.Fields("cn").value
    Wscript.Echo "NT Name: " & strName & ", Common Name: " & strCN
    ' Move to the next record in the recordset.
    adoRecordset.MoveNext
Loop

' Clean up.
adoRecordset.Close
adoConnection.Close

You step through the recordset in a loop, using the MoveNext method of the Recordset object to advance to the next record. If you forget to call the MoveNext method, the "Do Until" loop will never meet the EOF (End Of File) condition and the loop will never end. You retrieve values with the Fields collection of the Recordset object. You specify the name of the attribute you are retrieving with the Fields collection. The Value property of the Fields collection is the default property. In the example above I specified several properties for the Command object. These are not necessary, but can improve performance. It is good practice to close the Recordset and Connection objects when you are done.

The search filter specifies all conditions that must be met for a record to be included in the Recordset. Each condition is in the form of a conditional statement in parentheses, such as "(cn=TestUser)", which has a Boolean result. The general form of a condition is an attribute name and a value separated by an operator, which is usually the equals sign "=". The attribute cannot be operational (also known as constructed), since the values of these attributes are only calculated by the Domain Controller on demand and are not saved in Active Directory. Other operators that can separate attribute names and values are ">=", and "<=" (the operators "<" and ">" are not supported). Conditions can be combined using the following operators.

& - The "And" operator (the ampersand). All conditions operated by "&" must be met in order for a record to be included.

| - The "Or" operator (the pipe symbol). Any condition operated by "|" must be met for the record to be included.

! - The "Not" operator (the exclamation point). The condition must return False to be included.

Conditions can be nested using parenthesis. In addition, you can use the "*" wildcard character in the search filter. However, the wildcard character cannot be used with Distinguished Name attributes (attributes of data type DN), such as the distinguishedName, memberOf, directReports, and managedBy attributes.

If the value in a filter includes any of the following characters, the character must be escaped, since it has special meaning in filters:

* ( ) \

These characters are escaped using the backslash escape character, "\", and the 2 digit ASCII hex equivalent of the character. Replace the "*" character with "\2A", the "(" character with "\28", the ")" with "\29", and the "\" character with "\5C". For example, to find all objects where cn is equal to "James (Jim)" you can use the filter:

(cn=James \28Jim\29)

To find all objects where description is equal to "5 * 3 \ 2" use the filter:

(description=5 \2A 3 \5C 2)

Actually, you can escape any character in this manner. Some search filter examples follow.

To return all user objects with cn (Common Name) beginning with the string "Joe":

"(&(objectCategory=person)(objectClass=user)(cn=Joe*))"

To return all user objects. This filter is more efficient than the one using both objectCategory and objectClass, but is harder to remember:

"(sAMAccountType=805306368)"

To return all computer objects with no entry for description:

"(&(objectCategory=computer)(!description=*))"

To return all user and contact objects:

"(objectCategory=person)"

To return all group objects with any entry for description:

"(&(objectCategory=group)(description=*))"

To return all groups with cn starting with either "Test" or "Admin":

"(&(objectCategory=group)(|(cn=Test*)(cn=Admin*)))"

To return all objects with Common Name "Jim * Smith":

"(cn=Jim \2A Smith)"

To retrieve the object with GUID = "90395FB99AB51B4A9E9686C66CB18D99":

"(objectGUID=\90\39\5F\B9\9A\B5\1B\4A\9E\96\86\C6\6C\B1\8D\99)"

To return all users with "Password Never Expires" set:

"(&(objectCategory=person)(objectClass=user)" _
    & "(userAccountControl:1.2.840.113556.1.4.803:=65536))"

To return all users with disabled accounts:

"(&(objectCategory=person)(objectClass=user)" _
    & "(userAccountControl:1.2.840.113556.1.4.803:=2))"

To return all distribution groups:

"(&(objectCategory=group)" _
    & "(!groupType:1.2.840.113556.1.4.803:=2147483648))"

To return all users with "Allow access" checked on the "Dial-in" tab of the user properties dialog of Active Directory Users & Computers. This is all users allowed to dial-in. Note that "TRUE" is case sensitive:

"(&(objectCategory=person)(objectClass=user)" _
    & "(msNPAllowDialin=TRUE))"

To return all user objects created after a specified date (09/01/2007):

"(&(objectCategory=person)(objectClass=user)" _
    & "(whenCreated>=20070901000000.0Z))"

To return all users that must change their password the next time they logon:

"(&(objectCategory=person)(objectClass=user)" _
    & "(pwdLastSet=0))"

To return all users that changed their password since 2/5/2004. See the link below for a function to convert a date value to an Integer8 (64-bit) value. The date 2/5/2004 converts to the number 127,204,308,000,000,000:

"(&(objectCategory=person)(objectClass=user)" _
    & "(pwdLastSet>=127204308000000000))"

To return all users with the group "Domain Users" designated as their "primary" group:

"(&(objectCategory=person)(objectClass=user)" _
    & "(primaryGroupID=513))"

The group "Domain Users" has the primaryGroupToken attribute equal to 513. To return all users with any group other than "Domain Users" designated as their "primary" group:

"(&(objectCategory=person)(objectClass=user)" _
    & "(!primaryGroupID=513))"

To return all users not required to have a password:

"(&(objectCategory=person)(objectClass=user)" _
    & "(userAccountControl:1.2.840.113556.1.4.803:=32))"

To return all users that are direct members of a specified group. You must specify the Distinguished Name of the group. Wildcards are not allowed:

"(&(objectCategory=person)(objectClass=user)" _
    & "(memberOf=cn=TestGroup,ou=Sales,dc=MyDomain,dc=com))"

To return all computers that are not Domain Controllers:

"(&(objectCategory=Computer)" _
    & "(!userAccountControl:1.2.840.113556.1.4.803:=8192))"

There is a new filter, called LDAP_MATCHING_RULE_IN_CHAIN, but it is only available if your Active Directory is installed on Windows 2003 SP2 or Windows 2008 (or above). This filter can only be used with DN attributes, like member or memberOf, but walks the hierarchical chain of objects to reveal nesting. For example, to find all groups that a specific user is a member of, even due to group nesting:

"(member:1.2.840.113556.1.4.1941:=cn=Jim Smith,ou=Sales,dc=MyDomain,dc=com)"

Or to find all members of a specified group, even due to group nesting:

"(memberOf:1.2.840.113556.1.4.1941:=cn=Test Group,ou=West,dc=MyDomain,dc=com)"

To return all user accounts that do not expire. The value of the accountExpires attribute can be either 0 or 2^63-1:

"(&(objectCategory=person)(objectClass=user)" _
    & "(|(accountExpires=9223372036854775807)(accountExpires=0)))"

See the link below for a program that converts a date time value to the equivalent Integer8 (64-bit) value. This program converts the date 2/5/2004 to the equivalent Integer8 value of 127204308000000000 (depending on your time zone, and whether daylight savings time is in affect).

DateToInteger8.txt

When your filter clause includes objectCategory or objectClass, ADO does some magic to convert the values for your convenience. For example, the usual filter for all user objects is:

"(&(objectCategory=person)(objectClass=user))"

But of course, the objectCategory attribute never has the value "person". In reality, the filter should be:

"(&(objectCategory=cn=person,cn=Schema,cn=Configuration,dc=MyDomain,dc=com)" _
    & "(objectClass=user))"

In fact, you can filter on objectCategory equal to "user", which is not really possible, but ADO will deal with it. The following table documents the result of ADO converting several filter combinations:

objectCategory objectClass Result
person user user objects
person user and contact objects
person contact contact objects
user user and computer objects
computer computer objects
user user and contact objects
contact contact objects
computer computer objects
person user, computer, and contact objects
contact user and contact objects
group group objects
group group objects
person organizationalPerson user and contact objects
organizationalPerson user, computer, and contact objects
organizationalPerson user and contact objects

I would recommend using the filter that makes your intent most clear. Also, if you have a choice between using objectCategory and objectClass, it is recommended that you use objectCategory. That is because objectCategory is both single valued and indexed, while objectClass is multi-valued and not indexed (except on Windows Server 2008). A query using a filter with objectCategory will be more efficient than a similar filter with objectClass. Windows Server 2008 domain controllers have a special behavior that indexes the objectClass attribute. You can take advantage of this if all of your domain controllers are Windows Server 2008, or if you specify a Windows Server 2008 domain controller in your query.

You can use the program linked below to experiment with various filters in your domain. The program prompts for the base of the ADO query, the LDAP syntax filter, and the comma delimited list of attribute values to retrieve. The program displays the values of the specified attributes for all objects matching the specified filter in the specified base (and child containers).

GenericADO.htm

Most Active Directory attributes have string values, so you can echo the values directly, or assign the values to variables. Some Active Directory attributes are not single-valued strings. Multi-valued attributes are returned by ADO as arrays. Examples include the attributes memberOf, directReports, otherHomePhone, and objectClass. In these cases, the Value property of the Fields collection will be Null if there are no values in the multi-valued attribute, and will be an array if there is one or more values. For example, if the list of attributes includes the sAMAccountName and memberOf attributes, you could enumerate the Recordset object with a loop similar to:

Do Until adoRecordset.EOF
    strName = adoRecordset.Fields("sAMAccountName").Value
    Wscript.Echo "User: " & strName
    arrGroups = adoRecordset.Fields("memberOf").Value
    If IsNull(arrGroups) Then
        Wscript.Echo "-- No group memberships"
    Else
        For Each strGroup In arrGroups
            Wscript.Echo "-- Member of group: " & strGroup
        Next
    End If
    adoRecordset.MoveNext
Loop


It should be pointed out that the "description" attribute of user objects is actually multi-valued. However, it can only have one value. It is treated as a normal string by ADSI, but not by ADO. ADO returns either a Null (if the "description" attribute has no value) or an array of one string value. You must use code similar to above for this attribute.

If any attribute value can be missing, be sure to account for the possibility that a Null is retrieved from the recordset. An error will be raised if you attempt to echo a Null. For example, if you retrieve the value of the displayName attribute of all objects in Active Directory, many objects will not have this attribute. Other objects will have the attribute, but no value will be assigned. Neither of these situations will raise an error when the recordset is retrieved, but you may need to convert the Null value into a string to avoid a type mismatch error. For example:

Do Until adoRecordset.EOF
    ' The displayName attribute may not have a value assigned.
    ' Appending a blank string, "", converts a Null into a blank string.
    strName = adoRecordset.Fields("displayName").Value & ""
    Wscript.Echo strName
Loop

Other Active Directory attributes are Integer8. This means that they are 64-bit (8 byte) values, usually representing dates. These must be treated using the techniques at this link - Integer8 Attributes. For example, the pwdLastSet attribute is Integer8. If you use ADO to retrieve Integer8 attribute values, the following code will not invoke the IADsLargeInteger interface and will raise an error:

Do Until adoRecordset.EOF
    ' This does not invoke the IADsLargeInteger interface.
    Set objDate = adoRecordset.Fields("pwdLastSet")
    ' This statement raises an error.
    lngHigh = objDate.HighPart
    ' Likewise, the Intger8Date function, documented in the
    ' link above, raises an error.
    dtmDate = Integer8Date(objDate, lngTZBias)
    adoRecordset.MoveNext
Loop

You must either specify the Value property of the Field object and use the Set keyword:

Do Until adoRecordset.EOF
    ' Specify the Value property of the Field object.
    Set objDate = adoRecordset.Fields("pwdLastSet").Value
    ' Invoke methods of the IADsLargeInteger interface directly.
    lngHigh = objDate.HighPart
    ' Or use the Integer8Date function documented in the link above.
    dtmDate = Integer8Date(objDate, lngTZBias)
    adoRecordset.MoveNext
Loop

Or, you must assign the value to a variant, and then use the Set keyword to invoke the IADsLargeInteger interface:

Do Until adoRecordset.EOF
    ' Assign the value to a variant.
    lngDate = adoRecordset.Fields("pwdLastSet")
    ' Use the Set keyword to invoke the IADsLargeInteger interface.
    Set objDate = lngDate
    ' Invoke methods of the IADsLargeInteger interface directly.
    lngHigh = objDate.HighPart
    ' Or use the Integer8Date function documented in the link above.
    dtmDate = Integer8Date(objDate, lngTZBias)
    adoRecordset.MoveNext
Loop

Some attributes are Boolean, such as msNPAllowDialin and IsDeleted. If you retrieve the value of such an attribute, it will be either True or False. For example:

Do Until adoRecordset.EOF
    strName = adoRecordset.Fields("sAMAccountName").Value
    blnAllow = adoRecordset.Fields("msNPAllowDialin").Value
    If (blnAllow = True) Then
        Wscript.Echo "User " & strName & " is allowed to dial in"
    End If
    adoRecordset.MoveNext
Loop

If you use ADO to retrieve the Distinguished Names of objects, all characters that must be escaped will be properly escaped, with the exception of any forward slash "/" characters. This should be rare, but if you attempt to bind to the corresponding object, an error will be raised if the forward slash is not escaped with the backslash escape character "\". For more on characters that need to be escaped, see this link:

CharactersEscaped.htm

However, the forward slash is the only character that is not properly escaped when retrieved by ADO. If it is possible for the forward slash character to be found in a Distinguished Name, use code similar to this example:

Do Until adoRecordset.EOF
    ' Retrieve user Distinguished Name from recordset.
    strUserDN = adoRecordset.Fields("distinguishedName").Value

    ' Escape any "/" characters with backslash escape character.
    ' All other characters that need to be escaped will be escaped.
    strUserDN = Replace(strUserDN, "/", "\/")

    ' Bind to the user object in Active Directory with the LDAP provider.
    Set objUser = GetObject("LDAP://" & strUserDN)

    adoRecordset.MoveNext
Loop

Finally, some attributes are OctetString, which is a byte array. The array must be converted to a hexadecimal string before it can be displayed. Examples include logonHours, and objectGUID. For an example of a function to convert OctetString values to a hexadecimal string, see this program - IsMember Function 8.

For a complete discussion of ADO and searching Active Directory see the following links:

http://www.microsoft.com/resources/documentation/windows/2000/server/scriptguide/en-us/sas_ads_emwf.mspx

http://www.microsoft.com/resources/documentation/windows/2000/server/scriptguide/en-us/sas_ads_jgtf.mspx

http://www.microsoft.com/resources/documentation/windows/2000/server/scriptguide/en-us/sas_ads_shpc.mspx

http://support.microsoft.com/kb/187529/en-us

http://msdn2.microsoft.com/en-us/library/Aa746471.aspx

http://msdn2.microsoft.com/en-us/library/Aa746475.aspx