Retrieving information from Active Directory is perhaps one of the most important and frequently used operations. (Writing data into Active Directory or modifying attributes of Active Directory objects is not a challenge if you know the type of these data, i.e., attribute syntax rules, and are familiar with methods of access to data of that type.) Certainly, you can use any standard tools and administrative snap-ins discussed earlier in this book to get information; however, custom scripts allow you to save a lot of time on routine operations or combine results in a way that is most convenient for you. Here are topics that the following scripts aim to demonstrate:
Which data and operations can be valuable for administrators
Where these data are located
How to retrieve data of frequently used types
Before we begin discussing various ADSI scripts, let us consider how to save results for subsequent analysis. As an example, we will use a script that produces more than two thousand lines in total. The following script reads all information about attributes and classes represented in the Active Directory schema (in a particular installation), simultaneously displays results on the screen and writes them to a file. You can insert statements (only four in all!) similar to the bold ones into any script, and save the obtained results if necessary.
All attributes of the abstract schema used — attribute Types, extendedAttributeInfo, objectClasses, and extendedClassInfo — are multi-valued. Thus, this script is also an example of reading properties that may have more than one value. Note that in Windows. NET these attributes are only accessible with the GetInfo Ex method, whereas on Windows 2000 servers you can use the usual GetInfo method.
The following table shows the total number of attributes and classes in Windows 2000 (schema version 13) and Windows. NET (build 3663, schema version 29):
Windows 2000 (schema version 13)
Windows .NET (build 3663, schema version 29)
Listing 17.1. AbstrSchema.vbs — Writing the Definitions of Active Directory Attributes and Classes to a File
Dim objSchema 'As IADsContainer Dim attr Dim x 'As Variant Dim i 'As Integer Dim myFile 'As Variant Dim objFSO 'As Variant Set objSchema = _ Getobject ("LDAP: //CN=Aggregate, CN=Schema, CN=Configuration," + - "DC=net, DC=dom") objSchema.GetInfo Set ObjFSO = Createobject ("Scripting. FileSystemObject") ' Specify your own file name. An existing file will be re-written! Set myFile = ObjFSO. CreateTextFile ("C: \Abstract Schema. txt", True) i = 1 ' ***** Read attributes represented in the schema. ***** ' Here is a sample output string (parentheses are included): ' ( 126.96.36.199 NAME 'cn' SYNTAX '188.8.131.52.4.1.14184.108.40.206.15' ' SINGLE-VALUE ) ' All Microsoft attribute IDs begin with the prefix ''1.2.840.113556.1''. objSchema. GetInfoEx Array (''attributeTypes''), 0 attr = objSchema.Get (''attributeTypes'') myFile.WriteLine ''Attributes'' + vbCrLf For Each x In attr 'WScript.Echo CStr(i) + ''. '' + x myFile.WriteLine CStr(i) + ''. ''.'' + x i = i + 1 Next WScript.Echo '' Attributes (TOTAL) '' + CStr(i - 1) i = 1 ' ***** Read additional information about attributes. ***** ' Here is a sample output string: ' ( 220.127.116.11 NAME 'cn' RANGE-LOWER '1' RANGE-UPPER '64' ' PROPERTY-GUID '3F7996BFE60DD011A28500AA003049E2' ' PROPERTY-SET-GUID '54018<SP>DE4F8BCD111870200C04FB96050' INDEXED ) objSchema.GetInfoEx Array ("extendedAttributeInfo"), 0 attr = objSchema.Get ("extendedAttributeInfo") myFile.WriteLine vbCrLf + "Attributes — Extended information" + vbCrLf For Each x In attr 'WScript. Echo CStr(i) + " . " + x i = i + 1 Next i = 1 ' ***** Read classes represented in the schema. ****** ' Output strings are similar to the following: ' ( 18.104.22.168 NAME 'subSchema' SUP top STRUCTURAL MAY ' (extendedClassInfo $ extendedAttributeInfo $ dITContentRules ' $ attributeTypes $ objectClasses $ modifyTimeStamp ) ) ' Keep in mind that each class also inherits the attribute list of its ' parent class! objSchema.GetInfoEx Array ("objectClasses"), 0 attr = objSchema.Get ("objectClasses") myFile.WriteLine vbCrLf + "Classes" + vbCrLf For Each x In attr 'WScript.Echo CStr(i) + "." + x myFile.WriteLine CStr(i) + "." + x i = i + 1 Next WScript.Echo " Classes (TOTAL) " + CStr(i - 1) i = 1 '***** Read additional information about classes, for example: ' ( 22.214.171.124 NAME 'subSchema' ' CLASS-GUID '61328B5A8DC3D111BBC90080C76670C0' ) objSchema.GetInfoEx Array("extendedClassInfo"), 0 attr = objSchema.Get ("extendedClassInfo") myFile.WriteLine vbCrLf + "Classes - Extended information" + vbCrLf For Each x In attr 'WScript.Echo CStr(i) + "." + x myFile.WriteLine CStr(i) + "." + x i = i + 1 Next WScript .Echo "End." myFile.Close Set objSchema = Nothing Set attr = Nothing Set x = Nothing Set objFSO = Nothing Set myFile = Nothing
From the following script, you can learn how to access the RootDSE object and use two popular interfaces, namely, IADsPropertyList and IADsPropertyEntry. RootDSE is the main source of information about names of Active Directory partitions and Directory Service Agents. (See Chapter 2, "Active Directory Terminology and Concepts," for detailed information on RootDSE.)
This script can also serve as an example of handling ADSI errors.
Listing 17.2. getRootDSE.vbs — Reading the Attributes of a RootDSE Object
Dim objRootDSE 'As IADsPropertyList Dim objProperty 'As IADsPropertyEntry Dim i 'As Integer Dim intCount 'As Integer On Error Resume Next ' Serverless binding is preferable. Set objRootDSE = GetObject ("LDAP://RootDSE") 'Yet in some cases, you may need to bind to a specific DC: ' Set objRootDSE =_ ' GetObject ("LDAP://netdc2.subdom.net.dom/RootDSE") ' Catch possible errors: If Hex (Err.Number) = "8007203A" Then WScript.Echo "ERROR_DS_ SERVER _ DOWN (" & Hex(Err.Number) & ")" WScript.Quit ElseIf Hex(Err.Number) <> 0 Then WScript .Echo "Error", Hex (Err .Number) WScript . Quit End If objRootDSE.GetInfo WScript .Echo "*** RootDSE object on " + objRootDSE.Get ("dnsHostName") - + " ***" + vbCrLf intCount = objRootDSE.PropertyCount For i = 0 To intCount - 1 Set objProperty = objRootDSE. Item(i) WScript.Echo CStr(i + 1) + ") " + objProperty.Name Next WScript.Echo "------------------------------------" WScript .Echo " DSA name: " + objRootDSE. dsServiceName ' Names of Active Directory partitions must be always obtained ' from the RootDSE object only: WScript.Echo " Domain patition: " + _ objRootDSE. Get ("defaultNamingContext") WScript.Echo " Schema partition: " + _ objRootDSE. Get ("schemaNamingContext") WScript. Echo " Configuration partition: " + _ objRootDSE. Get ("configurationNamingContext") WScript.Echo " Highest USN: " + objRootDSE. Get ("highestCommittedUSN") WScript.Echo " Is synchronized? "+ _ objRootDSE. Get ("isSynchronized") WScript. Echo " Is a Global Catalong? " + _ objRootDSE. Get ("isGlobalCatalogReady") Set objRootDSE = Nothing Set objProperty = Nothing
The highest USN committed by the server is one of the basic parameters used for troubleshooting Active Directory replication. Besides, if you write down the USN at a specific moment, you'll be able to determine which objects have been modified since then, for instance, by using a simple command similar to the following one:
search "LDAP: //DC=net, DC=dom" /C: "uSNChanged>=123456" /S: subtree
The isSynchronized attribute indicates whether the DC has replicated all directory partitions after its promotion. Do not confuse this initial replication with a normally scheduled one.
Remember that the isGlobalCatalogReady attribute is TRUE only when the DC has successfully completed its promotion to a GC server. Do not expect that it will be immediately set once you have designated (manually or programmatically) that DC as a GC server.
Here is another example of using the IADsPropertyList and IADsPropertyEntry interfaces with various providers as well as with Global Catalog. By using this code, you can compare a directory object's attribute lists received from different providers or see the attributes replicated to Global Catalog. Only defined attributes (i.e., those that have values) are included in the list. (To see all possible attributes, you must refer to the schema; see Listing 17.4.) You can also view the type of each attribute as it defined in the ADSTYPE enumeration.
The program also illustrates how to use one more important (core) interface — IADsOpenDSObject. This interface is used when you want to explicitly specify the credentials, which the program will use for binding to the directory object. Authentication flags used to define the binding options are listed in the ADS_AUTHENTICATION_ENUM enumeration.
Listing 17.3. Prop-of-obj.bas — Retrieving the Property List of a Directory Object
Option Explicit Sub Main ( ) Dim objcontext As IADsOpenDSObject Dim objAD As IADsPropertyList Dim objProp As IADsPropertyEntry Dim iCount As Integer Dim i As Integer Set objcontext = GetObject ("LDAP:") ' Set objAD = objContext.OpenDSObject _ ' ("LDAP: //CN=John Smith,OU=Staff, DC=net, DC=dom", _ ' vbNullString, vbNullString, _ ' ADS_SECURE_AUTHENTICATION) ' The preceding statement is equal to ' Set objAD = GetObject("LDAP://...") ' i.e. you bind to the object using the credentials ' of the caller (currently logged on user) On Error Resume Next ' Bind using the specified credentials: Set objAD = objContext.OpenDSObject _ ("LDAP://CN=John Smith,OU=Staff,DC=net,DC=dom", - "administrator", "psw",_ ADS_SECURE_AUTHENTICATION) If Hex (Err.Number) = "80072030" Then Debug.Print "LDAP_NO_SUCH_OBJECT (" + Hex(Err.Number) + ")" Exit Sub ElseIf Hex (Err.Number) <> 0 Then ' You may or may not be authenticated: Debug.Print "Error (" + Str(Err.Number) + ") Hex: " + Hex(Err.Number) Exit Sub End If ' As an alternative, you can use the security context of the currently ' logged on user. Instead of all preceding statements choose one ' of the following applicable binding strings: ' * Binding to a user object: ' Set objAD = Getobject ("LDAP: //CN=John Smith,OU=Staff,DC=net,DC=dom") ' * Binding through Global Catalog: ' Set objAD = GetObject ("GC: //CN=John Smith,OU=Staff,DC=net,DC=dom") ' * Binding to a user or group object with use of the WinNT provider: ' Set objAD = GetObject ("WinNT: //NET/jsmith,user") ' Set objAD = GetObject ("WinNT: //NET/Domain Users, group") ' * Binding to the domain object: ' Set objAD = GetObject ("WinNT: //NET, domain") objAD.GetInfo Debug. Print "Attributes of", objAD.ADsPath iCount = objAD.PropertyCount Debug.Print " Total # " + CStr(iCount) For i = 0 To iCount - 1 Set objProp = objAD.Item(i) Debug.Print CStr(i + 1) + ") " + objProp.Name + _ " (type " + CStr(objProp.ADsType) + ")" Next Set objAD = Nothing Set objProp = Nothing End Sub
You might want to retrieve the complete information about a directory object derived from an object class. These data are stored in the Schema partition, and there is a special interface named IADsClass that allows you to access these data. A few methods of this interface as well as the Schema and Class methods of the IADs interface are represented in the following program.
Listing 17.4. Attrs-of-Class.bas — Obtaining Common Information and a List of Possible Attributes for a Directory Class
Option Explicitsub Main() Dim strPath As String Dim objAD As IADs Dim objClass As IADsClass Dim varAttr As Variant Dim arr As Variant Dim n As Integer ' A user account has been specified here; ' You can specify any directory object: strPath = "LDAP: //CN=John Smith,OU=Staff, DC=net, DC=dom" ' Connect to the directory object specified in the path Set objAD = GetObject (strPath) Debug.Print "Schema path: '" + objAD.Schema & "'for " + strPath Debug.Print "Object class: '" + objAD.Class + "'" + vbCrLf Debug.Print "=== The properties of the class ===" ' Retrieve the information about the class — bind to the schema. ' You could omit the previous statements and directly specify the name ' of the necessary object class, for example ''LDAP: //schema/user''. Set objClass = GetObject (objAD.Schema) Debug.Print "Is abstract class? " + CStr (objClass.Abstract) Debug.Print "ADsPath: " + objClass.ADsPath Debug.Print "Is container? " + CStr (objClass.Container) If objClass.Container Then Debug.Print " May contain: " ' The list of object classes that the selected class can contain: arr = objClass.Containment On Error Resume Next If Len(arr) = 0 Then For Each varAttr In arr Debug.Print " - " + varAttr Next Else Debug.Print " - " + objClass.Containment End If End If Debug.Print "Derived from: " + objClass.DerivedFrom Debug.Print "Naming: " + objClass.NamingProperties Debug.Print "Class OID: " + objClass.OID Debug.Print "Possible superiors:" arr = objClass.PossibleSuperiors On Error Resume Next If Len(arr) = 0 Then For Each varAttr In arr Debug.Print " - " + varAttr Next Else Debug.Print " - " + objClass.PossibleSuperiors End If Debug.Print vbCrLf + "=== Attributes ===" Debug.Print "MUST have:" ' The list of mandatory attributes n=0 For Each varAttr In objClass.MandatoryProperties Debug.Print " :: " + varAttr n = n + 1 Next Debug.Print " (TOTAL) : " + CStr (n) Debug.Print Debug.Print "MAY have: " ' The list of optional attributes n=0 For Each varAttr In objClass.OptionalProperties Debug.Print " :: " + varAttr n = n + 1 Next Debug.Print " (TOTAL): " + CStr (n) Set objAD = Nothing Set objClass = Nothing End Sub
Some problems may arise when you attempt to display the values of certain properties. This is often due to selection of an inappropriate format. The following example program displays some property types. Some types, such as Large Integer, NT Security Descriptor, or Octet String (for example, the objectSid property), require special conversion procedures. Take note of the obj.Guid method inherited from the IADs interface. It produces a string that can be used for binding to the object (in the format "LDAP: //<GUID=XXXX…>"); however, this string cannot be used with the Search.vbs script or Guid2obj.exe tool.
Data types are defined in the ADSTYPE enumeration.
Listing 17.5. getProps.vbs - Retrieving the Property Values for a User Object
Option Explicit Sub Main () Dim objPropList As IADsPropertyList Dim objPropEntry As IADsPropertyEntry Dim objVar As IADsPropertyValue Dim v As Variant Dim i As Integer Dim iCount As Integer ' You can select any directory object; a user account, for example: Set objPropList = _ GetObject ("LDAP: //CN=John Smith,OU=Staff, DC=net, DC=dom") objPropList.GetInfo iCount = objPropList.PropertyCount Debug.Print "***** Total " + CStr(iCount) + _ "attributes have values for " + _ objPropList.ADsPath; " * * * * * " Debug.Print "GUID: " + objPropList.Guid For i = 0 To iCount - 1 Set objPropEntry = objPropList.Item(i) 'or = objPropList.Next Debug.Print objPropEntry.Name + " (" + _ CStr (objPropEntry.ADsType) + ") " Select Case objPropEntry.ADsType Case 1 'ADSTYPE_DN_STRING For Each v In objPropEntry.Values Set objVar = v Debug.Print " Value: " + objVar.DNString Next Case 3 'ADSTYPE_CASE_IGNORE_STRING For Each v In objPropEntry.Values Set objVar = v Debug.Print " Value: " + objVar.CaseIgnoreString Next Case 7 'ADSTYPE_INTEGER For Each v In objPropEntry.Values Set objVar = v Debug.Print " Value: " + CStr (objVar.Integer) Next Case 8 'ADSTYPE_OCTET_STRING Debug.Print " .OctetString" Case 9 'ADSTYPE_UTC_TIME For Each v In objPropEntry.Values Set objVar = v Debug.Print " Value: " + CStr (objVar.UTCTime) Next Case 10 'ADSTYPE_LARGE_INTEGER Debug.Print " .LargeInteger" 'Object doesn't support this ' property or method Case 25 'ADSTYPE_NT_SECURITY_DESCRIPTOR Debug.Print " .SecurityDescriptor" ' Object doesn't 'support this property or method Case Else Debug.Print " The value hasn't been converted." End Select Next Set objPropList = Nothing Set objPropEntry = Nothing Set objVar = Nothing End Sub