Editor's note: This article is the second part of a 12-part series on Active Directory Service Interfaces (ADSI). The series started in the January 1999 issue.
In Part 1 of this series, I said I would cover the property cache this month. However, I realized that the information about the property cache makes more sense if you understand how to manipulate Active Directory (AD) objects in Windows 2000 (Win2K—formerly Windows NT 5.0) instead. Therefore, I'll discuss that topic in this article.
The scripts in this article are in Visual Basic Script (VBScript) and manipulate objects in the AD. However, you can use any automation language and apply the script's concepts to manipulate objects in any supported ADSI namespace. In addition, all the scripts use VBScript's GetObject method, which assumes that you are already logged on to the domain tree with an administrator equivalent account. If you aren't logged on with an administrator equivalent account, you need to use IADsOpenDSObject's OpenDSObject method, which I covered in Part 1.
The easiest way to show how to manipulate objects with ADSI is through a series of real-world examples—specifically, through simple tasks that form the building blocks of everyday scripting. To that end, imagine that you want to perform the following tasks on nt5domain.mycorp.com in your AD tree:
- Create an organizational unit called Sales under the domain root.
- Create a group called Managers in the Sales organizational unit.
- Create two users named Sue Peace and Keith Cooper in the default Users container.
- Add both users to the Managers group.
- Remove both users from the Managers group.
- Delete the users and the Managers group.
- Delete the Sales organizational unit.
In addition to these seven simple tasks, I'll show you how to perform two complex tasks: Test membership of both users in the group and list all group members. I chose these two tasks, because they have corresponding ADSI methods.
Learning how to perform the seven simple tasks and the two complex tasks provides a great introduction to how ADSI works. You need to use most of the major ADSI interfaces to perform these nine tasks.
Task 1: Creating an Organizational Unit
AD has two basic types of objects: directory service leaf objects and directory service container objects (which I refer to simply as containers). The main difference between them is that container objects can hold other objects (both leaf and container), but leaf objects can't hold other objects.
AD's standard containers are Namespaces, Country, Locality, Organization, OrganizationalUnit, Container, Domain, and Computer. The most common container that you will be using throughout the coming months is the OrganizationalUnit. You create this container in the AD hierarchy to not only contain other objects, but also to delegate administrative responsibility. It acts similarly to the resource domains in a master-domain model under NT 4.0. AD has 11 standard leaf objects, including the User object. (For more information about AD objects and ADSI in general, see The Active Directory Programmer's Guide at http://msdn.microsoft.com/developer/windowsnt5/adsi/actdirguide.htm and Steve Judd's white paper "Migrating to the Active Directory" at http://www.microsoft.com/developer/news/janfeb97/nt4lead.htm.)
The process of creating a container, such as one representing the Sales organizational unit, is the same as creating any other type of object. As Listing 1 shows, you use VBScript's Set statement with the GetObject method to establish the object variable myContainer as a reference, or pointer, to the object that the ADsPath "LDAP://dc=nt5domain,dc=mycorp,dc=com" represents. (For more information about object variables, ADsPaths, and pointers, see Part 1. In addition, you can download all the listings in this article from http://www.winntmag.com/newsletter/scripting.) In this case, the pointer references the object representing the root of the domain tree. Because this location in the tree is a container rather than a leaf object, you can use the IADsContainer interface methods and properties on myContainer.
From any container, you can use the IADsContainer interface to directly create, delete, and manage other AD objects. Think of IADsContainer as the interface that lets you manage the directory hierarchy. A second interface, IADs, goes hand in hand with IADsContainer. IADs works on any object, whereas IADsContainer works on containers only. Table 1 lists the IADs and IADsContainer methods.
As the second line in Listing 1 shows, you're using IADsContainer's Create method to create an OrganizationalUnit container that represents Sales. You pass two arguments to this method: the schema class name for the class of object that you want to create (i.e., "organizationalUnit") and the relative distinguished name (RDN) of the object (i.e., "ou=Sales").
RDNs use a letter code to indicate the class of object. In Request for Comments (RFC) 1779, "A String Representation of Distinguished Names," the ISODE Consortium Network Working Group defined a few letter codes as standard. Table 2 contains the code list from RFC 1779. (You can find the full RFC 1779 report at http://src.doc.ic.ac.uk/computing/internet/rfc/rfc1779.txt.) Any object class that doesn't have a specific letter code uses the default of CommonName (CN). All AD objects have a common name anyway, no matter what their class, but the system can uniquely identify an object only by its appropriate code.
At this point, you might think that you've created the Sales organizational unit container, but you haven't. You must use the IADs SetInfo method to execute this line of code to create the object. Execution is necessary, because ADSI implements a system in which the OS first writes the object and its properties to an area of memory called the property cache on the client executing the script. Each object has a separate property cache. When you use SetInfo, you're telling each cache to write any creations or modifications to AD. This process might sound counterintuitive but, in fact, it makes sense for many reasons, most of which concern reducing network traffic.
Because AD creates objects in a hierarchical fashion, you can put your SetInfo commands at any point in the script as long as you put them in the correct order. The system doesn't check the legality of the properties or Lightweight Directory Access Protocol (LDAP) ADsPaths until the SetInfo statement.
Theoretically, you can perform all your cache writes at the end of the script. However, this practice doesn't offer any benefits and has several potential disadvantages. Bunching cache writes at the end of a script can lead to confusion if you think properties exist in the underlying directory service during later portions of the script, but they don't. Bunching cache writes also encourages you to neglect proper error-checking and progress-logging procedures. Finally, if you bunch up cache writes and the server crashes, none of your writes will have gone through. For these reasons, I recommend that you flush the caches as soon as possible.
Task 2: Creating a Group
Until now, you've worked with AD objects. ADSI offers objects for you to use as well. Take, for example, ADSI's Group object. With a name such as Group, you might think that this object is a container. However, the Group object is a leaf object, because it represents a list that contains pointers to other objects and not the actual objects.
Another misconception is that the Group and OrganizationalUnit objects are interchangeable. They aren't interchangeable, because the objects that the Group object points to (i.e., Users, Groups, and Computers) can be on multiple domains in a site, whereas the objects in an OrganizationalUnit must be on the same domain. In addition, you use the Group and the OrganizationalUnit objects for different purposes. You use Group objects to manage access to shared resources, create distribution lists, and control access rights of small groups. You use OrganizationalUnit objects to create hierarchical groupings in a directory and control access rights of large groups.
The Group object has a separate interface: IADsGroup. When you want to access and manipulate the data in a Group object, you use the IADsGroup methods. Table 1 lists those methods.
You use the same method to create Group objects that you use to create OrganizationalUnit containers: IADsContainer's Create method. However, as Listing 2 shows, there are two important differences. First, you must use cn= rather than ou= with the RDN to specify that the object type is CommonName. Second, you need to include a mandatory property. Although the OrganizationalUnit container has no mandatory properties that you must set before writing out the cache, the Group object does. Group objects require that you set the sAMAccountName property in the cache. This property represents the Security Accounts Manager (SAM) Account Name, which is the name of the user or group as it appears in previous NT versions (NT 4.0 and NT 3.51). Systems sometimes need the SAM Account Name to communicate with older servers.
You might wonder why the property for SAM Account Name is sAMAccountName. Classes, properties, and attributes follow these naming guidelines: Concatenate all words and capitalize the first letter of each word, except for the first letter of the first word, which must be lowercase, no matter what case it was before.
In Listing 2, the IADs Put method sets the SAM Account Name. This property has no default value. At this point in the script, you can also set any optional properties, which can save you time later. For example, the Group object has six additional properties that you can set. In this case, however, you don't need any additional properties, so you can apply SetInfo.
Task 3: Creating Users
Having created the Sales organizational unit and Managers group, you can now create users in the Users container. The Users container is actually an instance of the class called Container. A Container is exactly what it says it isa container of objects, no more, no less. The default containers that the OS creates when you install Win2K are Containers; hence, Container is the default container for users.
You can't delegate a user as the administrator for the hierarchy below a Container, like you can with OrganizationalUnit objects. And since the demise of NT 5.0 beta 1, you also can't directly create this type of object with the OS GUI, which indirectly implies that Microsoft doesn't want administrators creating Containers either. Although you can create Containers programmatically, you probably won't have much need to, because an OrganizationalUnit object has a superset of the Container's properties, so you can use an OrganizationalUnit object instead. If you can think of a good reason for creating a Container rather than an OrganizationalUnit object, please email me.
As Listing 3 shows, you create a user by using VBScript's GetObject method to create a pointer to the Users container. You then call the IADsContainer's Create method on the Users Container, stipulating the class of object to create (user) and the object's common name. Finally, you set a mandatory property as you did for the group, and call SetInfo.
Task 4: Adding Users to a Group
Adding a user to a group is simple with IADsGroup's Add method. You just pass one argument: the DN of the object that you want to add to the group. The code in Listing 4 adds two users—Sue Peace and Keith Cooper—to the Managers group.
With the addition of these two users, you've created the directory hierarchy that Figure 1 depicts. Figure 1 also shows the corresponding object reference names.
Task 5: Removing Users from a Group
Up to this point, you've been creating the hierarchy that Figure 1 depicts. Now I'll show you how to tear it down. Let's start with removing the two users from the group. As the code in Listing 5 shows, you use IADsGroup's Remove method. Once again, you simply pass the DN as the argument.
Tasks 6-7: Deleting the Users, the Group, and the Organizational Unit
Listing 6 contains the code to delete both users, the Managers group, and the Sales organizational unit. Each delete line uses a method I haven't shown you before. When deleting objects, you can use the IADs Name method to retrieve an object's programmatic identifier (progID), which saves you from having to type it in.
If you want to delete a container, you must first delete all its children. If these children are containers with objects, you have to recursively delete these containers and their children before you can delete the parent. If you attempt to delete a container object that still has objects in it, you receive an error message that states The directory service can perform the requested operation only on a leaf object.
However, if you attempt to delete a group that still has members or is a member of other groups, the delete call will succeed. The call succeeds because the membership relationships aren't true parent-child relationships within AD.
I recommend that you use VBScript's Nothing keyword after any Delete statement to disassociate the object variable from the object. Setting the value of each object to Nothing might seem unnecessary, especially when the script is due to end soon. However, you can save yourself hassles if you get into this habit. Here's why. After you've deleted an object from the underlying directory service, the property cache for that object still exists. If you don't remove the reference to that object and then you use that reference, you're referring to information that no longer exists. Trying to use SetInfo or GetInfo on a deleted object's property cache generates a failure. I once spent hours fixing a script in which I had made this mistake; now I always use Nothing to disassociate my variables.
Your script is all but complete except for the two complex tasks of testing for group membership and listing group members. To complete these tasks, you can use IADsGroup's IsMember and Members methods. IsMember is easier, so I'll cover that method first.
Task 8: Testing for Group Membership
The IsMember method takes one argument: the DN of the user you want to test for group membership. This method returns a Boolean value (i.e., true or false). As a result, you can use IsMember in an If...Then...Else statement. (If you're unfamiliar with the If...Then...Else statement, check out Microsoft's VBScript Language Reference Web site at http://premium.microsoft.com/msdn/library/tools/vbscript/htm/vbstoc.htm.)
As Listing 7 shows, the rest of the code uses the same types of calls and methods I covered previously, so the code is self-explanatory—with one exception: WScript.Echo. Because you're writing this code in VBScript, you're using the Windows Scripting Host (WSH) scripting engine to run it. Thus, to display the results, you use the Echo method of the WScript object, which is WSH's primary object. If a user is not a member of the group, Echo displays the message Is a Member! If the user isn't a member, Echo displays the message Is NOT a Member!
Task 9: Listing Group Members
The Members method is different from all the others you've used so far. This method returns a pointer to an ADSI Members object. The Members object, in turn, lets you create and manage sets of object pointers. For example, the Members object lets you create a list of pointers to User objects that represent the members of a group.
When you call IADsGroup's Members method, it creates a Members object, which has a separate interface, IADsMembers. As Table 1 shows, this interface has three methods. To complete the enumerating task, you need the Count method.
You can enumerate the members of a group many different ways. Using VBScript's For Each...In...Next loop is the most commonly used approach. Listing 8 shows how it works. You begin the script by using GetObject to create a pointer to the container that holds the Group object you want to enumerate (in this case, the Managers group).
The Echo method then displays the message Number of members of the group, followed by the number of members. You use IADsMembers' Count method to provide the number of members. The way in which the script retrieves this count demonstrates ADSI's support of multiple methods. Scripts resolve methods from left to right. Thus, with adsGroup.Members.Count, the script first resolves adsGroup.Members (i.e., creates a Members object). Because the Members object supports the IADsMembers Count method, you can add .Count to the end of adsGroup.Members. If you're still confused, think of the situation this way:
Set adsMembers = adsGroup.Members
WScript.Echo "Number of members of the group: " & adsMembers.Count
Both approaches provide the same output. I prefer using the simpler, shorter approach when possible.
After the script opens a dialog box stating the number of members, it opens successive dialog boxes for each member, displaying the member's name. You use VBScript's For Each...Next statement to generate these dialog boxes. (If you're unfamiliar with this type of statement, see Microsoft's VBScript Language Reference Web site.) As the For Each...Next loop executes, adsMember ends up holding a class of object called Members. Remember that the string ads on the front of a variable indicates an ADSI object, so this loop isn't generating a standard count. The Members object supports IADs, as do all objects, so you can just use the IADs Name property to get the name for that member of the group and print it—a very neat solution.
Bringing It All Together
Using all the elements I've discussed, I've created the NineTasks.vbs script, which you can find on the Win32 Scripting Journal Web site (http://www.winntmag.com/newsletter/scripting). In this script, the only explicit LDAP paths I used were for the Users container and the root of the domain. Every other reference uses object.ADsPath. The script uses object.Name to retrieve names.
You can explore NineTasks.vbs on your own. If you do, you'll be even more prepared for next month's article. I'll be covering the property cache—honest!
This article is adapted from Alistair G. Lowe-Norris' forthcoming book about the Windows 2000 Active Directory to be published by O'Reilly and Associates.