Ado

84
ADO Search Tips 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. SQL Syntax How to use SQL syntax with ADO queries, with some examples. ADO Alternatives Alternative methods of using ADO. Alternate Credentials How to specify alternate credentials with ADO. Disconnected Recordsets Sort and Filter with disconnected ADO recordsets. SQL Distributed Queries

Transcript of Ado

Page 1: Ado

ADO Search Tips

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.

SQL Syntax

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

ADO Alternatives

Alternative methods of using ADO.

Alternate Credentials

How to specify alternate credentials with ADO.

Disconnected Recordsets

Sort and Filter with disconnected ADO recordsets.

SQL Distributed Queries

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

ANR in ADO Searches

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

Query AD with PowerShell

Use PowerShell scripts to query Active Directory.

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

Page 2: Ado

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:

Page 3: Ado

"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 ExplicitDim adoCommand, adoConnection, strBase, strFilter, strAttributesDim objRootDSE, strDNSDomain, strQuery, adoRecordset, strName, strCN

' Setup ADO objects.Set adoCommand = CreateObject("ADODB.Command")

Page 4: Ado

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 = strQueryadoCommand.Properties("Page Size") = 100adoCommand.Properties("Timeout") = 30adoCommand.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.MoveNextLoop

' Clean up.adoRecordset.CloseadoConnection.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.

Page 5: Ado

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:

Page 6: Ado

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

Page 7: Ado

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

Page 8: Ado

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

' DateToInteger8.vbs' VBScript program demonstrating how to convert a datetime value to' the corresponding Integer8 (64-bit) value. The Integer8 value is the' number of 100-nanosecond intervals since 12:00 AM January 1, 1601,' in Coordinated Universal Time (UTC). The conversion is only accurate' to the nearest second, so the Integer8 value will always end in at' least 7 zeros.'' ----------------------------------------------------------------------' Copyright (c) 2004 Richard L. Mueller' Hilltop Lab web site - http://www.rlmueller.net' Version 1.0 - June 11, 2004'' You have a royalty-free right to use, modify, reproduce, and' distribute this script file in any way you find useful, provided that' you agree that the copyright owner above has no warranty, obligations,' or liability for such use.

Option Explicit

Dim dtmDateValue, dtmAdjusted, lngSeconds, str64BitDim objShell, lngBiasKey, lngBias, k

If (Wscript.Arguments.Count <> 1) Then Wscript.Echo "Required argument <DateTime> missing" Wscript.Echo "For example:" Wscript.Echo "" Wscript.Echo "cscript DateToInteger8.vbs ""2/5/2004 4:58:58 PM""" Wscript.Echo "" Wscript.Echo "If the date/time value has spaces, enclose in quotes" Wscript.QuitEnd If

dtmDateValue = CDate(Wscript.Arguments(0))

' Obtain local Time Zone bias from machine registry.' This bias changes with Daylight Savings Time.Set objShell = CreateObject("Wscript.Shell")lngBiasKey = objShell.RegRead("HKLM\System\CurrentControlSet\Control\" _ & "TimeZoneInformation\ActiveTimeBias")If (UCase(TypeName(lngBiasKey)) = "LONG") Then lngBias = lngBiasKeyElseIf (UCase(TypeName(lngBiasKey)) = "VARIANT()") Then lngBias = 0 For k = 0 To UBound(lngBiasKey) lngBias = lngBias + (lngBiasKey(k) * 256^k) NextEnd If

' Convert datetime value to UTC.dtmAdjusted = DateAdd("n", lngBias, dtmDateValue)

' Find number of seconds since 1/1/1601.lngSeconds = DateDiff("s", #1/1/1601#, dtmAdjusted)

' Convert the number of seconds to a string

Page 9: Ado

' and convert to 100-nanosecond intervals.str64Bit = CStr(lngSeconds) & "0000000"Wscript.Echo "Integer8 value: " & str64Bit

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 Resultperson user user objectsperson user and contact objectsperson contact contact objects

user user and computer objectscomputer computer objectsuser user and contact objects

contact contact objectscomputer computer objectsperson user, computer, and contact objects

contact user and contact objectsgroup group objects

group group objectsperson organizationalPerson user and contact objects

organizationalPerson user, computer, and contact objectsorganizationalPerson 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

Page 10: Ado

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

Program to use ADO to query Active Directory for objects meeting specified filter criteria and display the values of specified attributes of the objects found. The program first prompts for the base of the search, which must be the Distinguished Name of a container, organizational unit, or the domain. If you enter nothing, the program will default to search the entire domain. Next the program prompts for the LDAP syntax filter to be used. For example, to retrieve information on all user objects in Active Directory you would enter:

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

Finally, the program prompts for a comma delimited list of attribute values to retrieve. You must specify the LDAP Display Names of the attributes. Operational attributes cannot be retrieved. The program always retrieves the Distinguished Names of the objects and displays this value first. For each object that meets the filter criteria in the base of the search, the program outputs the values of all of the attributes requested. The scope of the query is always subtree, so that the search includes all child OU's and Containers of the base.

The program is designed to be run at a command prompt with the cscript host. The output can be redirected to a text file. If you want the program to output in a comma delimited format that can be read by a spreadsheet program, specify the optional parameter /csv. If you do not use /csv, the program outputs each attribute value on separate lines. If you use /csv, multi-valued attributes are documented with the values delimited by semicolons.

Just about all attributes, other than operational ones (like tokenGroups), can be retrieved. All Integer8 attributes (like pwdLastSet, lastLogon, or lockoutTime) are converted into Long integer values. If the value is large enough to correspond to a date (after about April 4, 1981), the equivalent date value in the local time zone is shown in parentheses. All SID and OctetString attributes are converted into hex strings. In addition, if any OctetString value is recognized as a SID, it is converted into the standard decimal format beginning with the string "S-1-5". If any OctetString value is recognized as a GUID value, it is converted into the standard decimal format enclosed in curly braces. If any attribute is not assigned a value, this is indicated in the output.

The LDAP filter specification assigns special meaning to a few characters. You must use the ASCII hex representation of these characters if they are used in the LDAP filter:

Page 11: Ado

* \2A( \28) \29\ \5CNUL \00

For example, to find all user objects that contain the "*" character anywhere in the Common Name, use the following filter:

(&(objectCategory=person)(objectClass=user)(cn=*\2A*))

No attempt is made to validate the values supplied by the user. An error will be raised if the base of the query is not a valid Distinguished Name of a container, if the filter syntax is incorrect, or if any attribute names are invalid.

GenericADO.txt

' GenericADO.vbs' VBScript program to use ADO to query Active Directory.'' ----------------------------------------------------------------------' Copyright (c) 2009 Richard L. Mueller' Hilltop Lab web site - http://www.rlmueller.net' Version 1.0 - December 11, 2009' Version 1.1 - December 12, 2009' Version 1.2 - December 15, 2009' Version 1.3 - December 31, 2009' Version 1.4 - January 27, 2010 - Option to create csv file.' Version 1.5 - February 6, 2010 - Bug fix.' Version 1.6 - May 23, 2011 - Convert SID and GUID values.'' The program prompts for the DN of the base of the query, the LDAP' syntax filter, and a comma delimited list of attribute values to be' retrieved. Displays attribute values for objects matching filter in' base selected.'' You have a royalty-free right to use, modify, reproduce, and' distribute this script file in any way you find useful, provided that' you agree that the copyright owner above has no warranty, obligations,' or liability for such use.

Option Explicit

Dim adoCommand, adoConnection, strBase, strFilter, strAttributesDim objRootDSE, strBaseDN, strQuery, adoRecordsetDim arrAttributes, k, intCount, strValue, strItem, strTypeDim objValue, lngHigh, lngLow, lngValue, strAttr, dtmValueDim objShell, lngBiasKey, lngBias, dtmDate, blnCSV, strLineDim strMulti, strArg

blnCSV = FalseIf (Wscript.Arguments.Count = 1) Then strArg = Wscript.Arguments(0) Select Case LCase(strArg) Case "/csv" blnCSV = True End SelectEnd If

' Obtain local Time Zone bias from machine registry.Set objShell = CreateObject("Wscript.Shell")lngBiasKey = objShell.RegRead("HKLM\System\CurrentControlSet\Control\" _ & "TimeZoneInformation\ActiveTimeBias")If (UCase(TypeName(lngBiasKey)) = "LONG") Then lngBias = lngBiasKeyElseIf (UCase(TypeName(lngBiasKey)) = "VARIANT()") Then lngBias = 0 For k = 0 To UBound(lngBiasKey) lngBias = lngBias + (lngBiasKey(k) * 256^k) NextEnd IfSet objShell = Nothing

Page 12: Ado

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

' Prompt for base of query.strBaseDN = Trim(InputBox("Specify DN of base of query, or blank for entire domain"))If (strBaseDN = "") Then ' Search entire Active Directory domain. Set objRootDSE = GetObject("LDAP://RootDSE") strBaseDN = objRootDSE.Get("defaultNamingContext")End IfIf (InStr(LCase(strBaseDN), "dc=") = 0) Then Set objRootDSE = GetObject("LDAP://RootDSE") strBaseDN = strBaseDN & "," & objRootDSE.Get("defaultNamingContext") strBaseDN = Replace(strBaseDN, ",,", ",")End IfstrBase = "<LDAP://" & strBaseDN & ">"

' Prompt for filter.strFilter = Trim(InputBox("Enter LDAP syntax filter"))If (Left(strFilter, 1) <> "(") Then strFilter = "(" & strFilterEnd IfIf (Right(strFilter, 1) <> ")") Then strFilter = strFilter & ")"End If

' Prompt for attributes.strAttributes = InputBox("Enter comma delimited list of attribute values to retrieve")strAttributes = Replace(strAttributes, " ", "")strAttr = strAttributesIf (strAttributes = "") Then strAttributes = "distinguishedName"Else strAttributes = "distinguishedName" & "," & strAttributesEnd IfarrAttributes = Split(strAttributes, ",")

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

If (blnCSV = False) Then Wscript.Echo "Base of query: " & strBaseDN Wscript.Echo "Filter: " & strFilter Wscript.Echo "Attributes: " & strAttrElse ' Output header line for csv. strLine = "DN" For k = 1 To UBound(arrAttributes) strLine = strLine & "," & arrAttributes(k) Next Wscript.Echo strLineEnd If

' Run the query.' Trap possible errors.On Error Resume NextSet adoRecordset = adoCommand.ExecuteIf (Err.Number <> 0) Then Select Case Err.Number Case -2147217865 Wscript.Echo "Table does not exist. Base of search not found." Case -2147217900 Wscript.Echo "One or more errors. Filter syntax error." Case -2147467259 Wscript.Echo "Unspecified error. Invalid attribute name." Case Else Wscript.Echo "Error: " & Err.Number Wscript.Echo "Description: " & Err.Description End Select Wscript.QuitEnd IfOn Error GoTo 0

' Enumerate the resulting recordset.intCount = 0Do Until adoRecordset.EOF ' Retrieve values and display. intCount = intCount + 1 If (blnCSV = True) Then strLine = """" & adoRecordset.Fields("distinguishedName").Value & """"

Page 13: Ado

Else Wscript.Echo "DN: " & adoRecordset.Fields("distinguishedName").Value End If For k = 1 To UBound(arrAttributes) strType = TypeName(adoRecordset.Fields(arrAttributes(k)).Value) If (strType = "Object") Then Set objValue = adoRecordset.Fields(arrAttributes(k)).Value lngHigh = objValue.HighPart lngLow = objValue.LowPart If (lngLow < 0) Then lngHigh = lngHigh + 1 End If lngValue = (lngHigh * (2 ^ 32)) + lngLow If (lngValue > 120000000000000000) Then dtmValue = #1/1/1601# + (lngValue/600000000 - lngBias)/1440 On Error Resume Next dtmDate = CDate(dtmValue) If (Err.Number <> 0) Then On Error GoTo 0 If (blnCSV = True) Then strLine = StrLine & ",<Never>" Else Wscript.Echo " " & arrAttributes(k) _ & ": " & FormatNumber(lngValue, 0) _ & " <Never>" End If Else On Error GoTo 0 If (blnCSV = True) Then strLine = strLine & "," & CStr(dtmDate) Else Wscript.Echo " " & arrAttributes(k) _ & ": " & FormatNumber(lngValue, 0) _ & " (" & CStr(dtmDate) & ")" End If End If Else If (blnCSV = True) Then strLine = strLine & ",""" & FormatNumber(lngValue, 0) & """" Else Wscript.Echo " " & arrAttributes(k) _ & ": " & FormatNumber(lngValue, 0) End If End If Else strValue = adoRecordset.Fields(arrAttributes(k)).Value Select Case strType Case "String" If (blnCSV = True) Then strLine = strLine & ",""" & strValue & """" Else Wscript.Echo " " & arrAttributes(k) _ & ": " & strValue End If Case "Variant()" strMulti = "" For Each strItem In strValue If (blnCSV = True) Then If (strMulti = "") Then strMulti = """" & strItem & """" Else strMulti = strMulti & ";""" & strItem & """" End If Else Wscript.Echo " " & arrAttributes(k) _ & ": " & strItem End If Next If (blnCSV = True) Then strLine = strLine & "," & strMulti End If Case "Long" If (blnCSV = True) Then strLine = strLine & ",""" & FormatNumber(strValue, 0) & """" Else Wscript.Echo " " & arrAttributes(k) _ & ": " & FormatNumber(strValue, 0) End If Case "Boolean" If (blnCSV = True) Then strLine = strLine & "," & CBool(strValue) Else Wscript.Echo " " & arrAttributes(k) _ & ": " & CBool(strValue) End If Case "Date" If (blnCSV = True) Then strLine = strLine & "," & CDate(strValue)

Page 14: Ado

Else Wscript.Echo " " & arrAttributes(k) _ & ": " & CDate(strValue) End If Case "Byte()" strItem = OctetToHexStr(strValue) If (Left(strItem, 6) = "010500") Then ' A SID value. If (blnCSV = True) Then strLine = strLine & "," & HexSIDToDec(strItem) Else Wscript.Echo " " & arrAttributes(k) _ & ": " & HexSIDToDec(strItem) End If ElseIf (InStr(UCase(arrAttributes(k)), "GUID") > 0) Then ' A GUID value. If (blnCSV = True) Then strLine = strLine & "," & HexGUIDToDisplay(strItem) Else Wscript.Echo " " & arrAttributes(k) _ & ": " & HexGUIDToDisplay(strItem) End If Else ' Other OctetString value. If (blnCSV = True) Then strLine = strLine & "," & strItem Else Wscript.Echo " " & arrAttributes(k) _ & ": " & strItem End If End If Case "Null" If (blnCSV = True) Then strLine = strLine & ",<no value>" Else Wscript.Echo " " & arrAttributes(k) _ & ": <no value>" End If Case Else If (blnCSV = True) Then strLine = strLine & ",<unsupported syntax>" Else Wscript.Echo " " & arrAttributes(k) _ & ": <unsupported syntax " & TypeName(strValue) & " >" End If End Select End If Next If (blnCSV = True) Then Wscript.Echo strLine End If adoRecordset.MoveNextLoopIf (blnCSV = False) Then Wscript.Echo "Number of objects found: " & CStr(intCount)End If

' Clean up.adoRecordset.CloseadoConnection.Close

Function OctetToHexStr(ByVal arrbytOctet) ' Function to convert OctetString (byte array) to Hex string.

Dim k

OctetToHexStr = "" For k = 1 To Lenb(arrbytOctet) OctetToHexStr = OctetToHexStr _ & Right("0" & Hex(Ascb(Midb(arrbytOctet, k, 1))), 2) Next

End Function

Function HexGUIDToDisplay(ByVal strHexGUID) ' Function to convert GUID value in hex format to display format.

Dim TempGUID, GUIDStr

GUIDStr = Mid(strHexGUID, 7, 2) GUIDStr = GUIDStr & Mid(strHexGUID, 5, 2) GUIDStr = GUIDStr & Mid(strHexGUID, 3, 2) GUIDStr = GUIDStr & Mid(strHexGUID, 1, 2) GUIDStr = GUIDStr & Mid(strHexGUID, 11, 2) GUIDStr = GUIDStr & Mid(strHexGUID, 9, 2) GUIDStr = GUIDStr & Mid(strHexGUID, 15, 2) GUIDStr = GUIDStr & Mid(strHexGUID, 13, 2) GUIDStr = GUIDStr & Mid(strHexGUID, 17)

Page 15: Ado

TempGUID = "{" & Mid(GUIDStr, 1, 8) & "-" & Mid(GUIDStr, 9, 4) _ & "-" & Mid(GUIDStr, 13, 4) & "-" & Mid(GUIDStr, 17, 4) _ & "-" & Mid(GUIDStr, 21, 15) & "}"

HexGUIDToDisplay = TempGUID

End Function

Function HexSIDToDec(ByVal strSID) ' Function to convert most hex SID values to decimal format.

Dim arrbytSID, lngTemp, j

ReDim arrbytSID(Len(strSID)/2 - 1) For j = 0 To UBound(arrbytSID) arrbytSID(j) = CInt("&H" & Mid(strSID, 2*j + 1, 2)) Next

If (UBound(arrbytSID) = 11) Then HexSIDToDec = "S-" & arrbytSID(0) & "-" _ & arrbytSID(1) & "-" & arrbytSID(8)

Exit Function End If

If (UBound(arrbytSID) = 15) Then HexSIDToDec = "S-" & arrbytSID(0) & "-" _ & arrbytSID(1) & "-" & arrbytSID(8)

lngTemp = arrbytSID(15) lngTemp = lngTemp * 256 + arrbytSID(14) lngTemp = lngTemp * 256 + arrbytSID(13) lngTemp = lngTemp * 256 + arrbytSID(12)

HexSIDToDec = HexSIDToDec & "-" & CStr(lngTemp)

Exit Function End If

HexSIDToDec = "S-" & arrbytSID(0) & "-" _ & arrbytSID(1) & "-" & arrbytSID(8)

lngTemp = arrbytSID(15) lngTemp = lngTemp * 256 + arrbytSID(14) lngTemp = lngTemp * 256 + arrbytSID(13) lngTemp = lngTemp * 256 + arrbytSID(12)

HexSIDToDec = HexSIDToDec & "-" & CStr(lngTemp)

lngTemp = arrbytSID(19) lngTemp = lngTemp * 256 + arrbytSID(18) lngTemp = lngTemp * 256 + arrbytSID(17) lngTemp = lngTemp * 256 + arrbytSID(16)

HexSIDToDec = HexSIDToDec & "-" & CStr(lngTemp)

lngTemp = arrbytSID(23) lngTemp = lngTemp * 256 + arrbytSID(22) lngTemp = lngTemp * 256 + arrbytSID(21) lngTemp = lngTemp * 256 + arrbytSID(20)

HexSIDToDec = HexSIDToDec & "-" & CStr(lngTemp)

If (UBound(arrbytSID) > 23) Then lngTemp = arrbytSID(27) lngTemp = lngTemp * 256 + arrbytSID(26) lngTemp = lngTemp * 256 + arrbytSID(25) lngTemp = lngTemp * 256 + arrbytSID(24)

HexSIDToDec = HexSIDToDec & "-" & CStr(lngTemp) End If

End Function

You can also target specific Domain Controllers when you run this  program. You would do this if you are retrieving attributes that are not replicated. For example, you might want to see how the value of the logonCount attribute varies between Domain Controllers. To specify a specific Domain Controller, include the name of the DC in the Base you supply for the query. For example, if you want to search

Page 16: Ado

ou=West,dc=MyDomain,dc=com, and you have a DC called West211, then supply the following for the base of the query:

West211/ou=West,dc=MyDomain,dc=com

An equivalent PowerShell script is linked below.

PSGenericADO.txt

# PSGenericADO.ps1# PowerShell program to use ADO to query Active Directory.## ----------------------------------------------------------------------# Copyright (c) 2011 Richard L. Mueller# Hilltop Lab web site - http://www.rlmueller.net# Version 1.0 - July 30, 2011# Version 1.1 - August 5, 2011 - Handle more SID values.# Version 1.2 - August 14, 2011 - Convert logonHours to local time.# Modify for PowerShell V1.## The program prompts for the DN of the base of the query, the LDAP# syntax filter, and a comma delimited list of attribute values to be# retrieved. Displays attribute values for objects matching filter in# base selected.## You have a royalty-free right to use, modify, reproduce, and# distribute this script file in any way you find useful, provided that# you agree that the copyright owner above has no warranty, obligations,# or liability for such use.

Trap {"Error: $_"; Break;}

$Colon = ":"# Check optional parameter indicating output should be in csv format.$Csv = $FalseIf ($Args.Count -eq 1){ If (($Args[0].ToLower() -eq "/csv") -or ($Args[0].ToLower() -eq "csv")) {$Csv = $True}}

# Retrieve local Time Zone bias from machine registry in hours.# This bias does not change with Daylight Savings Time.$Bias = [Math]::Round((Get-ItemProperty -Path HKLM:\System\CurrentControlSet\Control\TimeZoneInformation).Bias/60)

# Create an array of 168 bytes, representing the hours in a week.$LH = New-Object 'object[]' 168

Function OctetToGUID ($Octet){ # Function to convert Octet value (byte array) into string GUID value. $GUID = "{" + [Convert]::ToString($Octet[3], 16) ` + [Convert]::ToString($Octet[2], 16) ` + [Convert]::ToString($Octet[1], 16) ` + [Convert]::ToString($Octet[0], 16) + "-" ` + [Convert]::ToString($Octet[5], 16) ` + [Convert]::ToString($Octet[4], 16) + "-" ` + [Convert]::ToString($Octet[7], 16) ` + [Convert]::ToString($Octet[6], 16) + "-" ` + [Convert]::ToString($Octet[8], 16) ` + [Convert]::ToString($Octet[9], 16) + "-" ` + [Convert]::ToString($Octet[10], 16) ` + [Convert]::ToString($Octet[11], 16) ` + [Convert]::ToString($Octet[12], 16) ` + [Convert]::ToString($Octet[13], 16) ` + [Convert]::ToString($Octet[14], 16) ` + [Convert]::ToString($Octet[15], 16) + "}" Return $GUID}

Function OctetToHours ($Octet){ # Function to convert Octet value (byte array) into binary string # representing logonHours attribute. The 168 bits represent 24 hours # per day for 7 days, Sunday through Saturday. The values are converted # into local time. If the bit is "1", the user is allowed to logon # during that hour. If the bit is "0", the user is not allowed to logon. For ($j = 0; $j -le 20; $j = $j + 1) { For ($k = 7; $k -ge 0; $k = $k - 1)

Page 17: Ado

{ $m = 8*$j + $k - $Bias If ($m -lt 0) {$m = $m + 168} If ($Octet[$j] -band [Math]::Pow(2, $k)) {$LH[$m] = "1"} Else {$LH[$m] = "0"} } }

For ($j = 0; $J -le 20; $j = $J + 1) { $n = 8*$j If ($j -eq 0) {$Hours = [String]::Join("", $LH[$n..($n + 7)])} Else {$Hours = $Hours + "-" + [String]::Join("", $LH[$n..($n + 7)])} } Return $Hours}

$Searcher = New-Object System.DirectoryServices.DirectorySearcher$Searcher.PageSize = 200$Searcher.SearchScope = "subtree"

# Prompt for base of query.$BaseDN = Read-Host "Enter DN of base of query, or blank for entire domain"If ($BaseDN -eq ""){ # Default to the entire domain. $Base = New-Object System.DirectoryServices.DirectoryEntry}Else{ If ($BaseDN.ToLower().Contains("dc=") -eq $False) { $Domain = New-Object System.DirectoryServices.DirectoryEntry $BaseDN = $BaseDN + "," + $Domain.distinguishedName $BaseDN = $BaseDN.Replace(",,", ",") } $Base = New-Object System.DirectoryServices.DirectoryEntry "LDAP://$BaseDN"}$Searcher.SearchRoot = $Base

# Prompt for LDAP syntax filter.$Filter = Read-Host "Enter LDAP syntax filter"If ($Filter.StartsWith("(") -eq $False) {$Filter = "(" + $Filter}If ($Filter.EndsWith(")") -eq $False) {$Filter = $Filter + ")"}$Searcher.Filter = $Filter

# Prompt for attributes.$Attributes = Read-Host "Enter comma delimited list of attribute values to retrieve"# Remove any spaces.$Attributes = $Attributes -replace " ", ""$arrAttrs = $Attributes.Split(",")$Searcher.PropertiesToLoad.Add("distinguishedName") > $NullForEach ($Attr In $arrAttrs){ If ($Attr -ne "") { $Searcher.PropertiesToLoad.Add($Attr) > $Null }}

If ($Csv -eq $False){ "Base of query: " + $Base.distinguishedName "Filter: $Filter" "Attributes: $Attributes" "----------------------------------------------"}Else{ # Header line. $Line = "DN" ForEach ($Attr In $arrAttrs) { If ($Attr -ne "") { $Line = $Line + "," + $Attr } } $Line}

# Run the query.$Results = $Searcher.FindAll()

# Enumerate resulting recordset.$Count = 0ForEach ($Result In $Results){ $Count = $Count + 1 $DN = $Result.Properties.Item("distinguishedName") If ($Csv -eq $True) { # Any double quote characters in the DN must be doubled. $Line = """" + $DN[0].Replace("""", """""") + """"

Page 18: Ado

} Else { "DN: " + $DN } # Retrieve all requested attributes. ForEach ($Attr In $arrAttrs) { If ($Attr -ne "") { $Values = $Result.Properties.Item($Attr) If ($Values[0] -eq $Null) { # Attribute has no value. If ($Csv -eq $True) {$Line = "$Line,<no value>"} Else {" $Attr$Colon <no value>"} } Else { # Attribute might be multi-valued. Values will be semicolon delimited. # Values will only be quoted if they are String. $Multi = "" $Quote = $False ForEach ($Value In $Values) { Switch ($Value.GetType().Name) { "Int64" { # Attribute is Integer8 (64-bit). If ($Value -gt 9000000000000000000) { # Value is maximum 64-bit value, 2^63 - 1. If ($Csv -eq $True) { If ($Multi -eq "") {$Multi = "<never>"} Else {$Multi = "$Multi;<Never>"} } Else {" $Attr$Colon <never>"} } Else { If ($Value -gt 120000000000000000) { # Integer8 value is a date, greater than # April 07, 1981, 9:20 PM UTC. $Date = [Datetime]$Value If ($Csv -eq $True) { If ($Multi -eq "") { $Multi = $Date.AddYears(1600).ToLocalTime() } Else { $Multi = "$Multi;" ` + $Date.AddYears(1600).ToLocalTime() } } Else { " $Attr$Colon " + '{0:n0}' -f $Value ` + " (" + $Date.AddYears(1600).ToLocalTime() + ")" } } Else { # Integer8 value, not a date. If ($Csv -eq $True) { If ($Multi -eq "") {$Multi = '{0:n0}' -f $Value} Else {$Multi = "$Multi;" + '{0:n0}' -f $Value} } Else {" $Attr$Colon " + '{0:n0}' -f $Value} } } } "Byte[]" { # Attribute is a byte array (OctetString). If (($Value.Length -eq 16) ` -and ($Attr.ToUpper().Contains("GUID") -eq $True)) { # GUID value. If ($Csv -eq $True) { If ($Multi -eq "") {$Multi = $(OctetToGUID $Value)} Else {$Multi = "$Multi;" + $(OctetToGUID $Value)}

Page 19: Ado

} Else {" $Attr$Colon " + $(OctetToGUID $Value)} } Else { If (($Value.Length -eq 21) -and ($Attr -eq "logonHours")) { # logonHours attribute, byte array of 168 bits. # One binary bit for each hour of the week, in UTC. If ($Csv -eq $True) { If ($Multi -eq "") {$Multi = $(OctetToHours $Value)} Else {$Multi = "$Multi;" + $(OctetToHours $Value)} } Else {" $Attr$Colon " + $(OctetToHours $Value)} } Else { If (($Value[0] -eq 1) -and (` (($Value[1] -eq 1) -and ($Value.Length -eq 12)) ` -or (($Value[1] -eq 2) -and ($Value.Length -eq 16)) ` -or (($Value[1] -eq 4) -and ($Value.Length -eq 24)) ` -or (($Value[1] -eq 5) -and ($Value.Length -eq 28)))) { # SID value. $SID = New-Object System.Security.Principal.SecurityIdentifier $Value, 0 If ($Csv -eq $True) { If ($Multi -eq "") {$Multi = $SID} Else {$Multi = "$Multi;$SID"} } Else {" $Attr$Colon $SID"} } Else { # Byte array. If ($Csv -eq $True) { If ($Multi -eq "") {$Multi = $Value} Else {$Multi = "$Multi;$Value"} } Else {" $Attr$Colon $Value"} } } } } "String" { # String value. Enclose in quotes in case there are embedded # commas. Any double quote characters in the string must # be doubled. $Quote = $True If ($Csv -eq $True) { # Embedded quotes must be doubled. $Value = $Value.Replace("""", """""") If ($Multi -eq "") {$Multi = $Value} Else {$Multi = "$Multi;$Value"} } Else {" $Attr$Colon $Value"} } "Int32" { # 32-bit integer. If ($Csv -eq $True) { If ($Multi -eq "") {$Multi = "$Value"} Else {$Multi = "$Multi;$Value"} } Else {" $Attr$Colon $Value"} } "Boolean" { # 32-bit integer. If ($Csv -eq $True) { If ($Multi -eq "") {$Multi = "$Value"} Else {$Multi = "$Multi;$Value"} } Else {" $Attr$Colon $Value"} } "DateTime" { # 32-bit integer. If ($Csv -eq $True) { If ($Multi -eq "") {$Multi = "$Value"}

Page 20: Ado

Else {$Multi = "$Multi;$Value"} } Else {" $Attr$Colon $Value"} } Default { If ($Csv -eq $True) { If ($Multi -eq "") {$Multi = "<not supported> (" + $Value.GetType().Name + ")"} Else {Multi = "$Multi;<not supported> (" + $Value.GetType().Name + ")"} } Else {" $Attr$Colon <not supported> (" + $Value.GetType().Name + ")"} } } } If ($Csv -eq $True) { # Enclose values in double quotes if necessary. If ($Quote -eq $True) {$Line = "$Line,""$Multi"""} Else {$Line = "$Line,$Multi"} } } } } If ($Csv -eq $True) {$Line}}

If ($Csv -eq $False) {"Number of objects found: $Count"}

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

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

Page 21: Ado

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 strNameLoop

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

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

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.

Page 22: Ado

    lngHigh = objDate.HighPart    ' Or use the Integer8Date function documented in the link above.    dtmDate = Integer8Date(objDate, lngTZBias)    adoRecordset.MoveNextLoop

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

Integer8 Attributes

Many attributes in Active Directory have a data type (syntax) called Integer8. These 64-bit numbers (8 bytes) often represent time in 100-nanosecond intervals. If the Integer8 attribute is a date, the value represents the number of 100-nanosecond intervals since 12:00 AM January 1, 1601. Any leap seconds are ignored.

In .NET Framework (and PowerShell) these 100-nanosecond intervals are called ticks, equal to one ten-millionth of a second. There are 10,000 ticks per millisecond. In addition, .NET Framework and PowerShell DateTime values represent dates as the number of ticks since 12:00 AM January 1, 0001.

ADSI automatically employs the IADsLargeInteger interface to deal with these 64-bit numbers. This interface has two property methods, HighPart and LowPart, which break the number up into two 32-bit numbers. The HighPart and LowPart property methods return values between -2^31 and 2^31 - 1. The standard method of handling these attributes is demonstrated by this VBScript program to retrieve the domain lockoutDuration value in minutes.

Set objDomain = GetObject("LDAP://dc=MyDomain,dc=com")' Retrieve lockoutDuration with IADsLargeInteger interface.Set objDuration = objDomain.lockoutDuration' Calculate number of 100-nanosecond intervals.lngDuration = (objDuration.HighPart * (2^32)) + objDuration.Lowpart' Convert to minutes. The value retrieved is negative, so make positive.lngDuration = -lngDuration / (60 * 10000000)Wscript.Echo "Domain policy lockout duration in minutes: " & lngDuration

However, whenever the LowPart method returns a negative value, the calculation above is wrong by 7 minutes, 9.5 seconds. The work-around is to increase the value returned by the HighPart method by one whenever the value returned by the LowPart method is negative. The revised code below gives correct results in all cases.

Page 23: Ado

Set objDomain = GetObject("LDAP://dc=MyDomain,dc=com")' Retrieve lockoutDuration with IADsLargeInteger interface.Set objDuration = objDomain.lockoutDurationlngHigh = objDuration.HighPartlngLow = objDuration.LowPart' Adjust for error in IADsLargeInteger interface.If (lngLow < 0) then    lngHigh = lngHigh + 1End If' Calculate number of 100-nanosecond intervals.lngDuration = (lngHigh * (2^32)) + lngLow' Convert to minutes.lngDuration = -lngDuration / (60 * 10000000)Wscript.Echo "Domain policy lockout duration in minutes: " & lngDuration

The error introduced if this inaccuracy is not accounted for is not large. The error is always 2^32 100- nanosecond intervals, which is 7 minutes, 9.5 seconds. All the programs on this site that deal with Integer8 attributes have been revised as shown on this page to give accurate results.

Integer8 Discussion

Problem with the HighPart and LowPart Property Methods

A good way to demonstrate the problem encountered with these property methods is to use ADSI Edit to update the maxStorage attribute of a test user object.

ADSI Edit is part of the "Windows 2000 Support Tools" found on any Windows 2000 Server CD. It can be installed on any client with Windows 2000 or above by running Setup.exe in the \Support\Tools folder on the CD.

With ADSI Edit you can browse all objects in your Active Directory and the attributes of each object. The maxStorage attribute is used to specify the maximum amount of disk space the user is allowed to use. This attribute is Integer8. ADSI Edit allows you to enter a large 64-bit number as the value for this attribute. The VBScript program below will then reveal the values returned by the HighPart and LowPart property methods exposed by the IADsLargeInteger interface:

strUserDN = "cn=TestUser,ou=Sales,dc=MyDomain,dc=com")Set objUser = GetObject("LDAP://" & strUserDN)Set objMax = objUser.maxStoragelngHigh = objMax.HighPartlngLow = objMax.LowPartlngValue = lngHigh * (2^32) + lngLowWscript.Echo "HighPart: " & lngHighWscript.Echo "LowPart: " & lngLowWscript.Echo "Value: " & lngValue

By selecting a test user, setting various values for the maxStorage attribute, then finding the resulting values returned by the HighPart and LowPart methods, you can verify that

Page 24: Ado

LowPart always returns values between –2^31 and 2^31 – 1 (2^31 = 2,147,483,648). This reveals that these methods perform unsigned arithmetic on the 64-bit numbers. For example, the following table shows the HighPart and LowPart values corresponding to critical values for maxStorage. The values are shown below with commas for legibility, but commas are not allowed in the actual values:

maxStorage HighPart LowPart Value10,737,418,238 2 2,147,483,646 10,737,418,23810,737,418,239 2 2,147,483,647 10,737,418,23910,737,418,240 2 -2,147,483,648 6,442,450,944

Notice in the last example, when the LowPart property method returns a negative value, the standard formula for determining the value of maxStorage is wrong by 4,294,967,296 which is 2^32. Because VB and VBScript do not support unsigned numbers, we must correct the value returned by the LowPart property method by adding 2^32. This is the same as increasing the value returned by the HighPart property method by one.

The following C code demonstrates the same issue:

_int64 i64;i64 = -10;i64 = i64 << 32;i64 = i64 + -10;

You might expect the hex value of i64 to be

0xfffffff6fffffff6

but it will actually be

0xfffffff5fffffff6

In the last step of the program, the value

0xfffffff600000000

is added to –10. When the lower 32 bits (4 bytes) underflow, the upper 32 bits are decremented by one. This can be avoided in C by using unsigned numbers:

_int64 i64;i64 = (ULONG)-10;i64 = i64 << 32;i64 = i64 + (ULONG)-10;

You can also assign negative values to the maxStorage attribute (even though these values make no sense). The same formula applies to calculate the value from the HighPart and LowPart methods. When the Integer8 value is negative, the HighPart method returns a negative value.

Page 25: Ado

Examples of Integer8 attributes include the following: accountExpires, badPasswordTime, lastLogon, lockoutTime, maxStorage, pwdLastSet, uSNChanged, uSNCreated, lockoutDuration, lockoutObservationWindow, maxPwdAge, minPwdAge, and modifiedCount.

The link on the left discusses the details of this problem and unsigned arithmetic.

The function linked below accounts for this problem and can be used to convert any Integer8 attribute value into a date in the local time zone:

Integer8Date.txt

' Integer8Date.vbs' VBScript program demonstrating how to convert an Integer8 attribute,' such as pwdLastSet, to a date value. The Integer8Date function' corrects for the inaccuracy in the HighPart and LowPart methods of' the IADsLargeInteger interface. It is desirable to have the' Integer8Date function always return a date, so the main program' can compare values to find the latest date. For this reason, if the' Integer8 attribute has no value, the function returns the date' 1/1/1601, which is the "zero" date. This really means "never".'' ----------------------------------------------------------------------' Copyright (c) 2003 Richard L. Mueller' Hilltop Lab web site - http://www.rlmueller.net' Version 1.0 - May 21, 2003' Version 1.1 - June 5, 2003 - Retrieve time zone bias from registry.' Version 1.2 - October 29, 2003 - Account for very large values.' Version 1.3 - January 25, 2004 - Modify error trapping.' Version 1.4 - December 29, 2009 - Handle Integer8 attribute no value.'' You have a royalty-free right to use, modify, reproduce, and' distribute this script file in any way you find useful, provided that' you agree that the copyright owner above has no warranty, obligations,' or liability for such use.

Option ExplicitDim strUserDN, lngTZBias, objUser, objPwdLastSetDim objShell, lngBiasKey, k

' Obtain local Time Zone bias from machine registry.' This bias changes with Daylight Savings Time.Set objShell = CreateObject("Wscript.Shell")lngBiasKey = objShell.RegRead("HKLM\System\CurrentControlSet\Control\" _ & "TimeZoneInformation\ActiveTimeBias")If (UCase(TypeName(lngBiasKey)) = "LONG") Then lngTZBias = lngBiasKeyElseIf (UCase(TypeName(lngBiasKey)) = "VARIANT()") Then lngTZBias = 0 For k = 0 To UBound(lngBiasKey) lngTZBias = lngTZBias + (lngBiasKey(k) * 256^k) NextEnd If

strUserDN = "cn=TestUser,ou=Sales,dc=MyDomain,dc=com"Set objUser = GetObject("LDAP://" & strUserDN)

' The pwdLastSet attribute should always have a value assigned,' but other Integer8 attributes representing dates could be "Empty".If (TypeName(objUser.pwdLastSet) = "Object") Then Set objPwdLastSet = objUser.pwdLastSet Wscript.Echo "Password last set: " & Integer8Date(objPwdLastSet, lngTZBias)Else Wscript.Echo "Password never set"End If

Function Integer8Date(ByVal objDate, ByVal lngBias) ' Function to convert Integer8 (64-bit) value to a date, adjusted for ' local time zone bias. Dim lngAdjust, lngDate, lngHigh, lngLow lngAdjust = lngBias lngHigh = objDate.HighPart lngLow = objdate.LowPart

Page 26: Ado

' Account for error in IADsLargeInteger property methods. If (lngLow < 0) Then lngHigh = lngHigh + 1 End If If (lngHigh = 0) And (lngLow = 0) Then lngAdjust = 0 End If lngDate = #1/1/1601# + (((lngHigh * (2 ^ 32)) _ + lngLow) / 600000000 - lngAdjust) / 1440 ' Trap error if lngDate is ridiculously huge. On Error Resume Next Integer8Date = CDate(lngDate) If (Err.Number <> 0) Then On Error GoTo 0 Integer8Date = #1/1/1601# End If On Error GoTo 0End Function

For completeness, here is a VBScript program that converts a date and time in the local time zone into the corresponding Integer8 value:

DateToInteger8.txt

' DateToInteger8.vbs' VBScript program demonstrating how to convert a datetime value to' the corresponding Integer8 (64-bit) value. The Integer8 value is the' number of 100-nanosecond intervals since 12:00 AM January 1, 1601,' in Coordinated Universal Time (UTC). The conversion is only accurate' to the nearest second, so the Integer8 value will always end in at' least 7 zeros.'' ----------------------------------------------------------------------' Copyright (c) 2004 Richard L. Mueller' Hilltop Lab web site - http://www.rlmueller.net' Version 1.0 - June 11, 2004'' You have a royalty-free right to use, modify, reproduce, and' distribute this script file in any way you find useful, provided that' you agree that the copyright owner above has no warranty, obligations,' or liability for such use.

Option Explicit

Dim dtmDateValue, dtmAdjusted, lngSeconds, str64BitDim objShell, lngBiasKey, lngBias, k

If (Wscript.Arguments.Count <> 1) Then Wscript.Echo "Required argument <DateTime> missing" Wscript.Echo "For example:" Wscript.Echo "" Wscript.Echo "cscript DateToInteger8.vbs ""2/5/2004 4:58:58 PM""" Wscript.Echo "" Wscript.Echo "If the date/time value has spaces, enclose in quotes" Wscript.QuitEnd If

dtmDateValue = CDate(Wscript.Arguments(0))

' Obtain local Time Zone bias from machine registry.' This bias changes with Daylight Savings Time.Set objShell = CreateObject("Wscript.Shell")lngBiasKey = objShell.RegRead("HKLM\System\CurrentControlSet\Control\" _ & "TimeZoneInformation\ActiveTimeBias")If (UCase(TypeName(lngBiasKey)) = "LONG") Then lngBias = lngBiasKeyElseIf (UCase(TypeName(lngBiasKey)) = "VARIANT()") Then lngBias = 0 For k = 0 To UBound(lngBiasKey) lngBias = lngBias + (lngBiasKey(k) * 256^k) NextEnd If

' Convert datetime value to UTC.dtmAdjusted = DateAdd("n", lngBias, dtmDateValue)

' Find number of seconds since 1/1/1601.lngSeconds = DateDiff("s", #1/1/1601#, dtmAdjusted)

' Convert the number of seconds to a string

Page 27: Ado

' and convert to 100-nanosecond intervals.str64Bit = CStr(lngSeconds) & "0000000"Wscript.Echo "Integer8 value: " & str64Bit

An alternative method to convert Integer8 values into dates uses the Windows time service tool w32tm.exe. This is included with Windows XP and Windows Server 2003 default installations (and newer operating systems). This tool can be used to convert 64-bit values to dates in the local time zone. The program must still use the IADsLargeInteger property methods to convert the Integer8 value to a 64-bit number. We must also account for the inaccuracy described above when the LowPart method returns a negative value. However, w32tm.exe takes the local time zone bias into account and converts a 64-bit value into a date and time in the local time zone. The program linked below also uses the Exec method of the wshShell object, so it requires WSH 5.6 as well as w32tm.exe. The example program demonstrating a function using w32tm.exe is linked here:

Integer8Date2.txt

' Integer8Date2.vbs' VBScript program demonstrating an alternative method to convert' an Integer8 attribute, such as pwdLastSet, to a date value. The' Integer8Date2 function uses the utility w32tm.exe.'' ----------------------------------------------------------------------' Copyright (c) 2007 Richard L. Mueller' Hilltop Lab web site - http://www.rlmueller.net' Version 1.0 - January 22, 2007' Version 1.1 - December 29, 2009 - Handle invalid dates.'' You have a royalty-free right to use, modify, reproduce, and' distribute this script file in any way you find useful, provided that' you agree that the copyright owner above has no warranty, obligations,' or liability for such use.

Option Explicit

Dim objUser, objPwdLastSet

' Bind to user object.Set objUser = GetObject("LDAP://cn=TestUser,ou=Sales,dc=MyDomain.dc=com")

' Retrieve pwdLastSet using IADsLargeInteger interface.' The pwdLastSet attribute should always have a value assigned,' but other Integer8 attributes representing dates could be "Empty".If (TypeName(objUser.pwdLastSet) = "Object") Then Set objPwdLastSet = objUser.Get("pwdLastSet")

' Convert the Integer8 value to a date and display. Wscript.Echo "PwdLastSet = " & Integer8Date2(objPwdLastSet)Else Wscript.Echo "Password never set"End If

Function Integer8Date2(objDate) ' Function to convert Integer8 (64-bit) value to a date and time ' in the local time zone. Dim objShell, strCmd, lngValue, objExec, arrWork, strValue Dim lngHigh, lngLow, strWork

lngHigh = objDate.HighPart lngLow = objdate.LowPart ' Account for error in IADslargeInteger property methods. If lngLow < 0 Then lngHigh = lngHigh + 1 End If

lngValue = (lngHigh) * (2 ^ 32) + lngLow strValue = FormatNumber(lngValue, 0, 0, 0, 0)

Set objShell = CreateObject("Wscript.Shell") strCmd = "w32tm.exe /ntte " & strValue Set objExec = objShell.Exec(strCmd) Do While objExec.Status = 0 Wscript.Sleep 100 Loop strWork = objExec.StdOut.Read(80)

Page 28: Ado

' Check for invalid date. If (InStr(strWork, "not a valid") > 0) Then Integer8Date2 = "<Not a valid date>" Else arrWork = Split(strWork, " ") Integer8Date2 = arrWork(3) & " " & arrWork(4) End If

End Function

In PowerShell (and .NET Framework) DateTime values are represented internally as the number of Ticks since 12:00 AM January 1, 0001. Ticks due to leap seconds are ignored (as are the days lost when the switch was made from the Julian to the Gregorian calendar in 1582). A PowerShell script to convert an Integer8 value into the corresponding date in both the local time zone and UTC (Coordinated Universal Time) is linked here:

PSInteger8ToDate.txt

# PSInteger8ToDate.ps1# PowerShell script demonstrating how to convert an Integer8 value into# a datetime value.## ----------------------------------------------------------------------# Copyright (c) 2011 Richard L. Mueller# Hilltop Lab web site - http://www.rlmueller.net# Version 1.0 - March 19, 2011## You have a royalty-free right to use, modify, reproduce, and# distribute this script file in any way you find useful, provided that# you agree that the copyright owner above has no warranty, obligations,# or liability for such use.

# Read Integer8 value from command line or prompt for value.Param ($Integer)If ($Integer -eq $Null){ $Integer = Read-Host "Integer8 value"}

# Convert Integer8 value into datetime in local time zone.$Date = [DateTime]::FromFileTime($Integer)

# Correct for daylight savings.If ($Date.IsDaylightSavingTime){ $Date = $Date.AddHours(1)}

# Display the datetime value."Local Time: $Date"

And a PowerShell script to convert a DateTime value in the local time zone into the corresponding Integer8 value is linked here:

PSDateToInteger8.txt

# PSDateToInteger8.ps1# PowerShell script demonstrating how to convert a datetime value to the# corresponding Integer8 (64-bit) value. The Integer8 value is the# number of 100-nanosecond intervals (ticks) since 12:00 AM January 1,# 1601, in Coordinated Univeral Time (UTC).## ----------------------------------------------------------------------# Copyright (c) 2011 Richard L. Mueller# Hilltop Lab web site - http://www.rlmueller.net# Version 1.0 - March 19, 2011## You have a royalty-free right to use, modify, reproduce, and# distribute this script file in any way you find useful, provided that# you agree that the copyright owner above has no warranty, obligations,# or liability for such use.

Page 29: Ado

# Read Datetime value from command line or prompt for value.Param ($strDate)If ($strDate -eq $Null){ $strDate = Read-Host "Date (Local Time)"}

# Convert string to datetime.$Date = [DateTime]"$strDate"

# Correct for daylight savings.If ($Date.IsDaylightSavingTime){ $Date = $Date.AddHours(-1)}

# Convert the datetime value, in UTC, into the number of ticks since# 12:00 AM January 1, 1601.$Value = ($Date.ToUniversalTime()).Ticks - ([DateTime]"January 1, 1601").Ticks$Value

If you use ADO in a VBScript program 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 above,    ' raises an error.    dtmDate = Integer8Date(objDate, lngTZBias)    adoRecordset.MoveNextLoop

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

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

Page 30: Ado

    ' Or use the Integer8Date function documented in the link above.    dtmDate = Integer8Date(objDate, lngTZBias)    adoRecordset.MoveNextLoop

A complication arises if the Integer8 attribute does not have a value. If you attempt to retrieve the value directly from the Active Directory object, the IADs interface returns data type "Empty", instead of "Object". If you use ADO to retrieve the attribute value, the data type is "Null" when the Integer8 attribute has no value. In both cases, the Set statement used to invoke the IADsLargeInteger interface raises an error. One way to handle this is to trap the possible error. For example:

Set objUser = GetObject("LDAP://cn=Jim Smith,ou=West,dc=MyDomain,dc=com")On Error Resume NextSet objDate = objUser.lockoutTimeIf (Err.Number <> 0) Then    On Error GoTo 0    dtmDate = "Never"Else    On Error GoTo 0    ' Use the Integer8Date function documented in the link above.    dtmDate = Integer8Date(objDate, lngTZBias)End If

An alternative way to handle the possibility that an Integer8 attribute does not have a value is to use the VBscript TypeName or VarType function. For example:

Do Until adoRecordset.EOF    strType = TypeName(adoRecordset.Fields("lockoutTime").Value)    If (strType = "Object") Then        Set objDate = adoRecordset.Fields("lockoutTime").Value        ' Use the Integer8Date function documented in the link above.        dtmDate = Integer8Date(objDate, lngTZBias)    Else        dtmDate = "Never"    End If    adoRecordset.MoveNextLoop

Another complication arises if the Integer8 value corresponds to a date so far in the future that an error is raised when the 64-bit value is converted into a date. The accountExpires attribute is the only one where this has been seen. If a user object has never had an expiration date, Active Directory assigns the value 2^63 - 1 to the accountExpires attribute. This is the largest number that can be saved as a 64-bit value. It really means "never". If you attempt to convert the value to a date using the CDate function, an error is raised. The Integer8Date and Integer8Date2 functions linked above account for this and trap the error. The following table documents the possible values that have been observed for several Integer8 attributes that represent dates.

attribute No Value 0 2^63 - 1 Date

Page 31: Ado

lastLogon  Yes Yes   YeslastLogonTimeStamp Yes Yes   YespwdLastSet   Yes   YesaccountExpires    Yes Yes YeslockoutTime Yes Yes   YesbadPasswordTime Yes Yes   Yes

Finally, it should be noted that a few Integer8 attributes can be modified with the IADsLargeInteger interface. So far the only Integer8 attributes found that can be modified in code (and assigned values other than 0 and -1) are maxStorage, accountExpires, maxPwdAge, minPwdAge, lockoutDuration, and lockoutObservationWindow. For example, the following VBScript program assigns the account expiration date of November 21, 2009, 4:02:18 PM UTC:

Set objUser = GetObject("LDAP://cn=Jim Smith,ou=West,dc=MyDomain,dc=com")Set objDate = objUser.Get("accountExpires")objDate.HighPart = 30042820objDate.LowPart = 1500000objUser.Put "accountExpires", objDateobjUser.SetInfo

Because the accountExpires attribute seems to always have a value (see the table above), we do not expect an error to be raised by the "Set objDate" statement. If an error could be raised, the solution would be to first assign a value that does not require the IADsLargeInteger interface, such as 0 (zero), so that we can then retrieve the object reference required to assign values with the HighPart and LowPart methods.

And the following VBScript program assigns the value -9,000,000,000 (corresponding to 15 minutes) to the domain minPwdAge attribute:

Set objDomain = GetObject("LDAP://dc=MyDomain,dc=com")Set objDate = objDomain.Get("minPwdAge")objDomain.HighPart = -3objDomain.LowPart = -410065408objDomain.Put "minPwdAge", objDateobjDomain.SetInfo

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

Page 32: Ado

Almost any characters can be used in Distinguished Names. However, some must be escaped with the backslash "\" escape character. Active Directory requires that the following characters be escaped:

comma ,Backslash character \Pound sign (hash sign) #Plus sign +Less than symbol <Greater than symbol >Semicolon ;Double quote (quotation mark) "Equal sign =Leading or trailing spaces  

The space character must be escaped only if it is the leading or trailing character in a component name, such as a Common Name. Embedded spaces should not be escaped.

In addition, ADSI requires that the forward slash character "/" also be escaped in Distinguished Names. The ten characters above, plus the forward slash, must be escaped in VBScript programs because they use ADSI. If you view attribute values with ADSI Edit you will see the ten characters above escaped, but not the forward slash. Utilities (like adfind.exe) that do not use ADSI need to have the ten characters above escaped, but not the forward slash.

For example, the following table shows example Common Names as they would appear in ADUC and the corresponding escaping required if the Distinguished Name is hard coded in VBScript:

Name in ADUC Escaped in VBScriptcn=Last, First cn=Last\, Firstcn=Windows 2000/XP cn=Windows 2000\/XPcn=Sales\Engr cn=Sales\\Engrcn=E#Test cn=E\#Test

Some characters that are allowed in Distinguished Names and do not need to be escaped include:

* ( ) . & - _ [ ] ` ~ | @ $ % ^ ? : { } ! '

The following characters are not allowed in sAMAccountName's:

" [ ] : ; | = + * ? < > / \ ,

All of these characters are allowed in Distinguished Names, but the last three must be escaped.

Page 33: Ado

If you are binding to an object in Active Directory and specifying the Distinguished Name in the binding string, the characters listed at the top of this page must be escaped with the backslash escape character. For example:

Set objUser = GetObject("LDAP://cn=Wilson\, Fred,ou=Sales,dc=MyDomain,dc=com")Set objGroup = GetObject("LDAP://cn=W2k\/XP,ou=East,dc=MyDomain,dc=com")Set objUser = GetObject("LDAP://cn=Jim Smith,ou=E\#Acctg,dc=MyDomain,dc=com")Set objGroup = GetObject("LDAP://cn=West\\Engr,ou=West,dc=MyDomain,dc=com")

You can escape any characters, including foreign and other non-keyboard characters. For example, the following characters.

á é í ó ú ñ

would be escaped as follows (in order):

\E1 \E9 \ED \F3 \FA \F1

For escaping characters in PowerShell, see this page:

PowerShellEscape.htm

The situation is different if you are using PowerShell. You still must escape most of the characters required by Active Directory, using the backslash "\" escape character, if they appear in hard coded Distinguished Names. However, PowerShell also requires that the backtick "`" and dollar sign "$" characters be escaped if they appear in any string that is quoted with double quotes. The backtick is also called the back apostrophe. If the string is quoted with single quote characters, then "$" characters do not need to be escaped. The PowerShell escape character is the backtick "`" character. This applies whether you are running PowerShell statements interactively, or running PowerShell scripts.

I have not determined why, but the pound sign character "#" does not need to escaped as part of a hard coded Distinguished Name in PowerShell. This is despite the fact that when PowerShell retrieves a Distinguished Name that includes the "#" character, it is escaped with the backslash character. Also, the dollar sign "$" need not be escaped if it is the last character in a PowerShell string. Of course, it never hurts to escape any character.

If you use the [ADSI] accelerator, (or the equivalent [System.DirectoryServices.DirectoryEntry] class) or ADO in PowerShell, the forward slash character "/" must be escaped with the backslash "\" in Distinguished Names. The [ADSI] accelerator and ADO both use ADSI. But if you use the new Active Directory cmdlets installed with Windows Server 2008 R2, like Get-ADUser, the forward slash "/" does not need to be escaped. The new AD modules use the .NET Framework instead of ADSI.

Page 34: Ado

Finally, if your PowerShell strings are quoted with double quotes, then any double quote characters in the string must be escaped with the backtick "`". Any single quote characters would not need to be escaped. Of course, the situation is reversed if the PowerShell string is quoted with single quotes. In that case, single quote characters would need to be escaped with the backtick "`", but double quote characters would not. The single quote (') character does not need to be escaped in Active Directory, but the double quote (") character does. This means that if you hard code a Distinguished Name in PowerShell, and the string is enclosed in double quotes, any embedded double quotes must be escaped first by a backtick "`", and then by a backslash "\". A few examples should clarify the situation. Below are some Common Names as they might appear using ADSI Edit, and how they must be hard coded as part of a Distinguished Name in PowerShell.

Name in ADUC Escaped in PowerShell stringcn=James "Jim" Smith "cn=James \`"Jim\`" Smith"cn=James $ Smith "cn=James `$ Smith"cn=James $ Smith 'cn=James $ Smith'cn=Sally Wilson + Jones "cn=Sally Wilson \+ Jones"cn=William O'Brian "cn=William O'Brian"cn=William O'Brian 'cn=William O`'Brian'cn=William O`Brian "cn=William O``Brian"cn=Richard #West "cn=Richard #West"cn=Roy Johnson$ "cn=Roy Johnson$"

Note the instances of " are replaced with \`", while $ and ` characters are both escaped with the backtick (because it is required in PowerShell) in strings quoted with double quotes, the $ character need not be escaped if the string is quoted with single quotes, and the + character is escaped with a backslash (because it is required in Active Directory). Also note that the # character does not need to be escaped in PowerShell, and the $ character need not be escaped if it is the trailing character in a string.

If you use the NameTranslate object to convert the NT name (NetBIOS name) of an object to the Distinguished Name, these characters will already be escaped by NameTranslate, except for the forward slash character. If the Distinguished Name has the "/" character, you must replace it with "\/" to avoid an error when you bind to the object. For example:

' Constants for the NameTranslate object.Const ADS_NAME_INITTYPE_GC = 3Const ADS_NAME_TYPE_NT4 = 3Const ADS_NAME_TYPE_1779 = 1

' Specify the NetBIOS name of the domain and the NT name of the user.strNTName = "MyDomain\TestUser"

' Use the NameTranslate object to convert the NT user name to the' Distinguished Name required for the LDAP provider.Set objTrans = CreateObject("NameTranslate")

Page 35: Ado

objTrans.Init ADS_NAME_INITTYPE_GC, ""objTrans.Set ADS_NAME_TYPE_NT4, strNTName

strUserDN = objTrans.Get(ADS_NAME_TYPE_1779)

' Replace any "/" characters with "\/".' All other characters that need to be escaped already are escaped.

strUserDN = Replace(strUserDN, "/", "\/")Set objUser = GetObject("LDAP://" & strUserDN)

The same thing happens if you use ADO to retrieve the value of the distinguishedName attribute. All characters will be properly escaped except any "/" characters. For example:

' 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 = "distinguishedName"

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

' Run the query.Set adoRecordset = adoCommand.Execute

' Enumerate the resulting recordset.Do Until adoRecordset.EOF    ' Retrieve values.    strDN = adoRecordset.Fields("distinguishedName").value    ' Replace any "/" characters with "\/".

Page 36: Ado

    ' All other characters that need to be escaped already are escaped.    strDN = Replace(strDN, "/", "\/")    ' Bind to user object.    Set objUser = GetObject("LDAP://" & strDN)    Wscript.Echo "NT Name: " & objUser.sAMAccountName _        & ", First Name: " & objUser.givenName _        & ", Last Name: " & objUser.sn    ' Move to the next record in the recordset.    adoRecordset.MoveNextLoop

' Clean up.adoRecordset.CloseadoConnection.Close

If you use the dsquery and dsget command line utilities the situation is a bit different. The dsget command requires that comma, backslash, and quote characters be escaped. However, the dsquery command only escapes the comma and backslash characters. The quote character must be escaped because the Distinguished Names output by the dsquery command are enclosed in quotes, in case there are any spaces. The dsqet command is fooled by any quote characters embedded in the Distinguished Name. For example, the following command should output the sAMAccountName of all users in the domain:

dsquery user -limit 0 | dsget user -samid

This command raises an error if any users have a quote in their common name. A workaround is to redirect the output of the dsquery command to a text file, modify the text file to escape any quotes with the backslash character, and then feed the modified text file to the dsget command. For example, create the text file of user Distinguished Names with the command.

dsquery user -limit 0 > users.txt

It is not easy to modify the file users.txt since all lines begin and end with quote characters. You need to replace all other instances of " with \". After this modification, use this command:

type users.txt | dsget user -samid

A further complication arises if you use LDAP filters to query Active Directory. For example, if you use ADO to query Active Directory, and you use the LDAP syntax, one of the clauses is an LDAP filter clause. Command line utilities like adfind and dsquery also accept LDAP filters. The LDAP filter specification assigns special meaning to the following characters:

* ( ) \ NUL

Page 37: Ado

The NUL character is ASCII 00. In LDAP filters these 5 characters should be escaped with the backslash escape character, followed by the two digit ASCII hex representation of the character. The following table documents this:

* \2A( \28) \29\ \5CNUL \00

For example, if you use ADO in a VBScript program to query for the user with Common Name "James Jim*) Smith", the LDAP filter would be:

strFilter = "(cn=James Jim\2A\29 Smith)"

Actually, the parentheses only need to be escaped if they are unmatched, as above. If instead the Common name were "James (Jim) Smith", nothing would need to be escaped. However, any characters, including non-display characters, can be escaped in a similar manner in an LDAP filter.

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

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.

IsMember Function 8

VBScript program demonstrating the use of an efficient IsMember function to test for group membership for any number of users or computers, using the "tokenGroups" attribute. The function reveals membership in nested groups and the "Primary Group".

Page 38: Ado

The IsMember function uses a dictionary object, so that group memberships only have to be enumerated once, no matter how many times the function is called.

Nested Groups

An example best explains the concept of "Nested Groups". Assume user "Johnny" is a member of group "Grade 1". In turn, group "Grade 1" is a member of group "Students". In addition, the group "Students" is a member of the group "School". User "Johnny" is a member of "School" by virtue of "Nested Group" membership. To recognize that "Johnny" is a member of "School", you need a function that reveals "Nested Group" memberships. "Nested Groups" are only allowed if the domain is in "Native Mode". However, they are very useful in environments with many departments, especially if they are hierarchical.

An example of "Circular Nested Groups" would result if someone made the group "School" a member of the group "Grade 1". Any function that deals with "Nested Groups" must avoid an infinite loop if it encounters this situation.

Unfortunately, the WinNT provider cannot reveal "Nested Group" membership of Global and Universal Security Groups. An IsMember function must use the LDAP provider to recognize "Nested Groups". The WinNT provider will reveal nested local groups and nested domain distribution groups.

Primary Group

Another important concept is the "Primary Group". By default, the "Primary Group" of a user object is the group "Domain Users", but this can be changed. The default "Primary Group" for computer objects is "Domain Computers". There should be no need to change the "Primary Group" unless the network supports Macintosh clients or POSIX-compliant applications. Unfortunately, the LDAP provider does not reveal membership in the "Primary Group" directly, so some IsMember functions have this drawback.

Page 39: Ado

In most cases you can assume that every user is a member of the group "Domain Users", and that every computer is a member of the group "Domain Computers". There should be no need to test memberships in these groups. If you have users or computers with different "Primary Groups", then you might need to select an IsMember function that reveals membership in the "Primary Group".

Instead of using the objectSid of each group in the tokenGroups collection to bind to the group object and retrieve the group name, this program uses ADO to search for the objects in Active Directory that have the values for objectSid and retrieve the group names. If the user or computer is a member of many groups, this method should be much faster, at the expense of more lines of code.

This program uses the LDAP provider to bind to the user or computer object in Active Directory. The "tokenGroups" attribute does not reveal cross-domain groups. If you have more than one domain, this function will not reveal membership in groups that are not in the same domain as the user or computer.

This program should work on any 32-bit Windows client that can log onto the domain. Windows NT and Windows 98/95 clients should have DSClient installed. If DSClient is not installed, WSH and ADSI should be installed.

Typically, this IsMember function would be used in a logon script to map drives to network shares according to user group membership. It can also be used to map local ports to shared printers according to computer group membership.

IsMember8.txt

' IsMember8.vbs' VBScript program demonstrating the use of Function IsMember.'' ----------------------------------------------------------------------' Copyright (c) 2004-2010 Richard L. Mueller' Hilltop Lab web site - http://www.rlmueller.net' Version 1.0 - March 28, 2004' Version 1.1 - July 6, 2007 - Modify use of Fields collection of' Recordset object.' Version 1.2 - November 6, 2010 - No need to set objects to Nothing.'' An efficient IsMember function to test group membership for any number' of users or computers, using the "tokenGroups" attribute. The function' reveals membership in nested groups and the primary group. It requires' that the user or computer object be bound with the LDAP provider.' Based on an idea by Joe Kaplan.'' You have a royalty-free right to use, modify, reproduce, and' distribute this script file in any way you find useful, provided that' you agree that the copyright owner above has no warranty, obligations,' or liability for such use.

Option Explicit

Dim objADUser, objComputer, strGroup, objGroupListDim adoCommand, adoConnection, strBase, strAttributes

' Bind to the user or computer object in Active Directory with the LDAP' provider.Set objADUser = _ GetObject("LDAP://cn=TestUser,ou=Sales,dc=MyDomain,dc=com")Set objComputer = _ GetObject("LDAP://cn=TestComputer,ou=Sales,dc=MyDomain,dc=com")

' Test for group membership.strGroup = "Engineering"If (IsMember(objADUser, strGroup) = True) Then

Page 40: Ado

Wscript.Echo "User " & objADUser.name _ & " is a member of group " & strGroupElse Wscript.Echo "User " & objADUser.name _ & " is NOT a member of group " & strGroupEnd If

strGroup = "Domain Users"If (IsMember(objADUser, strGroup) = True) Then Wscript.Echo "User " & objADUser.name _ & " is a member of group " & strGroupElse Wscript.Echo "User " & objADUser.name _ & " is NOT a member of group " & strGroupEnd If

strGroup = "Front Office"If (IsMember(objComputer, strGroup) = True) Then Wscript.Echo "Computer " & objComputer.name _ & " is a member of group " & strGroupElse Wscript.Echo "Computer " & objComputer.name _ & " is NOT a member of group " & strGroupEnd If

' Clean up.If (IsObject(adoConnection) = True) Then adoConnection.CloseEnd If

Function IsMember(ByVal objADObject, ByVal strGroupNTName) ' Function to test for group membership. ' objADObject is a user or computer object. ' strGroupNTName is the NT name (sAMAccountName) of the group to test. ' objGroupList is a dictionary object, with global scope. ' Returns True if the user or computer is a member of the group. ' Subroutine LoadGroups is called once for each different objADObject.

Dim objRootDSE, strDNSDomain

' The first time IsMember is called, setup the dictionary object ' and objects required for ADO. If (IsEmpty(objGroupList) = True) Then Set objGroupList = CreateObject("Scripting.Dictionary") objGroupList.CompareMode = vbTextCompare

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

Set objRootDSE = GetObject("LDAP://RootDSE") strDNSDomain = objRootDSE.Get("defaultNamingContext")

adoCommand.Properties("Page Size") = 100 adoCommand.Properties("Timeout") = 30 adoCommand.Properties("Cache Results") = False

' Search entire domain. strBase = "<LDAP://" & strDNSDomain & ">" ' Retrieve NT name of each group. strAttributes = "sAMAccountName"

' Load group memberships for this user or computer into dictionary ' object. Call LoadGroups(objADObject) End If If (objGroupList.Exists(objADObject.sAMAccountName & "\") = False) Then ' Dictionary object established, but group memberships for this ' user or computer must be added. Call LoadGroups(objADObject) End If ' Return True if this user or computer is a member of the group. IsMember = objGroupList.Exists(objADObject.sAMAccountName & "\" _ & strGroupNTName)End Function

Sub LoadGroups(ByVal objADObject) ' Subroutine to populate dictionary object with group memberships. ' objGroupList is a dictionary object, with global scope. It keeps track ' of group memberships for each user or computer separately. ADO is used ' to retrieve the name of the group corresponding to each objectSid in ' the tokenGroup array. Based on an idea by Joe Kaplan.

Dim arrbytGroups, k, strFilter, adoRecordset, strGroupName, strQuery

' Add user name to dictionary object, so LoadGroups need only be

Page 41: Ado

' called once for each user or computer. objGroupList.Add objADObject.sAMAccountName & "\", True

' Retrieve tokenGroups array, a calculated attribute. objADObject.GetInfoEx Array("tokenGroups"), 0 arrbytGroups = objADObject.Get("tokenGroups")

' Create a filter to search for groups with objectSid equal to each ' value in tokenGroups array. strFilter = "(|" If (TypeName(arrbytGroups) = "Byte()") Then ' tokenGroups has one entry. strFilter = strFilter & "(objectSid=" _ & OctetToHexStr(arrbytGroups) & ")" ElseIf (UBound(arrbytGroups) > -1) Then ' TokenGroups is an array of two or more objectSid's. For k = 0 To UBound(arrbytGroups) strFilter = strFilter & "(objectSid=" _ & OctetToHexStr(arrbytGroups(k)) & ")" Next Else ' tokenGroups has no objectSid's. Exit Sub End If strFilter = strFilter & ")"

' Use ADO to search for groups whose objectSid matches any of the ' tokenGroups values for this user or computer. strQuery = strBase & ";" & strFilter & ";" _ & strAttributes & ";subtree" adoCommand.CommandText = strQuery Set adoRecordset = adoCommand.Execute

' Enumerate groups and add NT name to dictionary object. Do Until adoRecordset.EOF strGroupName = adoRecordset.Fields("sAMAccountName").Value objGroupList.Add objADObject.sAMAccountName & "\" _ & strGroupName, True adoRecordset.MoveNext Loop adoRecordset.Close

End Sub

Function OctetToHexStr(ByVal arrbytOctet) ' Function to convert OctetString (byte array) to Hex string, ' with bytes delimited by \ for an ADO filter.

Dim k OctetToHexStr = "" For k = 1 To Lenb(arrbytOctet) OctetToHexStr = OctetToHexStr & "\" _ & Right("0" & Hex(Ascb(Midb(arrbytOctet, k, 1))), 2) NextEnd Function

A PowerShell script that performs the same function is linked below.

PSIsMember8.txt

# PSIsMember8.ps1# PowerShell program demonstrating the use of Function IsMember.## ----------------------------------------------------------------------# Copyright (c) 2011 Richard L. Mueller# Hilltop Lab web site - http://www.rlmueller.net# Version 1.0 - May 14, 2011## An efficient IsMember function to test security group membership for# any number of users or computers, using the "tokenGroups" attribute.# The function reveals membership in nested groups and the primary group.# Based on an idea by Joe Kaplan.## You have a royalty-free right to use, modify, reproduce, and# distribute this script file in any way you find useful, provided that# you agree that the copyright owner above has no warranty, obligations,# or liability for such use.

Trap {"Error: $_"; Break;}

# Hash table of security groups memberships.$Script:GroupList = @{}

Function IsMember($ADObject, $GroupName)

Page 42: Ado

{ # Function returns $True if $ADObject is a member of $GroupName, $False otherwise. If ($Script:GroupList.Count -eq 0) { # Hash table has no entries. Setup DirectorySearcher. # Search entire domain. $Domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain() $Root = $Domain.GetDirectoryEntry() $Script:Searcher = [System.DirectoryServices.DirectorySearcher]$Root

$Script:Searcher.PageSize = 200 $Script:Searcher.SearchScope = "subtree" $Script:Searcher.PropertiesToLoad.Add("sAMAccountName") > $Null

LoadGroups $ADObject } # Check if security group membership retrieved for $ADObject. If ($Script:GroupList.ContainsKey($ADObject.sAMAccountName.ToString() + "\") -eq $False) { LoadGroups $ADObject } # Check if $ADObject is a member of $GroupName. If ($Script:GroupList.ContainsKey($ADObject.sAMAccountName.ToString() + "\" + $GroupName)) { Return $True } Else { Return $False }}

Function LoadGroups($ADObject){ # Function to retrieve all security group memberships of $ADObject and # populate the hash table. # Add an entry for $ADObject so we can check if security membership retrieved. $Script:GroupList.Add($ADObject.sAMAccountName.ToString() + "\", $True)

# Retrieve tokenGroups attribute, a multi-valued operational attribute. # Each item in the tokenGroups collection is a SID value (a byte array). $ADObject.psbase.RefreshCache("tokenGroups") $SIDs = $ADObject.psbase.Properties.Item("tokenGroups") # Create a filter to search for the groups with the corresponding objectSID values. $Filter = "(|" ForEach ($Value In $SIDs) { $HexSID = "" # Convert each byte of the group SID into hex. ForEach ($Byte In $Value) { $Hex = [Convert]::ToString($Byte, 16) $HexSID = $HexSID + "\" + $Hex } $Filter = $Filter + "(objectSid=$HexSID)" } $Filter = $Filter + ")" $Script:Searcher.Filter = $Filter

# Retrieve all security groups represented by SID values in tokenGroups. $Results = $Script:Searcher.FindAll() ForEach ($Result In $Results) { # Add the sAMAccountName of each group to the has table. $Name = $Result.Properties.Item("sAMAccountName") $Script:GroupList.Add($ADObject.sAMAccountName.ToString() + "\" + $Name, $True) }}

# Bind to the user object in Active Directory.$User = [ADSI]"LDAP://cn=TestUser,ou=Sales,dc=MyDomain,dc=com"

# Bind to the computer object in Active Directory.$Computer = [ADSI]"LDAP://cn=TestComputer,ou=Sales,dc=MyDomain,dc=com"

$GroupName = "Engineering"If (IsMember $User $GroupName -eq $True){ "User " + $User.sAMAccountName + " is a member of group " + $GroupName}Else{ "User " + $User.sAMAccountName + " is NOT a member of group " + $GroupName}

$GroupName = "Domain Users"If (IsMember $User $GroupName -eq $True){

Page 43: Ado

"User " + $User.sAMAccountName + " is a member of group " + $GroupName}Else{ "User " + $User.sAMAccountName + " is NOT a member of group " + $GroupName}

$GroupName = "Front Office"If (IsMember $User $GroupName -eq $True){ "User " + $User.sAMAccountName + " is a member of group " + $GroupName}Else{ "User " + $User.sAMAccountName + " is NOT a member of group " + $GroupName}

$GroupName = "Front Office"If (IsMember $Computer $GroupName -eq $True){ "Computer " + $Computer.sAMAccountName + " is a member of group " + $GroupName}Else{ "Computer " + $Computer.sAMAccountName + " is NOT a member of group " + $GroupName}

$GroupName = "Domain Computers"If (IsMember $Computer $GroupName -eq $True){ "Computer " + $Computer.sAMAccountName + " is a member of group " + $GroupName}Else{ "Computer " + $Computer.sAMAccountName + " is NOT a member of group " + $GroupName}

Page 44: Ado

SQL Syntax

ADO can also use SQL syntax to query Active Directory. This syntax is more familiar to some people, but many SQL features are not supported. SQL syntax uses the keywords SELECT and FROM. You can also use the keywords WHERE and even ORDER BY.

Both LDAP and SQL queries are strings that are assigned to properties of ADO objects. Like any VBScript string, the value is enclosed with double quotes. If you are using LDAP syntax, embedded string values in the query are not enclosed by quotes. For example, the following LDAP syntax query uses several string values, like "person" and "user":

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

SQL syntax requires that embedded strings be enclosed with single quotes. For example, the same query in SQL syntax would be:

"SELECT sAMAccountName,cn " _     & "FROM 'LDAP://ou=Sales,dc=MyDomain,dc=com' " _     & "WHERE objectCategory='person' AND objectClass='user'"

Date values are also enclosed in single quotes, but numeric values are not. Note also that you must be careful to include spaces in the correct locations. If any string values in an SQL statement have single quote characters, the single quotes must be doubled. For example:

"SELECT distinguishedName, displayName " _     & "FROM 'LDAP://ou=Sales,dc=MyDomain,dc=com' " _     & "WHERE sAMAccountName='maryo''brian'"

Some more examples of SQL syntax queries follow:

To return sAMAccountName and distinguishedName of all objects of class "person" in the Sales Organizational Unit that are not members of any group (except their "primary" group):

"SELECT sAMAccountName, distinguishedName " _     & "FROM 'LDAP://ou=Sales,dc=MyDomain,dc=com' " _     & "WHERE objectClass='person' AND NOT memberOf='*'"

To return sAMAccountName and distinguishedName of all users objects that do not expire in ou=Sales:

"SELECT sAMAccountName, distinguishedName " _     & "FROM 'LDAP://ou=Sales,dc=MyDomain,dc=com' " _     & "WHERE objectCategory='person' " _

Page 45: Ado

     & "AND objectClass='user' " _     & "AND (accountExpires=0 OR accountExpires=9223372036854775807)"

To return sAMAccountName of all objects created after September 2, 2005, in ou=Sales:

"SELECT sAMAccountName " _     & "FROM 'LDAP://ou=Sales,dc=MyDomain,dc=com' " _     & "WHERE createTimestamp>='20050902000000.0Z'"

If you specify SELECT * the only attribute value returned is the ADsPath. For example, the following returns the ADsPath of all groups in ou=Sales:

"SELECT * FROM 'LDAP://ou=Sales,dc=MyDomain,dc=com' " _     & "WHERE objectClass='group'"

To return distinguishedName of all user objects in ou=Sales with a value of 546 assigned to the userAccountControl attribute:

"SELECT distinguishedName " _     & "FROM 'LDAP://ou=Sales,dc=MyDomain,dc=com' " _     & "WHERE objectCategory='person' AND objectClass='user' " _     & "AND userAccountControl=546"

To return distinguishedName of object in ou=Sales with GUID = "6394351061438F4B82662379F7C4408E":

"SELECT distinguishedName " _     & "FROM 'LDAP://ou=Sales,dc=MyDomain,dc=com' " _     & "WHERE objectGuid='\63\94\35\10\61\43\8F\4B\82\66\23\79\F7\C4\40\8E'"

To return sAMAccountName of all objects in ou=Sales that are members of group TestGroup:

"SELECT sAMAccountName " _     & "FROM 'LDAP://ou=Sales,dc=MyDomain,dc=com' " _     & "WHERE memberOf='cn=TestGroup,ou=West,dc=MyDomain,dc=com'"

To return distinguishedName and whenCreated for all users in ou=Sales created after September 1, 2006:

"SELECT distinguishedName, whenCreated " _     & "FROM 'LDAP://ou=Sales,dc=MyDomain,dc=com' " _     & "WHERE objectCategory='person' AND objectClass='user' " _     & "AND whenCreated>='20060901000000.0Z'"

To return distinguishedName of all users in ou=Sales that have no value assigned to the description attribute:

Page 46: Ado

"SELECT distinguishedName " _     & "FROM 'LDAP://ou=Sales,dc=MyDomain,dc=com' " _     & "WHERE objectCategory='person' AND objectClass='user' " _     & "AND NOT description ='*'"

To return the values of the distinguishedName, cn, sn, and givenName attributes of all user objects in ou=Sales sorted by cn:

"SELECT distinguishedName, cn, sn, GivenName " _     & "FROM 'LDAP://ou=Sales,dc=MyDomain,dc=com' " _     & "WHERE ObjectCategory='person' And ObjectClass='user' " _     & "ORDER By cn"

An example using range limits would be:

"SELECT 'member;range=0-999', sAMAccountName " _     & "FROM 'LDAP://ou=Sales,dc=MyDomain,dc=com' " _     & "WHERE ObjectCategory='group'"

Note in the previous example that the attribute name and range limits are enclosed by single quotes. There is no way known to test bits of the userAccountControl attribute using SQL syntax.

If the version of MDAC on the client is less than 2.8, the maximum number of attribute values that can be retrieved using SQL syntax is 49. For example, a Windows 2000 Professional computer with MDAC 2.7 will raise an error if you attempt to retrieve more than 49 attributes using SQL syntax. No such limit has been seen if MDAC is 2.8 or greater. Also, no limit on any client OS has been experienced using LDAP syntax. The version of MDAC on a client can be determined from the version of the files msdadc.dll and oledb32.dll.

Page 47: Ado

ADO Alternatives

ADO provides a great deal of flexibility. There are several ways to accomplish the same task, which is generally to return a recordset with information from Active Directory. The recordset contains attribute values for objects satisfying the search filter.

In the example on the previous page an ADO Connection object is used to establish a connection to Active Directory. This Connection object is assigned to the ActiveConnection property of an ADO Command object. Then the LDAP syntax query is assigned to the CommandText property of the Command object. Finally, the Execute method of the Command object returns the ADO Recordset object.

The ADO Command object is used because we can assign values to several useful properties. In particular, we can assign a value to the "Page Size" property. Any value up to a maximum of 1000 can be assigned to this property. The value assigned is less important than the fact that a value is assigned, since this turns on "paging". With paging turned on, ADO returns rows in pages until all row satisfying the query are retrieved. Without paging, ADO will stop after 1000 rows.

Tests were performed using different values for "Page Size" to see if any difference in performance could be detected. The tests involved a query for all users in the domain. The test domain has over 2100 user objects. The test was repeated with "Page Size" values of 100, 200, 400, 600, 800, and 1000. The differences were very small, but perhaps the optimal value in this case was 200. The tests were performed using both VBScript and PowerShell.

Another ADO Command object property often assigned is the "Timeout" property. This specifies the timeout value for the query in seconds. Also, queries will be more efficient if we assign "False" to the "Cache Results" property.

An alternative approach is to assign the query to the Source property of an ADO Recordset object. The Open method of the Recordset object is used to run the query. No Command object is used. The Connection object is assigned to the ActiveConnection property of the Recordset object. This approach is bit more straightforward. For example, the same program given earlier can be coded as follows:

Option ExplicitDim adoConnection, strBase, strFilter, strAttributesDim objRootDSE, strDNSDomain, strQuery, adoRecordset, strName, strCN

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

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

Page 48: Ado

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"

' Run the query.adoRecordset.Source = strQueryadoRecordset.Open

' 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.MoveNextLoop

' Clean up.adoRecordset.CloseadoConnection.Close

Because the Recordset object is created before the recordset is opened, you can assign a value to the cursorType property of the Recordset object. This can be useful if you want to use a cursor other the default forward only cursor. This would be necessary, for example, if you want to retrieve the value of the RecordCount property of the Recordset object, and then enumerate the recordset. The RecordCount property is the number of rows in the recordset. Retrieving the value of this property requires reading the entire recordset, which leaves the cursor at the end of the recordset. You must move the cursor back to the beginning with the MoveFirst method before enumerating the recordset, but this cannot be done with the default forward only cursor. The following code snippet demonstrates how this is done by assigning a value to the cursorType property of the Recordset object before it is opened.

Const adOpenStatic = 3

Set adoRecordset = CreateObject("ADODB.Recordset")Set adoRecordset.ActiveConnection = adoConnection' Assign cursorType that allows forward and backward movement.adoRecordset.cursorType = adOpenStatic

' Run the query.adoRecordset.Source = strQuery

Page 49: Ado

adoRecordset.Open

' Display number of records.' This positions the cursor at the end of the recordset.Wscript.Echo adoRecordset.RecordCount

' Move the cursor back to the beginning.' The cursorType assignment allows this.adoRecordset.MoveFirst

' 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.MoveNextLoop

' Clean up.adoRecordset.Close

The disadvantage is that you cannot assign values to the Command object properties. In particular, you cannot turn on paging. This is a problem if the recordset has more than 1000 rows. To handle this situation, you can instead assign a value to the cursorLocation property of the Connection object. If the cursorLocation property of the Connection object is adUseClient, then the default cursorType of any Recordset object using this Connection object will be adOpenStatic. This allows us to declare an ADO Command object, assign values to the Command object properties like "Page Size", and use the Execute method of the Command object to create the recordset. This is necessary if you need a cursor that allows movement forward and backward and you need to retrieve more than 1000 rows. The following demonstrates how this is done.

Option ExplicitDim adoCommand, adoConnection, strBase, strFilter, strAttributesDim objRootDSE, strDNSDomain, strQuery, adoRecordset, strName, strCN

Const adUseClient = 3

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

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

Page 50: Ado

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 = strQueryadoCommand.Properties("Page Size") = 100adoCommand.Properties("Timeout") = 30adoCommand.Properties("Cache Results") = False

' Run the query.Set adoRecordset = adoCommand.Execute

' Display number of records.' This positions the cursor at the end of the recordset.Wscript.Echo adoRecordset.RecordCount

' Move the cursor back to the beginning.adoRecordset.MoveFirst

' 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.MoveNextLoop

' Clean up.adoRecordset.CloseadoConnection.Close

Page 51: Ado

Alternate Credentials

Alternate credentials can be passed to ADO by assigning values to properties of the ADO connection object. The syntax is shown below. In this example, I include several constants that I have seen used for the "ADSI Flag" value. I also show three different forms that can be used for the user name.

' ADS Authentication constants that can be used.Const ADS_SECURE_AUTHENTICATION = &H1Const ADS_USE_ENCRYPTION = &H2Const ADS_USE_SSL = &H2Const ADS_USE_SIGNING = &H40Const ADS_USE_SEALING = &H80Const ADS_USE_DELEGATION = &H100Const ADS_SERVER_BIND = &H200

' Specify credentials.' Select one of the three possible forms for the user name.strUser = "cn=TestUser,ou=Sales,dc=MyDomain,dc=com"strUser = "[email protected]"strUser = "MyDomain\TestUser"strPassword = "xyz12345"

' Create the ADO Connection object.Set adoConnection = CreateObject("ADODB.Connection")adoConnection.Provider = "ADsDSOObject"adoConnection.Properties("User ID") = strUseradoConnection.Properties("Password") = strPasswordadoConnection.Properties("Encrypt Password") = TrueadoConnection.Properties("ADSI Flag") = ADS_SERVER_BIND _     Or ADS_SECURE_AUTHENTICATIONadoConnection.Open "Active Directory Provider"

The client computer must be joined to the domain. If the user is authenticated to the domain you can use serverless binding. However, if the user is logged in locally to the computer, and not authenticated to the domain, I find that you must use a server bind. You must specify the name of a Domain Controller. In the example below I assume the user is logged in locally. This VBScript program queries for all user objects in the domain and outputs the Distinguished Names. The name of a Domain Controller is hard coded, but the program uses the RootDSE object to retrieve the DNS name of the domain. The alternate credentials must also be used to connect to this object. You could instead hard code the DNS name of the domain.

Option Explicit

Dim objRootDSE, strDNSDomain, adoCommand, adoConnectionDim strBase, strFilter, strAttributes, strQuery, adoRecordsetDim strDN, strUser, strPassword, objNS, strServer

Const ADS_SECURE_AUTHENTICATION = &H1

Page 52: Ado

Const ADS_SERVER_BIND = &H200

' Specify a server (Domain Controller).strServer = "MyServer"

' Specify or prompt for credentials.strUser = "MyDomain\TestUser"strPassword = "xyz12345"

' Determine DNS domain name. Use server binding and alternate' credentials. The value of strDNSDomain can also be hard coded.Set objNS = GetObject("LDAP:")Set objRootDSE = objNS.OpenDSObject("LDAP://" & strServer & "/RootDSE", _     strUser, strPassword, _     ADS_SERVER_BIND Or ADS_SECURE_AUTHENTICATION)strDNSDomain = objRootDSE.Get("defaultNamingContext")

' Use ADO to search Active Directory.' Use alternate credentials.Set adoCommand = CreateObject("ADODB.Command")Set adoConnection = CreateObject("ADODB.Connection")adoConnection.Provider = "ADsDSOObject"adoConnection.Properties("User ID") = strUseradoConnection.Properties("Password") = strPasswordadoConnection.Properties("Encrypt Password") = TrueadoConnection.Properties("ADSI Flag") = ADS_SERVER_BIND _     Or ADS_SECURE_AUTHENTICATIONadoConnection.Open "Active Directory Provider"Set adoCommand.ActiveConnection = adoConnection

' Search entire domain. Use server binding.strBase = "<LDAP://" & strServer & "/" & strDNSDomain & ">"

' Search for all users.strFilter = "(&(objectCategory=person)(objectClass=user))"

' Comma delimited list of attribute values to retrieve.strAttributes = "distinguishedName"

' Construct the LDAP query.strQuery = strBase & ";" & strFilter & ";" _     & strAttributes & ";subtree"

' Run the query.adoCommand.CommandText = strQueryadoCommand.Properties("Page Size") = 100adoCommand.Properties("Timeout") = 30adoCommand.Properties("Cache Results") = FalseSet adoRecordset = adoCommand.Execute

Page 53: Ado

' Enumerate the resulting recordset.Do Until adoRecordset.EOF     ' Retrieve values.     strDN = adoRecordset.Fields("distinguishedName").Value     Wscript.Echo strDN     adoRecordset.MoveNextLoop

' Clean up.adoRecordset.CloseadoConnection.Close

If the user is authenticated to the domain, but lacks permission to run the query, you can use the more normal serverless binding and alternate credentials.

Page 54: Ado

Disconnected Recordsets

It is also possible to sort and filter the ADO recordset that results from a query of Active Directory. However, the recordset must be disconnected. In addition, you must specify the proper cursor type and location. This is best demonstrated by examples. First, here is an example using the Sort method of the recordset object:

Option ExplicitDim objRootDSE, strDNSDomain, adoConnectionDim strBase, strFilter, strAttributes, strQuery, adoRecordsetDim strLast, strFirst, intCount, strName

Const adOpenStatic = 3Const adLockOptimistic = 3Const adUseClient = 3

' Determine DNS domain name.Set objRootDSE = GetObject("LDAP://RootDSE")strDNSDomain = objRootDSE.Get("defaultNamingContext")

' Use ADO to search Active Directory.Set adoConnection = CreateObject("ADODB.Connection")adoConnection.Provider = "ADsDSOObject"adoConnection.Open "Active Directory Provider"

Set adoRecordset = CreateObject("ADODB.Recordset")Set adoRecordset.ActiveConnection = adoConnectionadoRecordset.CursorLocation = adUseClientadoRecordset.CursorType = adOpenStaticadoRecordset.LockType = adLockOptimistic

' Search entire domain.strBase = "<LDAP://" & strDNSDomain & ">"

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

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

' Construct the LDAP query.strQuery = strBase & ";" & strFilter & ";" & strAttributes & ";subtree"

' Run the query.adoRecordset.Source = strQueryadoRecordset.Open

' Disconnect the recordset.Set adoRecordset.ActiveConnection = NothingadoConnection.Close

Page 55: Ado

' Sort the recordset.adoRecordset.Sort = "sn,givenName"adoRecordset.MoveFirst

' Enumerate the resulting recordset.intCount = 0Do Until adoRecordset.EOF    ' Retrieve values.    strLast = adoRecordset.Fields("sn").Value    strFirst = adoRecordset.Fields("givenName").Value    strName = adoRecordset.Fields("sAMAccountName").Value    Wscript.Echo strLast & ";" & strFirst & ";" & strName    intCount = intCount + 1    adoRecordset.MoveNextLoop

' Clean up.adoRecordset.Close

Wscript.Echo "Number of objects: " & CStr(intCount)

The next example demonstrates how to use the Filter method of the recordset object. Again, the recordset must be disconnected and you must specify the cursor type and location. You can filter on any single valued field in the recordset. For example:

Option ExplicitDim objRootDSE, strDNSDomain, adoConnectionDim strBase, strFilter, strAttributes, strQuery, adoRecordsetDim intCount

Const adOpenStatic = 3Const adLockOptimistic = 3Const adUseClient = 3

' Determine DNS domain name.Set objRootDSE = GetObject("LDAP://RootDSE")strDNSDomain = objRootDSE.Get("defaultNamingContext")

' Use ADO to search Active Directory.Set adoConnection = CreateObject("ADODB.Connection")adoConnection.Provider = "ADsDSOObject"adoConnection.Open "Active Directory Provider"

Set adoRecordset = CreateObject("ADODB.Recordset")Set adoRecordset.ActiveConnection = adoConnectionadoRecordset.CursorLocation = adUseClientadoRecordset.CursorType = adOpenStaticadoRecordset.LockType = adLockOptimistic

Page 56: Ado

' Search entire domain.strBase = "<LDAP://" & strDNSDomain & ">"

' Filter on all objects regardless of category.' We will later filter the recordset on various object categories.strFilter = "(objectCategory=*)"

' Comma delimited list of attribute values to retrieve. Any attributes' (fields) we later wish to filter on must be included in the list.strAttributes = "distinguishedName,sAMAccountName,objectCategory,memberOf"

' Construct the LDAP query.strQuery = strBase & ";" & strFilter & ";" & strAttributes & ";subtree"

' Run the query.adoRecordset.Source = strQueryadoRecordset.Open

' Disconnect the recordset.Set adoRecordset.ActiveConnection = NothingadoConnection.Close

' Filter the recordset on objects of category person.adoRecordset.Filter = "objectCategory='cn=Person," _    & "cn=Schema,cn=Configuration,dc=MyDomain,dc=com'"intCount = adoRecordset.RecordCountWscript.Echo "Number of persons: " & CStr(intCount)

' Filter the recordset on objects of category computer.adoRecordset.MoveFirstadoRecordset.Filter = "objectCategory='cn=Computer," _    & "cn=Schema,cn=Configuration,dc=MyDomain,dc=com'"intCount = adoRecordset.RecordCountWscript.Echo "Number of computers: " & CStr(intCount)

' Filter the recordset on objects of category group.adoRecordset.MoveFirstadoRecordset.Filter = "objectCategory='cn=Group," _    & "cn=Schema,cn=Configuration,dc=MyDomain,dc=com'"intCount = adoRecordset.RecordCountWscript.Echo "Number of groups: " & CStr(intCount)

' Enumerate the groups.adoRecordset.MoveFirstDo Until adoRecordset.EOF    Wscript.Echo adoRecordset.Fields("sAMAccountName").Value    adoRecordset.MoveNextLoop

' Filter the recordset on objects of category OU.

Page 57: Ado

adoRecordset.MoveFirstadoRecordset.Filter = "objectCategory='cn=Organizational-Unit," _    & "cn=Schema,cn=Configuration,dc=MyDomain,dc=com'"intCount = adoRecordset.RecordCountWscript.Echo "Number of OU's: " & CStr(intCount)

adoRecordset.Close

Unfortunately, you cannot filter on multi-valued attributes, like memberOf. In the example above we were able to filter on objectCategory because it is a single-valued attribute. We could not filter on objectClass because it is multi-valued. If the following lines of code are added to the example above, an error is raised:

' Filter the recordset on members of Accountants group.adoRecordset.MoveFirstadoRecordset.Filter = "memberOf='cn=Accountants,ou=West,dc=MyDomain,dc=com'"intCount = adoRecordset.RecordCountWscript.Echo "Number of members of Accountants group: " & CStr(intCount)

Page 58: Ado

SQL Distributed Queries

It is also possible to query Active Directory from an SQL Server instance using an SQL distributed query. You use a system stored procedure in SQL Server to add Active Directory as a linked server. Then you use the SQL OPENQUERY command to invoke the ADSI OLEDB provider ADsDSOObject, pass a query to Active Directory, and return a recordset. SQL Server must be version 7.0 or above.

The first step is to use the system stored procedure sp_addlinkedserver to add your Active Directory as a linked server. The syntax would be (this is one line):

EXEC sp_addlinkedserver 'ADSI', 'Active Directory Service Interfaces', 'ADsDSOObject', 'adsdatasource'

If you use Windows authenticated logins, which is recommended, this is all that is required. If you use SQL Server authenticated logins you must use the sp_addlinkedsrvlogin system stored procedure to configure a login.

Once the linked server is established, you can use the OPENQUERY command to pass a query to Active Directory. You use either SQL syntax or LDAP syntax. An example using an SQL syntax query:

SELECT * FROM OPENQUERY(ADSI,    'SELECT sAMAccountName, mail    FROM ''LDAP://dc=MyDomain,dc=com''    WHERE objectCategory = ''person'' AND objectClass = ''user'''

The same query using LDAP syntax would be:

SELECT * FROM OPENQUERY(ADSI,    '<LDAP://dc=MyDomain,dc=com>;    (&(objectCategory=person)(objectClass=user));    sAMAccountName,Mail;subtree')

Note that constants, like "person" and "user", are enclosed in single quotes when you use SQL syntax, but are not enclosed by any character when you use LDAP syntax. The entire query is enclosed in single quotes, so any single quotes in the query, such as those enclosing the constants, must be doubled.

There are two limitations you should be aware of. First, the OPENQUERY statement does not support multi-valued attributes. You cannot retrieve the values of multi-valued attributes, like memberOf. Second, the total number of records that can be retrieved is limited to 1500 (1000 in Windows 2000 Active Directory). Paging is not supported from an SQL distributed query, so this limitation cannot be overcome, except by modifying the Active Directory server limit for maxPageSize.

For more information, see these links:

http://support.microsoft.com/kb/299410http://msdn.microsoft.com/en-us/library/aa746379(VS.85).aspx

Page 59: Ado

http://msdn.microsoft.com/en-us/library/aa746494(VS.85).aspxhttp://msdn.microsoft.com/en-us/library/aa746385(VS.85).aspx

Many people have reported problems using OPENQUERY. It may be necessary for the SQL Server instance to run with a service account that is a domain user. If the local system account is used instead, it may be that the instance lacks permission in Active Directory.

Page 60: Ado

ANR in ADO Searches

ANR is the acronym for Ambiguous Name Resolution. This is an efficient search algorithm in Active Directory that allows you to specify complex filters involving multiple naming-related attributes in a single clause. It can be used to locate objects in Active Directory when you know something about the object, but not necessarily which naming-attribute has the information. ANR is enabled by default in Active Directory. The following naming-related attributes support Ambiguous Name Resolution.

Attribute Windows 2000 Server

Windows Server 2003

AD LDS

Windows Server 2003 R2

Windows Server 2008

displayName X X X X XgivenName X X X XlegacyExchangeDN X X X XmsDS-AdditionalSamAccountName   X X X

msDS-PhoneticCompanyName X

msDS-PhoneticDepartment XmsDS-PhoneticDisplayName XmsDS-PhoneticFirstName XmsDS-PhoneticLastName XphysicalDeliveryOfficeName X X X X XproxyAddresses X X X X XName X X X X XsAMAccountName X X X Xsn X X X X

AD LDS in the table above refers to Active Directory Lightweight Directory Services (formerly called Active Directory Application Mode, or ADAM). All of the other columns refer to AD DS (Active Directory Directory Services). Note that the Name attribute above is the Relative Distinguished Name of the object. For user objects, this is the Common Name, the value of the cn attribute. As an example, suppose you want to find information on someone named "Smith". You can use the filter:

(anr=Smith)

This will return objects where the string "smith" appears at the start of any of the naming attributes listed above. As always, the search is not case sensitive. In other words, Active Directory will convert the filter to the following (in Windows 2000 Active Directory):

(|(displayName=Smith*)(givenName=Smith*)(legacyExchangeDN=Smith*)(physicalDeliveryOfficeName=Smith*)(proxyAddresses=Smith*)(Name=Smith*)(sAMAccountName=Smith*)(sn=Smith*))

Page 61: Ado

 Where "|" is the "OR" operator and "*" is the wildcard character. Better yet, suppose you know the person's name is "Jim Smith". You can use the filter:

(anr=Jim Smith)

Now Active Directory will search for objects where any of the naming attributes matches "Jim Smith*", plus any objects where (givenName=Jim*) and (sn=Smith*), plus any objects where (givenName=Smith*) and (sn=Jim*). The algorithm considers only the first space in the string when breaking it up into two values. For example the filter:

(anr=Jim Smith Williams)

This will query for objects where any of the naming attributes matches "Jim Smith Williams*", plus objects where (givenName=Jim*) and (sn=Smith Williams*), or where (givenName=Smith Williams*) and (sn=Jim*).

For more documentation on Ambiguous Name Resolution, see these links:

http://support.microsoft.com/default.aspx/kb/243299

http://msdn.microsoft.com/en-us/library/ms675092(VS.85).aspx

http://msdn.microsoft.com/en-us/library/cc223243(PROT.13).aspx

Page 62: Ado

Query AD with PowerShell

You can also use PowerShell scripts to query Active Directory. There are several methods that can be used. All of the examples linked on this page query Active Directory for the objects that have a specified Common Name (value of the cn attribute). The examples demonstrate three different techniques.

The first example uses ADO in a PowerShell script. The steps are very similar to those that would be used in a VBScript program. We create ADO connection and command objects, assign properties like Page Size and Timeout, then assign an LDAP query with the same four clauses used in a VBScript program. The first clause specifies the "base" of the query, the second clause is an LDAP filter, the third clause is a comma delimited list of attributes, and the fourth clause specifies the scope. This script will work in PowerShell v1 or v2.

FindUser1.txt

# FindUser1.ps1# PowerShell script to query AD for user with specified Common Name.## ----------------------------------------------------------------------# Copyright (c) 2011 Richard L. Mueller# Hilltop Lab web site - http://www.rlmueller.net# Version 1.0 - January 9, 2011## This program demonstrates how to use ADO in PowerShell to query Active# Directory. This example finds the Distinguished Name of all objects# (there could be more than one) that have a specified Common Name.## You have a royalty-free right to use, modify, reproduce, and# distribute this script file in any way you find useful, provided that# you agree that the copyright owner above has no warranty, obligations,# or liability for such use.

# Specify Common Name of user.$strName = "James K. Smith"

$strDomain = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain()$strRoot = $strDomain.GetDirectoryEntry()

$adoConnection = New-Object -comObject "ADODB.Connection"$adoCommand = New-Object -comObject "ADODB.Command"$adoConnection.Open("Provider=ADsDSOObject;")$adoCommand.ActiveConnection = $adoConnection$adoCommand.Properties.Item("Page Size") = 100$adoCommand.Properties.Item("Timeout") = 30$adoCommand.Properties.Item("Cache Results") = $False

$strBase = $strRoot.distinguishedName$strAttributes = "distinguishedName"$strScope = "subtree"

$strFilter = "(cn=$strName)"$strQuery = "<LDAP://$strBase>;$strFilter;$strAttributes;$strScope"$adoCommand.CommandText = $strQuery$adoRecordset = $adoCommand.Execute()

Do{ $adoRecordset.Fields.Item("distinguishedName") | Select-Object Value $adoRecordset.MoveNext()} Until ($adoRecordset.EOF)

$adoRecordset.Close()$adoConnection.Close()

The next program uses the System.DirectoryServices.DirectorySearcher class to query Active Directory. We still are able to specify Page Size, the base of the query, and the LDAP filter. We use the PropertiesToLoad property to specify the attributes values to be retrieved. If we don't use this property, PowerShell will retrieve all attribute values,

Page 63: Ado

which will slow the program. This script will work in PowerShell v1 or v2.

FindUser2.txt

# FindUser2.ps1# PowerShell script to query AD for user with specified Common Name.## ----------------------------------------------------------------------# Copyright (c) 2011 Richard L. Mueller# Hilltop Lab web site - http://www.rlmueller.net# Version 1.0 - January 9, 2011## This program demonstrates how to use the # System.DirectoryServices.DirectorySearcher class to query Active# Directory. This example finds the Distinguished Name of all objects# (there could be more than one) that have a specified Common Name.## You have a royalty-free right to use, modify, reproduce, and# distribute this script file in any way you find useful, provided that# you agree that the copyright owner above has no warranty, obligations,# or liability for such use.

# Specify Common Name of user.$strName = "James K. Smith"

$strDomain = New-Object System.DirectoryServices.DirectoryEntry$objSearcher = New-Object System.DirectoryServices.DirectorySearcher$objSearcher.SearchRoot = $strDomain$objSearcher.PageSize = 100$objSearcher.SearchScope = "subtree"

# Specify attribute values to retrieve.$arrAttributes = @("distinguishedName")ForEach($strAttribute In $arrAttributes){ $objSearcher.PropertiesToLoad.Add($strAttribute) > $Null}

# Filter on object with specified Common Name.$objSearcher.Filter = "(cn=$strName)"

$colResults = $objSearcher.FindAll()ForEach ($strResult In $colResults){ $strDN = $strResult.Properties.Item("distinguishedName") Write-Host $strDN}

Finally we have a PowerShell script that uses the new Active Directory cmdlets in PowerShell v2 installed with Windows Server 2008 R2. This example uses the Get-ADObject cmdlet. We use the LDAPFilter parameter to specify our LDAP filter. This script requires PowerShell v2.

FindUser3.txt

# FindUser3.ps1# PowerShell script to query AD for user with specified Common Name.## ----------------------------------------------------------------------# Copyright (c) 2011 Richard L. Mueller# Hilltop Lab web site - http://www.rlmueller.net# Version 1.0 - January 9, 2011## This program demonstrates how to use the Active Directory cmdlet# Get-ADObject to query Active Directory. This example finds the# Distinguished Name of all objects (there could be more than one) that# have a specified Common Name.## You have a royalty-free right to use, modify, reproduce, and# distribute this script file in any way you find useful, provided that# you agree that the copyright owner above has no warranty, obligations,# or liability for such use.

# Specify Common Name of user.$strName = "James K. Smith"

Page 64: Ado

Get-ADObject -LDAPFilter "(cn=$strName)" -Properties distinguishedName | Format-Table distinguishedName

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