Home Up Feedback

ADO Search Tips

ADO Alternatives
SQL syntax
Alternate Credentials

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 link at left for examples of SQL syntax queries. 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".

The search scope - This can be one of three values.  "Base" means that only the search base is included in the search. No child containers or OU's are included. "OneLevel" means the search base and any immediate children (sub containers or OU's) are included. "Subtree" (the default) means the search includes the entire Active Directory structure below the search base.

The Range Limits - Specifies which records in a multivalued 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 sAMAccountName (NT name) of all objects in Active Directory would be:

"<LDAP://dc=MyDomain,dc=com;;sAMAccountName"

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, 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.

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"
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, such as "(cn=TestUser)", which has a Boolean result. Each such condition is enclosed in parenthesis. The general form of a condition is an attribute and a value separated by an operator, which is usually the equals sign "=". Other operators that can separate attributes 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 filter includes any attribute whose value has the "*" character, it must be replaced by the equivalent "\2A" to avoid having the "*" interpreted as the wildcard character. The backslash is an escape character and the ASCII hex equivalent for "*" is "2A".

Search filter examples:

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/2002):

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

    & "(whenCreated>=20020901000000.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))"

 

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))"

 

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

 

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, tokenGroups, directReport, 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 sAMAcountName 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.

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 syntax 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 "\". The following characters must be escaped if they appear in an LDAP binding string:

,\/#+<>;"=

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/default.aspx?scid=kb;en-us;187529

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

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

 
Send mail to HilltopLab@RLMueller.Net with questions or comments about this web site.
Copyright © 2002-2007 Richard L. Mueller
Last modified: May 02, 2008