Вы находитесь на странице: 1из 64

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:

The search base - The ADsPath to start the search, enclosed in angle brackets. For example, to start the search in the Sales OU of the MyDomain.com domain you might use a search base as follows: "<LDAP://ou=Sales,dc=MyDomain,dc=com>" The ADsPath can use either the LDAP or GC providers. You would use the GC provider to search for information in other trusted domains, but only attributes replicated to the Global Catalog are available. The search filter - A clause that specifies the conditions that must be met for records to be included in the resulting recordset. The attribute values for all objects meeting the conditions are included in the recordset. The syntax of the search filter is explained below. An example to filter for all user objects would be: "(&(objectCategory=person)(objectClass=user))" The attributes to return - A list of Active Directory attributes separated by commas. Use the LDAP display names of the attributes. An example would be: "sAMAccountName,displayName,description" Note that most property methods cannot be returned by ADO. For example, "LastName" is a property method whose value cannot be returned by ADO. The only property methods that can be returned by ADO are "Name" and "ADsPath". Also, the "tokenGroups", "tokenGroupsGlobalAndUniversal", and "tokenGroupsNoGCAcceptable" attributes cannot be retrieved by ADO. These are the only SID syntax operational attributes. If any of these attributes are listed in the attribute clause, the resulting recordset is empty. Other operational attributes, such as "canonicalName" and "modifyTimeStamp" can be retrieved using ADO. For the "tokenGroups" attributes, you must retrieve the "distinguishedName", bind to the corresponding object, then use the GetInfoEx method to load the attribute values into the local property cache. The search scope - This can be one of three values. "Base" means that only the object represented by the search base is included in the search. No child containers, OU's, or objects (like users) are included. This is used to check for the existence of the base object. You might assign the ADsPath of a user object as the base of a search and use a scope of "base" to check for existence of the user. "OneLevel" means the search only includes immediate children of the base, like the users in an OU. If the base of the search is the ADsPath of an OU, and the filter is to return only organizational unit objects, then a scope of "OneLevel" will return all child OU's of the base, but not the base OU. "Subtree" (the default) means the search includes the base, all children of the base, and the entire Active Directory structure below the search base. The Range Limits - Specifies which records in a multi-valued attribute are to be returned. This clause is optional, but if it is used, it must be the fourth clause in the query string between the attribute list and the search scope. As an example, to include records indexed by 0 through 999, you would use: "Range=0-999"

An example query string, with no Range Limits, would be: "<LDAP://ou=Sales,dc=MyDomain,dc=com>;(objectCategory=computer)" _ & ";sAMAccountName;Subtree" An example with Range Limits would be: "<LDAP://cn=Users,dc=MyDomain,dc=com>;(objectCategory=group)" _ & ";member;Range=0-999;Base" Only the Base and Attribute clauses are required. If there is no Filter clause, use two semicolons between the Base and Attribute clauses. The recordset will include all objects specified by the Base and Scope clauses. If there is no Scope clause, the search scope defaults to Subtree. A simple query string to return the Distinguished Names of all objects in Active Directory would be: "<LDAP://dc=MyDomain,dc=com>;;distinguishedName" The only part of the query string that is case sensitive is the LDAP or GC provider name, which must be in all capitals, and any Boolean values (either TRUE or FALSE), which must also be in all capitals. The query string is assigned to the "CommandText" property of the ADO Command object. An ADO Connection object specifies the provider used to connect to Active Directory. The Execute method of the Command object executes the query and returns a Recordset object. See the link above for alternative methods to retrieve recordsets using ADO. Several properties of the ADO command object can be assigned values to make the query more efficient. In particular, you can assign a value to the "Page Size" property. This specifies the number of rows of the Recordset object that are retrieved at one time. If no value is assigned, a maximum of 1000 rows will be retrieved. You can assign any value to "Page Size" up to 1000. This turns on paging, which means that ADO retrieves the number of rows you specify repeatedly until all rows are retrieved, no matter how many there are. It has been found that it makes very little difference what value you assign, as long as you assign a value so that paging is enabled. You enumerate the records in the Recordset object in a loop. For example, a complete program to retrieve the sAMAccountName and cn attributes of all user objects in the domain is shown below. To make this example more generic, the RootDSE object is used to retrieve the default naming context, which is the DNS name of the domain the computer has authenticated to. You could hard code the Distinguished Name of the domain instead. Option Explicit Dim adoCommand, adoConnection, strBase, strFilter, strAttributes Dim objRootDSE, strDNSDomain, strQuery, adoRecordset, strName, strCN ' Setup ADO objects. Set adoCommand = CreateObject("ADODB.Command") Set adoConnection = CreateObject("ADODB.Connection") adoConnection.Provider = "ADsDSOObject"

adoConnection.Open "Active Directory Provider" Set adoCommand.ActiveConnection = adoConnection ' Search entire Active Directory domain. Set objRootDSE = GetObject("LDAP://RootDSE") strDNSDomain = objRootDSE.Get("defaultNamingContext") strBase = "<LDAP://" & strDNSDomain & ">" ' Filter on user objects. strFilter = "(&(objectCategory=person)(objectClass=user))" ' Comma delimited list of attribute values to retrieve. strAttributes = "sAMAccountName,cn" ' Construct the LDAP syntax query. strQuery = strBase & ";" & strFilter & ";" & strAttributes & ";subtree" adoCommand.CommandText = strQuery adoCommand.Properties("Page Size") = 100 adoCommand.Properties("Timeout") = 30 adoCommand.Properties("Cache Results") = False ' Run the query. Set adoRecordset = adoCommand.Execute ' Enumerate the resulting recordset. Do Until adoRecordset.EOF ' Retrieve values and display. strName = adoRecordset.Fields("sAMAccountName").Value strCN = adoRecordset.Fields("cn").value Wscript.Echo "NT Name: " & strName & ", Common Name: " & strCN ' Move to the next record in the recordset. adoRecordset.MoveNext Loop ' Clean up. adoRecordset.Close adoConnection.Close You step through the recordset in a loop, using the MoveNext method of the Recordset object to advance to the next record. If you forget to call the MoveNext method, the "Do Until" loop will never meet the EOF (End Of File) condition and the loop will never end. You retrieve values with the Fields collection of the Recordset object. You specify the name of the attribute you are retrieving with the Fields collection. The Value property of the Fields collection is the default property. In the example above I specified several properties for the Command object. These are not necessary, but can improve performance. It is good practice to close the Recordset and Connection objects when you are done. The search filter specifies all conditions that must be met for a record to be included in

the Recordset. Each condition is in the form of a conditional statement in parentheses, such as "(cn=TestUser)", which has a Boolean result. The general form of a condition is an attribute name and a value separated by an operator, which is usually the equals sign "=". The attribute cannot be operational (also known as constructed), since the values of these attributes are only calculated by the Domain Controller on demand and are not saved in Active Directory. Other operators that can separate attribute names and values are ">=", and "<=" (the operators "<" and ">" are not supported). Conditions can be combined using the following operators. & - The "And" operator (the ampersand). All conditions operated by "&" must be met in order for a record to be included. | - The "Or" operator (the pipe symbol). Any condition operated by "|" must be met for the record to be included. ! - The "Not" operator (the exclamation point). The condition must return False to be included. Conditions can be nested using parenthesis. In addition, you can use the "*" wildcard character in the search filter. However, the wildcard character cannot be used with Distinguished Name attributes (attributes of data type DN), such as the distinguishedName, memberOf, directReports, and managedBy attributes. If the value in a filter includes any of the following characters, the character must be escaped, since it has special meaning in filters: *()\ These characters are escaped using the backslash escape character, "\", and the 2 digit ASCII hex equivalent of the character. Replace the "*" character with "\2A", the "(" character with "\28", the ")" with "\29", and the "\" character with "\5C". For example, to find all objects where cn is equal to "James (Jim)" you can use the filter: (cn=James \28Jim\29) To find all objects where description is equal to "5 * 3 \ 2" use the filter: (description=5 \2A 3 \5C 2) Actually, you can escape any character in this manner. Some search filter examples follow. To return all user objects with cn (Common Name) beginning with the string "Joe": "(&(objectCategory=person)(objectClass=user)(cn=Joe*))" To return all user objects. This filter is more efficient than the one using both objectCategory and objectClass, but is harder to remember: "(sAMAccountType=805306368)"

To return all computer objects with no entry for description: "(&(objectCategory=computer)(!description=*))" To return all user and contact objects: "(objectCategory=person)" To return all group objects with any entry for description: "(&(objectCategory=group)(description=*))" To return all groups with cn starting with either "Test" or "Admin": "(&(objectCategory=group)(|(cn=Test*)(cn=Admin*)))" To return all objects with Common Name "Jim * Smith": "(cn=Jim \2A Smith)" To retrieve the object with GUID = "90395FB99AB51B4A9E9686C66CB18D99": "(objectGUID=\90\39\5F\B9\9A\B5\1B\4A\9E\96\86\C6\6C\B1\8D\99)" To return all users with "Password Never Expires" set: "(&(objectCategory=person)(objectClass=user)" _ & "(userAccountControl:1.2.840.113556.1.4.803:=65536))" To return all users with disabled accounts: "(&(objectCategory=person)(objectClass=user)" _ & "(userAccountControl:1.2.840.113556.1.4.803:=2))" To return all distribution groups: "(&(objectCategory=group)" _ & "(!groupType:1.2.840.113556.1.4.803:=2147483648))" To return all users with "Allow access" checked on the "Dial-in" tab of the user properties dialog of Active Directory Users & Computers. This is all users allowed to dial-in. Note that "TRUE" is case sensitive: "(&(objectCategory=person)(objectClass=user)" _ & "(msNPAllowDialin=TRUE))" To return all user objects created after a specified date (09/01/2007): "(&(objectCategory=person)(objectClass=user)" _ & "(whenCreated>=20070901000000.0Z))"

To return all users that must change their password the next time they logon: "(&(objectCategory=person)(objectClass=user)" _ & "(pwdLastSet=0))" To return all users that changed their password since 2/5/2004. See the link below for a function to convert a date value to an Integer8 (64-bit) value. The date 2/5/2004 converts to the number 127,204,308,000,000,000: "(&(objectCategory=person)(objectClass=user)" _ & "(pwdLastSet>=127204308000000000))" To return all users with the group "Domain Users" designated as their "primary" group: "(&(objectCategory=person)(objectClass=user)" _ & "(primaryGroupID=513))" The group "Domain Users" has the primaryGroupToken attribute equal to 513. To return all users with any group other than "Domain Users" designated as their "primary" group: "(&(objectCategory=person)(objectClass=user)" _ & "(!primaryGroupID=513))" To return all users not required to have a password: "(&(objectCategory=person)(objectClass=user)" _ & "(userAccountControl:1.2.840.113556.1.4.803:=32))" To return all users that are direct members of a specified group. You must specify the Distinguished Name of the group. Wildcards are not allowed: "(&(objectCategory=person)(objectClass=user)" _ & "(memberOf=cn=TestGroup,ou=Sales,dc=MyDomain,dc=com))" To return all computers that are not Domain Controllers: "(&(objectCategory=Computer)" _ & "(!userAccountControl:1.2.840.113556.1.4.803:=8192))" There is a new filter, called LDAP_MATCHING_RULE_IN_CHAIN, but it is only available if your Active Directory is installed on Windows 2003 SP2 or Windows 2008 (or above). This filter can only be used with DN attributes, like member or memberOf, but walks the hierarchical chain of objects to reveal nesting. For example, to find all groups that a specific user is a member of, even due to group nesting: "(member:1.2.840.113556.1.4.1941:=cn=Jim Smith,ou=Sales,dc=MyDomain,dc=com)" Or to find all members of a specified group, even due to group nesting:

"(memberOf:1.2.840.113556.1.4.1941:=cn=Test Group,ou=West,dc=MyDomain,dc=com)" To return all user accounts that do not expire. The value of the accountExpires attribute can be either 0 or 2^63-1: "(&(objectCategory=person)(objectClass=user)" _ & "(|(accountExpires=9223372036854775807)(accountExpires=0)))" See the link below for a program that converts a date time value to the equivalent Integer8 (64-bit) value. This program converts the date 2/5/2004 to the equivalent Integer8 value of 127204308000000000 (depending on your time zone, and whether daylight savings time is in affect). DateToInteger8.txt
' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' 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, str64Bit Dim 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.Quit End 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 = lngBiasKey ElseIf (UCase(TypeName(lngBiasKey)) = "VARIANT()") Then lngBias = 0 For k = 0 To UBound(lngBiasKey) lngBias = lngBias + (lngBiasKey(k) * 256^k) Next End 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 ' 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 person person person computer user contact computer person contact group person organizationalPerson group organizationalPerson organizationalPerson objectClass user contact user Result user objects user and contact objects contact objects user and computer objects computer objects user and contact objects contact objects computer objects user, computer, and contact objects user and contact objects group objects group objects user and contact objects user, computer, and contact objects user and contact objects

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

You can use the program linked below to experiment with various filters in your domain. The program prompts for the base of the ADO query, the LDAP syntax filter, and the comma delimited list of attribute values to retrieve. The program displays the values of the specified attributes for all objects matching the specified filter in the specified base (and child containers). GenericADO 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: * ( ) \ \2A \28 \29 \5C

NUL

\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 Dim Dim Dim Dim Dim adoCommand, adoConnection, strBase, strFilter, strAttributes objRootDSE, strBaseDN, strQuery, adoRecordset arrAttributes, k, intCount, strValue, strItem, strType objValue, lngHigh, lngLow, lngValue, strAttr, dtmValue objShell, lngBiasKey, lngBias, dtmDate, blnCSV, strLine strMulti, strArg

blnCSV = False If (Wscript.Arguments.Count = 1) Then strArg = Wscript.Arguments(0) Select Case LCase(strArg) Case "/csv" blnCSV = True End Select End 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 = lngBiasKey ElseIf (UCase(TypeName(lngBiasKey)) = "VARIANT()") Then lngBias = 0 For k = 0 To UBound(lngBiasKey) lngBias = lngBias + (lngBiasKey(k) * 256^k) Next End If Set objShell = Nothing ' 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 If If (InStr(LCase(strBaseDN), "dc=") = 0) Then Set objRootDSE = GetObject("LDAP://RootDSE") strBaseDN = strBaseDN & "," & objRootDSE.Get("defaultNamingContext") strBaseDN = Replace(strBaseDN, ",,", ",") End If strBase = "<LDAP://" & strBaseDN & ">" ' Prompt for filter. strFilter = Trim(InputBox("Enter LDAP syntax filter")) If (Left(strFilter, 1) <> "(") Then strFilter = "(" & strFilter End If If (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 = strAttributes If (strAttributes = "") Then strAttributes = "distinguishedName" Else strAttributes = "distinguishedName" & "," & strAttributes End If arrAttributes = Split(strAttributes, ",") ' Construct the LDAP syntax query. strQuery = strBase & ";" & strFilter & ";" & strAttributes & ";subtree" adoCommand.CommandText = strQuery adoCommand.Properties("Page Size") = 200 adoCommand.Properties("Timeout") = 30 adoCommand.Properties("Cache Results") = False If (blnCSV = False) Then Wscript.Echo "Base of query: " & strBaseDN Wscript.Echo "Filter: " & strFilter Wscript.Echo "Attributes: " & strAttr Else ' Output header line for csv. strLine = "DN" For k = 1 To UBound(arrAttributes) strLine = strLine & "," & arrAttributes(k) Next Wscript.Echo strLine End If ' Run the query. ' Trap possible errors. On Error Resume Next Set adoRecordset = adoCommand.Execute If (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.Quit End If On Error GoTo 0 ' Enumerate the resulting recordset. intCount = 0 Do Until adoRecordset.EOF ' Retrieve values and display. intCount = intCount + 1 If (blnCSV = True) Then strLine = """" & adoRecordset.Fields("distinguishedName").Value & """" 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

Else

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 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) 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.MoveNext

Loop If (blnCSV = False) Then Wscript.Echo "Number of objects found: " & CStr(intCount) End If ' Clean up. adoRecordset.Close adoConnection.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 GUIDStr GUIDStr GUIDStr GUIDStr GUIDStr GUIDStr GUIDStr GUIDStr = = = = = = = = = Mid(strHexGUID, 7, 2) GUIDStr & Mid(strHexGUID, GUIDStr & Mid(strHexGUID, GUIDStr & Mid(strHexGUID, GUIDStr & Mid(strHexGUID, GUIDStr & Mid(strHexGUID, GUIDStr & Mid(strHexGUID, GUIDStr & Mid(strHexGUID, GUIDStr & Mid(strHexGUID, 5, 2) 3, 2) 1, 2) 11, 2) 9, 2) 15, 2) 13, 2) 17)

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 lngTemp lngTemp lngTemp = = = = arrbytSID(15) lngTemp * 256 + arrbytSID(14) lngTemp * 256 + arrbytSID(13) lngTemp * 256 + arrbytSID(12)

HexSIDToDec = HexSIDToDec & "-" & CStr(lngTemp) Exit Function End If HexSIDToDec = "S-" & arrbytSID(0) & "-" _ & arrbytSID(1) & "-" & arrbytSID(8) lngTemp lngTemp lngTemp lngTemp = = = = arrbytSID(15) lngTemp * 256 + arrbytSID(14) lngTemp * 256 + arrbytSID(13) lngTemp * 256 + arrbytSID(12)

HexSIDToDec = HexSIDToDec & "-" & CStr(lngTemp) lngTemp lngTemp lngTemp lngTemp = = = = arrbytSID(19) lngTemp * 256 + arrbytSID(18) lngTemp * 256 + arrbytSID(17) lngTemp * 256 + arrbytSID(16)

HexSIDToDec = HexSIDToDec & "-" & CStr(lngTemp) lngTemp lngTemp lngTemp lngTemp = = = = arrbytSID(23) lngTemp * 256 + arrbytSID(22) lngTemp * 256 + arrbytSID(21) lngTemp * 256 + arrbytSID(20)

HexSIDToDec = HexSIDToDec & "-" & CStr(lngTemp) If (UBound(arrbytSID) > 23) lngTemp = arrbytSID(27) lngTemp = lngTemp * 256 lngTemp = lngTemp * 256 lngTemp = lngTemp * 256 Then + arrbytSID(26) + arrbytSID(25) + 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 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 = $False If ($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) { $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") > $Null ForEach ($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 = 0 ForEach ($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("""", """""") + """" } 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)} } 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"} 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 $False) {"Number of objects found: $Count"} } } If ($Csv -eq $True) {$Line}

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 multivalued attribute, and will be an array if there is one or more values. For example, if the list of attributes includes the sAMAccountName and memberOf attributes, you could enumerate the Recordset object with a loop similar to: Do Until adoRecordset.EOF strName = adoRecordset.Fields("sAMAccountName").Value Wscript.Echo "User: " & strName arrGroups = adoRecordset.Fields("memberOf").Value If IsNull(arrGroups) Then Wscript.Echo "-- No group memberships" Else For Each strGroup In arrGroups Wscript.Echo "-- Member of group: " & strGroup Next End If adoRecordset.MoveNext Loop It should be pointed out that the "description" attribute of user objects is actually multivalued. However, it can only have one value. It is treated as a normal string by ADSI, but not by ADO. ADO returns either a Null (if the "description" attribute has no value) or an array of one string value. You must use code similar to above for this attribute. If any attribute value can be missing, be sure to account for the possibility that a Null is retrieved from the recordset. An error will be raised if you attempt to echo a Null. For example, if you retrieve the value of the displayName attribute of all objects in Active Directory, many objects will not have this attribute. Other objects will have the attribute, but no value will be assigned. Neither of these situations will raise an error when the recordset is retrieved, but you may need to convert the Null value into a string to avoid a type mismatch error. For example: Do Until adoRecordset.EOF ' The displayName attribute may not have a value assigned.

' Appending a blank string, "", converts a Null into a blank string. strName = adoRecordset.Fields("displayName").Value & "" Wscript.Echo strName Loop Other Active Directory attributes are Integer8. This means that they are 64-bit (8 byte) values, usually representing dates. These must be treated using the techniques at this link - Integer8 Attributes. For example, the pwdLastSet attribute is Integer8. If you use ADO to retrieve Integer8 attribute values, the following code will not invoke the IADsLargeInteger interface and will raise an error: Do Until adoRecordset.EOF ' This does not invoke the IADsLargeInteger interface. Set objDate = adoRecordset.Fields("pwdLastSet") ' This statement raises an error. lngHigh = objDate.HighPart ' Likewise, the Intger8Date function, documented in the ' link above, raises an error. dtmDate = Integer8Date(objDate, lngTZBias) adoRecordset.MoveNext Loop You must either specify the Value property of the Field object and use the Set keyword: Do Until adoRecordset.EOF ' Specify the Value property of the Field object. Set objDate = adoRecordset.Fields("pwdLastSet").Value ' Invoke methods of the IADsLargeInteger interface directly. lngHigh = objDate.HighPart ' Or use the Integer8Date function documented in the link above. dtmDate = Integer8Date(objDate, lngTZBias) adoRecordset.MoveNext Loop Or, you must assign the value to a variant, and then use the Set keyword to invoke the IADsLargeInteger interface: Do Until adoRecordset.EOF ' Assign the value to a variant. lngDate = adoRecordset.Fields("pwdLastSet") ' Use the Set keyword to invoke the IADsLargeInteger interface. Set objDate = lngDate ' Invoke methods of the IADsLargeInteger interface directly. lngHigh = objDate.HighPart ' Or use the Integer8Date function documented in the link above. dtmDate = Integer8Date(objDate, lngTZBias) adoRecordset.MoveNext Loop

Some attributes are Boolean, such as msNPAllowDialin and IsDeleted. If you retrieve the value of such an attribute, it will be either True or False. For example: Do Until adoRecordset.EOF strName = adoRecordset.Fields("sAMAccountName").Value blnAllow = adoRecordset.Fields("msNPAllowDialin").Value If (blnAllow = True) Then Wscript.Echo "User " & strName & " is allowed to dial in" End If adoRecordset.MoveNext Loop Integer8 Attributes Many attributes in Active Directory have a data type (syntax) called Integer8. These 64bit 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. Set objDomain = GetObject("LDAP://dc=MyDomain,dc=com") ' Retrieve lockoutDuration with IADsLargeInteger interface. Set objDuration = objDomain.lockoutDuration lngHigh = objDuration.HighPart lngLow = objDuration.LowPart

' Adjust for error in IADsLargeInteger interface. If (lngLow < 0) then lngHigh = lngHigh + 1 End 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.maxStorage lngHigh = objMax.HighPart lngLow = objMax.LowPart lngValue = lngHigh * (2^32) + lngLow Wscript.Echo "HighPart: " & lngHigh Wscript.Echo "LowPart: " & lngLow Wscript.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 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 10,737,418,238 10,737,418,239 10,737,418,240

HighPart 2 2 2

LowPart 2,147,483,646 2,147,483,647 -2,147,483,648

Value 10,737,418,238 10,737,418,239 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. 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 Explicit Dim strUserDN, lngTZBias, objUser, objPwdLastSet Dim 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 = lngBiasKey ElseIf (UCase(TypeName(lngBiasKey)) = "VARIANT()") Then lngTZBias = 0 For k = 0 To UBound(lngBiasKey) lngTZBias = lngTZBias + (lngBiasKey(k) * 256^k) Next End 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 ' 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 0 End 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, str64Bit Dim 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.Quit End 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 = lngBiasKey ElseIf (UCase(TypeName(lngBiasKey)) = "VARIANT()") Then lngBias = 0 For k = 0 To UBound(lngBiasKey) lngBias = lngBias + (lngBiasKey(k) * 256^k) Next End 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 ' 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 64bit 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) ' 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.

# 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.MoveNext Loop You must either specify the Value property of the Field object and use the Set keyword: Do Until adoRecordset.EOF ' Specify the Value property of the Field object. Set objDate = adoRecordset.Fields("pwdLastSet").Value ' Invoke methods of the IADsLargeInteger interface directly. lngHigh = objDate.HighPart ' Or use the Integer8Date function documented in the link above. dtmDate = Integer8Date(objDate, lngTZBias) adoRecordset.MoveNext Loop Or, you must assign the value to a variant, and then use the Set keyword to invoke the IADsLargeInteger interface: Do Until adoRecordset.EOF ' Assign the value to a variant. lngDate = adoRecordset.Fields("pwdLastSet") ' Use the Set keyword to invoke the IADsLargeInteger interface. Set objDate = lngDate ' Invoke methods of the IADsLargeInteger interface directly. lngHigh = objDate.HighPart ' Or use the Integer8Date function documented in the link above. dtmDate = Integer8Date(objDate, lngTZBias) adoRecordset.MoveNext Loop 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 Next Set objDate = objUser.lockoutTime If (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.MoveNext Loop 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 lastLogon lastLogonTimeStamp pwdLastSet accountExpires lockoutTime badPasswordTime No Value Yes Yes 0 Yes Yes Yes Yes Yes Yes 2^63 - 1 Date Yes Yes Yes Yes Yes Yes

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 = 30042820 objDate.LowPart = 1500000 objUser.Put "accountExpires", objDate objUser.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 = -3 objDomain.LowPart = -410065408 objDomain.Put "minPwdAge", objDate objDomain.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 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 cn=Last, First cn=Windows 2000/XP cn=Sales\Engr cn=E#Test Escaped in VBScript cn=Last\, First cn=Windows 2000\/XP cn=Sales\\Engr 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. 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. 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 cn=James "Jim" Smith Escaped in PowerShell string "cn=James \`"Jim\`" Smith"

cn=James $ Smith cn=James $ Smith cn=Sally Wilson + Jones cn=William O'Brian cn=William O'Brian cn=William O`Brian cn=Richard #West cn=Roy Johnson$

"cn=James `$ Smith" 'cn=James $ Smith' "cn=Sally Wilson \+ Jones" "cn=William O'Brian" 'cn=William O`'Brian' "cn=William O``Brian" "cn=Richard #West" "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 = 3 Const ADS_NAME_TYPE_NT4 = 3 Const 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") 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 = strQuery adoCommand.Properties("Page Size") = 100 adoCommand.Properties("Timeout") = 30 adoCommand.Properties("Cache Results") = False ' Run the query. Set adoRecordset = adoCommand.Execute ' Enumerate the resulting recordset. Do Until adoRecordset.EOF ' Retrieve values. strDN = adoRecordset.Fields("distinguishedName").value ' Replace any "/" characters with "\/". ' 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.MoveNext Loop ' Clean up. adoRecordset.Close adoConnection.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 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: * ( ) \ NUL \2A \28 \29 \5C \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.MoveNext Loop Finally, some attributes are OctetString, which is a byte array. The array must be converted to a hexadecimal string before it can be displayed. Examples include logonHours, and objectGUID. For an example of a function to convert OctetString values to a hexadecimal string, see this program - IsMember Function 8. 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". 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. 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, objGroupList Dim 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 Wscript.Echo "User " & objADUser.name _ & " is a member of group " & strGroup Else Wscript.Echo "User " & objADUser.name _ & " is NOT a member of group " & strGroup End If strGroup = "Domain Users" If (IsMember(objADUser, strGroup) = True) Then Wscript.Echo "User " & objADUser.name _ & " is a member of group " & strGroup Else Wscript.Echo "User " & objADUser.name _ & " is NOT a member of group " & strGroup End If strGroup = "Front Office" If (IsMember(objComputer, strGroup) = True) Then Wscript.Echo "Computer " & objComputer.name _ & " is a member of group " & strGroup Else Wscript.Echo "Computer " & objComputer.name _ & " is NOT a member of group " & strGroup End If ' Clean up.

If (IsObject(adoConnection) = True) Then adoConnection.Close End 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 ' 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) Next End 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) { # 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) { "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 }

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' " _ & "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: "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.

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 Explicit Dim adoConnection, strBase, strFilter, strAttributes Dim 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")

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 = strQuery adoRecordset.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.MoveNext Loop ' Clean up. adoRecordset.Close adoConnection.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

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.MoveNext Loop ' 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 Explicit Dim adoCommand, adoConnection, strBase, strFilter, strAttributes Dim 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 = adUseClient adoConnection.Open "Active Directory Provider" Set adoCommand.ActiveConnection = adoConnection ' Search entire Active Directory domain. Set objRootDSE = GetObject("LDAP://RootDSE")

strDNSDomain = objRootDSE.Get("defaultNamingContext") strBase = "<LDAP://" & strDNSDomain & ">" ' Filter on user objects. strFilter = "(&(objectCategory=person)(objectClass=user))" ' Comma delimited list of attribute values to retrieve. strAttributes = "sAMAccountName,cn" ' Construct the LDAP syntax query. strQuery = strBase & ";" & strFilter & ";" & strAttributes & ";subtree" adoCommand.CommandText = strQuery adoCommand.Properties("Page Size") = 100 adoCommand.Properties("Timeout") = 30 adoCommand.Properties("Cache Results") = False ' Run the query. Set adoRecordset = adoCommand.Execute ' 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.MoveNext Loop ' Clean up. adoRecordset.Close adoConnection.Close

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 = &H1 Const ADS_USE_ENCRYPTION = &H2 Const ADS_USE_SSL = &H2 Const ADS_USE_SIGNING = &H40 Const ADS_USE_SEALING = &H80 Const ADS_USE_DELEGATION = &H100 Const 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 = "TestUser@MyDomain.com" strUser = "MyDomain\TestUser" strPassword = "xyz12345" ' Create the ADO Connection object. Set adoConnection = CreateObject("ADODB.Connection") adoConnection.Provider = "ADsDSOObject" adoConnection.Properties("User ID") = strUser adoConnection.Properties("Password") = strPassword adoConnection.Properties("Encrypt Password") = True adoConnection.Properties("ADSI Flag") = ADS_SERVER_BIND _ Or ADS_SECURE_AUTHENTICATION adoConnection.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, adoConnection Dim strBase, strFilter, strAttributes, strQuery, adoRecordset Dim strDN, strUser, strPassword, objNS, strServer Const ADS_SECURE_AUTHENTICATION = &H1

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") = strUser adoConnection.Properties("Password") = strPassword adoConnection.Properties("Encrypt Password") = True adoConnection.Properties("ADSI Flag") = ADS_SERVER_BIND _ Or ADS_SECURE_AUTHENTICATION adoConnection.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 = strQuery adoCommand.Properties("Page Size") = 100 adoCommand.Properties("Timeout") = 30 adoCommand.Properties("Cache Results") = False Set adoRecordset = adoCommand.Execute

' Enumerate the resulting recordset. Do Until adoRecordset.EOF ' Retrieve values. strDN = adoRecordset.Fields("distinguishedName").Value Wscript.Echo strDN adoRecordset.MoveNext Loop ' Clean up. adoRecordset.Close adoConnection.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.

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 Explicit Dim objRootDSE, strDNSDomain, adoConnection Dim strBase, strFilter, strAttributes, strQuery, adoRecordset Dim strLast, strFirst, intCount, strName Const adOpenStatic = 3 Const adLockOptimistic = 3 Const 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 = adoConnection adoRecordset.CursorLocation = adUseClient adoRecordset.CursorType = adOpenStatic adoRecordset.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 = strQuery adoRecordset.Open ' Disconnect the recordset. Set adoRecordset.ActiveConnection = Nothing adoConnection.Close

' Sort the recordset. adoRecordset.Sort = "sn,givenName" adoRecordset.MoveFirst ' Enumerate the resulting recordset. intCount = 0 Do 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.MoveNext Loop ' 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 Explicit Dim objRootDSE, strDNSDomain, adoConnection Dim strBase, strFilter, strAttributes, strQuery, adoRecordset Dim intCount Const adOpenStatic = 3 Const adLockOptimistic = 3 Const 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 = adoConnection adoRecordset.CursorLocation = adUseClient adoRecordset.CursorType = adOpenStatic adoRecordset.LockType = adLockOptimistic

' 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 = strQuery adoRecordset.Open ' Disconnect the recordset. Set adoRecordset.ActiveConnection = Nothing adoConnection.Close ' Filter the recordset on objects of category person. adoRecordset.Filter = "objectCategory='cn=Person," _ & "cn=Schema,cn=Configuration,dc=MyDomain,dc=com'" intCount = adoRecordset.RecordCount Wscript.Echo "Number of persons: " & CStr(intCount) ' Filter the recordset on objects of category computer. adoRecordset.MoveFirst adoRecordset.Filter = "objectCategory='cn=Computer," _ & "cn=Schema,cn=Configuration,dc=MyDomain,dc=com'" intCount = adoRecordset.RecordCount Wscript.Echo "Number of computers: " & CStr(intCount) ' Filter the recordset on objects of category group. adoRecordset.MoveFirst adoRecordset.Filter = "objectCategory='cn=Group," _ & "cn=Schema,cn=Configuration,dc=MyDomain,dc=com'" intCount = adoRecordset.RecordCount Wscript.Echo "Number of groups: " & CStr(intCount) ' Enumerate the groups. adoRecordset.MoveFirst Do Until adoRecordset.EOF Wscript.Echo adoRecordset.Fields("sAMAccountName").Value adoRecordset.MoveNext Loop ' Filter the recordset on objects of category OU.

adoRecordset.MoveFirst adoRecordset.Filter = "objectCategory='cn=Organizational-Unit," _ & "cn=Schema,cn=Configuration,dc=MyDomain,dc=com'" intCount = adoRecordset.RecordCount Wscript.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.MoveFirst adoRecordset.Filter = "memberOf='cn=Accountants,ou=West,dc=MyDomain,dc=com'" intCount = adoRecordset.RecordCount Wscript.Echo "Number of members of Accountants group: " & CStr(intCount)

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/299410 http://msdn.microsoft.com/en-us/library/aa746379(VS.85).aspx

http://msdn.microsoft.com/en-us/library/aa746494(VS.85).aspx http://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.

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. Windows Windows Windows AD Server 2003 2000 Server Server 2003 LDS R2 X X X X X X X X X X X X Windows Server 2008 X X X X X X X X X X X X X X

Attribute

displayName givenName legacyExchangeDN msDSAdditionalSamAccountName msDS-PhoneticCompanyName msDS-PhoneticDepartment msDS-PhoneticDisplayName msDS-PhoneticFirstName msDS-PhoneticLastName physicalDeliveryOfficeName X proxyAddresses X Name X sAMAccountName X sn X

X X X X X

X X X

X 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*)) 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

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, 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 GetADObject 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"

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/enus/sas_ads_emwf.mspx http://www.microsoft.com/resources/documentation/windows/2000/server/scriptguide/enus/sas_ads_jgtf.mspx http://www.microsoft.com/resources/documentation/windows/2000/server/scriptguide/enus/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

Вам также может понравиться