As I've mentioned earlier in this book, in Active Directory, all objects, except the root object, are grouped under another object, known as a container. The container is often referred to as the parent object, while each object within the container is considered a child object. Active Directory documentation also refers to objects that have no children as leaves or leaf nodes.
In a virtual sense, a container object is simply a regular Active Directory object that is marked as a container. While the Active Directory schema provides a container class object, this does not mean that only container class objects can be containers. You need to understand the importance of this because any object can become a container object, not just objects of the class container. An excellent example of this is the objects representing computers on the network. These are objects of the class computer and are normally contained in the Computers or Domain Controllers container. When a printer is published in Active Directory, an object representing the print queue is created in the directory. The computer that physically hosts the printer is marked as a container, and the new printQueue object is made a child of the computer object.
Figure 6-5 shows a printer as a child of the computer object COPPER2. You need to select a View option in the Active Directory Users and Computers snap-in to show children of users, groups, and computers.
Figure 6-5 Computer object COPPER2 is shown as a container for a printer object.
The concept of containers has many benefits for developers. By allowing any object to be made a container, information that is relevant to all the child objects can be stored in the parent object. For example, information about the organizational unit, such as location information and group policy, can be stored within the parent, an organizationalUnit class object.
Enumeration is the process of retrieving each object within a container. Automation, coupled with Visual Basic, makes this process very easy.
Set objContainer = GetObject("LDAP://CN=Users,DC=coppersoftware,DC=com")
For Each objUser in objContainer
Debug.Print objUser.Name
Next
In this code, each iteration through the For Each loop assigns the variable objUser with an object that is part of the container referenced by the variable objContainer.
This enumeration is made possible by the IADsContainer interface. This interface is made available by ADSI for any object that contains other objects. IADsContainer is a versatile interface supplying several functions for operations such as enumerating container objects, creating and deleting objects, and moving objects. Tables 6-7 and 6-8 list the properties and methods of the IADsContainer interface.
IADsContainer Property | Returned Data Type | Description |
---|---|---|
Count | Long | A count of the number of objects in the container. ad-only. Not supported under Active Directory. |
Filter | Variant array of strings | An array of class names to filter on. When this property is set, only objects of the class that matches the filter are enumerated. The default is to enumerate all classes. |
Hints | Variant array of strings | An array of attribute names to retrieve for each object when enumerating a container. |
get__NewEnum (Not exposed in Visual Basic) | Object | Creates a new enumerator object that supports the IEnumVARIANT interface. Called indirectly from Visual Basic using the For Each statement. Returns an IUnknown interface pointer. Note that there are two underscore (_) characters in the method name. |
Table 6-7 Properties of the IADsContainer interface.
IADsContainer Method | Description |
---|---|
Create | Creates a new object in the container of the class specified. Also accepts a name. Returns an IDispatch interface from which IADs properties and methods can be called. The object is not stored in the directory until the SetInfo method is called. |
Delete | Deletes the object in the container specified by name. You must also supply the class name. The object will not be deleted from the directory until the SetInfo method is called. |
CopyHere | Creates a new copy of the specified object in the container. Not supported under Active Directory. |
GetObject | Binds to an object from the container using the provided relative name. Can also specify class name as added identification. Not required with Active Directory. Returns an IDispatch interface from which IADs properties and methods can be called. |
MoveHere | Moves an object from another container to the current container, optionally providing a new name. Can be used to rename objects within the same container. |
Table 6-8 Methods of the IADsContainer interface.
IADsContainer is unique among the ADSI interfaces in that it provides an enumerator object. An enumerator is a special COM object that allows a program to enumerate through a collection of other objects of the same type. Using the Visual Basic and VBScript For Each statement, you can use this enumerator object. JScript provides support for enumerators through the Enumerator object.
Visual Basic, VBScript, and JScript all handle enumerators transparently. Developers working with C or C++, as usual when it comes to COM, have a little extra work involved. Behind the scenes and hidden from the developer, Visual Basic and the scripting languages use the get__NewEnum property of IADsContainer to have ADSI create the enumerator object. In C and C++, you must call get__NewEnum yourself and then use QueryInterface to get the IEnumVARIANT interface. ADSI provides some help with utility functions that perform some of the steps required in working with enumerators. The ADsBuildEnumerator, ADsEnumerateNext, and ADsFreeEnumerator functions perform some of the work involved with calling get__NewEnum and the IEnumVARIANT interface directly. Enumeration is generic among all C and C++ programs using COM, so I won't go into the details here.
If you are working with C or C++, be careful to note that two underscore characters appear in get__NewEnum. Technically, the IADs-Container interface defines a read-only property called _NewEnum, with one underscore. The single underscore indicates a special restricted property. Since you only need to use _NewEnum from C or C++, and in those environments you are required to prefix method calls to properties with either get_ or put_, I've listed it here as get__NewEnum, which is the name of the function you need to call from the IADsContainer interface.
The IADsContainer interface has two properties that you can set to help control the enumeration process. The Filter property is used when you just want to enumerate certain classes of objects. Doing this is extremely helpful when a container has a large number of objects and you are interested only in a certain type. The Filter property actually takes an array of strings, where each string is the class name that should be included in the enumeration.
The filtering is done on the client-side. The client must still access all the objects of the container; however, only objects matching the filter are enumerated. Filtering on the client might be a performance issue with large containers. Take, for example, the enumeration of group objects in the Users container of a company with 40,000 employees and hundreds of groups. In those kinds of cases, letting the server do the filtering is best. This approach was discussed in Chapter 5, "Searching Active Directory."
The Hints property is another way to increase performance. Like the Filter property, it takes an array of strings, each with the name of an attribute to retrieve. When enumerating, ADSI usually binds to each enumerated object and loads all the attributes of the object using the GetInfo method. This can be a huge waste of time if you're only interested in a certain number of attributes of the object. Listing 6-4, from the EnumContainer.bas sample on the companion CD, shows how to use the Filter and Hints properties in Visual Basic to enumerate all the Organizational Unit containers in the directory. By removing or commenting the line varClasses = Array("organizationalUnit") and by deleting the reference to varClasses(0) in the Debug.Print line, you can enumerate all objects in the directory recursively. (For a C++ sample that shows how to enumerate objects in a container, see the EnumContainers folder on the companion CD.)
Option Explicit
Dim nIndentLevel As Double ` Global variable to track nesting level
Public Sub Main()
` Enumerate all the containers in the directory partion
Dim objRootDSE As IADs
Dim strPath As String
Dim objContainer As IADsContainer
Dim varClasses As Variant
` Connect to the LDAP server's root object
Set objRootDSE = GetObject("LDAP://RootDSE")
` Form a ADsPath string to the name of the default domain
strPath = "LDAP://" + objRootDSE.Get("defaultNamingContext")
` Connect to the directory specified in the path
Set objContainer = GetObject(strPath)
` Display the name of the object
Debug.Print objContainer.Name
` Setup array of classes to enumerate
varClasses = Array("organizationalUnit")
` Display ADsPath being used
Debug.Print "Listing " & varClasses(0) & " objects at " _
& strPath & "..." ` Enumerate the container
Call EnumContainer(objContainer, varClasses)
End Sub
Public Function EnumContainer(objContainer As IADsContainer, _
varSchemaClasses As Variant)
Dim objADs As IADs
Dim objClass As IADsClass
` Increase the indent level
nIndentLevel = nIndentLevel + 1
` Only enumerate certain classes
objContainer.Filter = varSchemaClasses
` Retrieve only the Name and Schema attributes
objContainer.Hints = Array("Name", "Schema")
` Loop through each object in the container
For Each objADs In objContainer
` Indent text according the nesting level
Debug.Print String(nIndentLevel, vbTab);
` Display the name of the object
Debug.Print objADs.Name
` Get the class of object
Set objClass = GetObject(objADs.Schema)
` Check whether this class is a container
If (objClass.Container = True) Then
` Recurse to enumerate this container
Call EnumContainer(objADs, varSchemaClasses)
End If
Next
` After going through a container, reduce the indent level
nIndentLevel = nIndentLevel - 1
End Function
Listing 6-4 EnumContainer.bas shows how to use the Filter and Hints properties to enumerate all the Organizational Unit containers in the directory.
The IADsContainer interface is also the interface you use to add objects to the directory. Once again, ADSI makes this a near trivial task. The function below accepts two strings, one a name for the new object and the other the class name for the new object. Active Directory uses the class name to determine what attributes are available for the object. You must call SetInfo to update the directory, otherwise the object is only created locally and will be discarded when the object reference is released.
The name of an object is given as the RDN for the new object. Generally this is "CN=MyObject". In the case of an Organizational Unit container, this would be "OU=MyOrgUnit". The following function shows how to create new objects in a container:
` Creates a new object in the container specified
Public Function CreateObject(strClass As String, strName As String, _
strADsPath As String) As IADs
Dim objContainer As IADsContainer
Dim objClass As IADsClass
Dim objNewObject As IADs
` Bind to the object at the path given
Set objContainer = GetObject(strADsPath)
` Get the class of object
Set objClass = GetObject(objContainer.Schema)
` Check whether this class is a container
If (objClass.Container = True) Then
` Create new object in the container
Set objNewObject = objContainer.Create(strClass, strName)
` Write the new object to the directory
objNewObject.SetInfo
` Return the new object back to the caller
Set CreateObject = objNewObject
Else
` Not a container, exit
Debug.Print objContainer.Name & " at "; _
strADsPath; " is not a container."
CreateObject = Nothing
End If
End Function
Deleting an object is nearly identical to creating one. Once again, the class name and RDN are used to identify the object within the container. Actually, under Active Directory and the ADSI LDAP provider, the object's RDN is sufficient to uniquely identify the object within a container. However, other directory services require both parameters and, as such, both are required when you call the Delete method of the IADsContainer interface.
To delete an object, you must ask the object's container to delete the object; you cannot perform the operation directly on the object itself. In the following function, the object to be deleted is named objADsToDelete, and I use its Parent method (from the IADs interface) to get the distinguished name string to its parent container. Then I use the Class and Name properties to identify the object to the container when calling the Delete method.
` Deletes the object
Public Function DeleteObject(objADsToDelete As IADs)
Dim objContainer As IADsContainer
` Get the parent of the object
Set objContainer = GetObject(objADsToDelete.Parent)
` Call the parent container's Delete method
` Specify object class and name
objContainer.Delete objADsToDelete.Class, objADsToDelete.Name
` Explicitly discard object reference, no longer valid
Set objADsToDelete = Nothing
` Update the directory
objContainer.SetInfo
End Function
If the object is a container itself, it must be empty or Active Directory will report an error. You must delete all the contained objects first. Fortunately, ADSI helps out again, supplying an interface you can use to wipe out the container and all the objects contained within it: the IADsDeleteOps interface.
The IADsDeleteOps interface is used for performing sweeping deletion operations in the directory. It can delete the current object and all objects contained within it, which is useful for removing chunks of the directory tree without having to enumerate each child object and delete it using the Delete method of IADsContainer.
The IADsDeleteOps interface has only one member, the DeleteObject method. This method uses the current object—which supports the IADsDeleteOps interface—enumerates any child objects and deletes the current object. After the call to DeleteObject, the reference to the object is no longer valid and should be released by setting the variable to Nothing (in Visual Basic and VBScript) or by calling the Release method (in C or C++).
The DeleteObject method accepts a number as a parameter, but at this time the parameter is unused. You can use 0 when calling DeleteObject. The following function shows how to use the IADsDeleteOps interface and the DeleteObject method.
` Deletes the current object and any subobjects
Public Function DeleteTree(objTreeToDelete As IADsDeleteOps)
` Call the parent containers Delete method
` Specify object class and name
objTreeToDelete.DeleteObject (0)
` Explicitly discard object reference, no longer valid
Set objTreeToDelete = Nothing
End Function
The following code, from the CreateDelete sample on the companion CD, shows how to call the functions shown earlier to create and delete objects. It creates two randomly named organizational unit containers in the root of Active Directory and then proceeds to delete them.
Option Explicit
` Create then delete a series of objects
Sub Main()
Dim objRootDSE As IADs
Dim strADsPath As String
Dim objContainer As IADsContainer
Dim objADs As IADs
Dim strName As String
` Connect to the LDAP server's root object
Set objRootDSE = GetObject("LDAP://RootDSE")
` Form an ADsPath string to the name of the default domain
strADsPath = "LDAP://" + adsRootDSE.Get("defaultNamingContext")
` Connect to the directory specified in the path
Set objContainer = GetObject(strADsPath)
` Display the name of the object
Debug.Print objContainer.Name
` Create, then delete 2 organizational unit objects in the
` directory root
Dim X As Long
For X = 1 To 2
` Assign a name to the new object using a random number
Randomize
strName = "Test" & Int((1000 * Rnd) + 1)
` Call function to create object
` Note that name must be an RDN
Set objADs = CreateObject("organizationalUnit", _
"OU=" & strName, strADsPath)
` Display info about new object
Debug.Print objADs.Name, objADs.Guid
` Delete the object
Call DeleteObject(objADs)
` Delete the object and all objects contained within it
`Call DeleteTree(objADs)
` Discard the reference
Set objADs = Nothing
Next X
End Sub