In "Querying and Updating AD, Part 1," February 2003, http://www.winscriptingsolutions.com, InstantDoc ID 27569, and "Querying and Updating AD, Part 2," March 2003, InstantDoc ID 37717, I cover how you can use the Net::LDAP Perl modules to automate your Active Directory (AD) infrastructure with Perl and standard Lightweight Directory Access Protocol (LDAP). But some of the basic functions you might need to perform, such as searching for or deleting objects, can't always be carried out fully with the standard LDAP operations. In some circumstances, LDAP imposes limitations on the client to prevent it from doing something it shouldn't, such as accidentally deleting an entire tree of the directory hierarchy. However, in many situations, you truly need to perform the actions that the default LDAP operations don't provide. These situations are where LDAP controls come into play.
Internet Engineering Task Force (IETF) Request for Comments (RFC) 2251 (http://www.ietf.org/rfc/rfc2251.txt) defines LDAP controls as part of the LDAP version 3 (LDAPv3) specification. Controls are an important feature of LDAP because they let vendors build extensions to LDAP operations on top of a directory server without revising the LDAP specification. You can include controls with a particular client request, and if the server supports the controls, the server performs the special processing that the controls dictate. Typically, controls follow the Internet standards process and are published in RFCs.
The LDAP controls that I cover in this article perform server-side sorting of search results, paged searching for more efficient processing of search results, and the deletion of an entire directory subtree in one operation. AD supports these controls and many more. For a complete list of the supported controls, go to http://msdn.microsoft.com/library/en-us/netdir/ldap/extended_controls.asp.
Controls and Net::LDAP
Before I delve into using specific controls, let's briefly review how you use controls with Net::LDAP. If you're new to Net::LDAP, I recommend reading "Querying and Updating AD, Part 1" for a general introduction to Net::LDAP and information about where to obtain it and how to install it. As I write this article, Net::LDAP 0.26 is the current version, so I use it for the scripts I describe later.
As you might expect with Net:: LDAP, controls are instantiated as objects. The Net::LDAP::Control module lets you create a new control by specifying parameters to the new() method with code such as
Net::LDAP::Control->new(parm, parm, parm)
where parm is a parameter that depends on the type of control you're instantiating. Each control typically has specific parameters that you can use to customize its behavior. After you've instantiated the control object, you can pass it as a parameter to the operation you want to extend. Most methods available within Net::LDAP that map to LDAP operations have an optional control parameter that accepts an array reference of control objects. This parameter lets you specify more than one control if necessary. Now let's get our feet wet with the first control.
Perhaps one of the most straightforward controls to use is the server-side sort control, which RFC 2891 (http://www.ietf.org/rfc/rfc2891.txt) defines. The RFC title "LDAP Control Extension for Server Side Sorting of Search Results" makes this control's purpose obvious. When you perform generic LDAP searches, the results don't come back in any kind of ordered fashion, which might be inconvenient if you want to present the returned data in a front-end interface for users. You could retrieve all the search results, store them in a data structure such as a hash or array, and sort them after the query has been completed. However, this type of sorting isn't efficient for large result sets. The more optimal solution, at least from the client viewpoint, is to have the server sort the search results so that the client can start displaying the entries in a sorted fashion as soon as it receives them.
Listing 1 shows an example of how to use the server-side sort control. The script searches for and prints all user objects in the Global Catalog (GC) that have a last name (sn). This example is contrived, but it illustrates how to use the control, and you can easily adapt the code to perform a useful task in your environment. The code at callout A in Listing 1 contains the module inclusions. Using strict is always a good idea—even in trivial scripts—because you never know when those scripts might turn into something more complex. Three Net::LDAP modules are required to perform the search and use the control. The Net::LDAP::Constant module retrieves the LDAP_CONTROL_SORTREQUEST constant, which contains a unique number that represents the control that the script later passes to the Net::LDAP::Control module.
The code at callout B in Listing 1 contains all the script configuration settings—that is, the variables you need to modify to make the script work in your environment. I've documented all the variables in the script so that you'll know what each one is used for. The code at callout C in Listing 1 shows the connection to the domain controller (DC) on the GC port (port 3268) and the binding information. Note the use of the version option in the Net::LDAP->new() method invocation. Net::LDAP 0.26 uses LDAPv2 as its default version of LDAP. Because controls weren't introduced until LDAPv3, you must use the version parameter to turn on LDAPv3 support. In a future version of Net::LDAP, LDAPv3 will be the default.
The code at callout D in Listing 1 is the guts of the script. The first line creates the server-side sort control object. I pass in the LDAP_CONTROL_SORTREQUEST constant, which I imported in the code at callout A, and an order parameter, which specifies the attribute on which the results should be sorted. The next step is to perform the query. I pass the standard search parameters plus a control parameter, which takes an array reference of control objects. The remainder of the code iterates through each matching entry and prints out the sn attribute for each entry.
One important limitation of using Net::LDAP to query AD is related to the LDAP administrative limit on searching. To help limit the amount of processing required for queries that match thousands of entries, directory servers such as AD define administrative limits for the maximum number of entries that any one query can return. By default, AD's administrative limit is 1000. Thus, if you perform a query that matches thousands of entries, AD will return only 1000.
If the query in Listing 1 matches more than 1000 entries, the script aborts with the error message Sizelimit exceeded. If I removed the line
die $search->error if $search->code;
the script would iterate through 1000 of the matching entries. Typically, this behavior isn't what you want. You usually want the search to return all matching entries regardless of the administrative limit. For that reason, RFC 2696 (http://www.ietf.org/rfc/rfc2696.txt) defines an LDAP control for simple paged-results manipulation. Several directory servers, including AD, support this control. The paged-results control informs the directory server that the search might return a lot of entries and you want to retrieve them in pages, or chunks. To work around the administrative limit, you specify that the script should return entries in pages of less than or equal to 1000 entries.
Listing 2 shows how to use the paged-results control, which is a little more complicated than the server-side sort control. In the code at callout A in Listing 2, I import the LDAP_CONTROL_PAGED constant so that I can use the paged-results control. In the code at callout B in Listing 2, I set the search filter ($filter) to match any user object in the GC and the @attrs array, which will contain a list of attributes to return. Specifying the attributes you want to return is important because otherwise the query returns all attributes for each entry, which can greatly increase the time it takes to run the script and the amount of memory it requires.
The code at callout C in Listing 2 instantiates a new paged-results control object and sets the size parameter (i.e., the number of entries to be returned for each page) to 1000. If you specify a number larger than the administrative limit defined on the server, the script will revert to the defined administrative limit during the results processing. The next section of code at callout C defines the @args array, which contains the key/value pairs that are passed to the search() method. The search() method is part of a while loop so that the script will continue to call the method until the method returns no more entries. Inside the while loop, a foreach loop processes each entry returned in the current search page. Then, the code at callout C sets a cookie to inform the server that I want the next page of entries. If this call fails for any reason, the script exits the loop.
Another important control to understand is the tree-delete control. As I show in "Querying and Updating AD, Part 2," you can use the delete() method of a Net::LDAP object ($ldap) to delete objects in the directory. The delete() method has one significant limitation: It can't delete containers or objects that are parents of other objects.
For example, if you tried to delete an organizational unit (OU) that contained 100 objects as follows:
$rc = $ldap->delete($dn_to_delete);
you would receive the error message 0000208C: UpdErr: DSID-030A02AF, problem 6003 (CANT_ON_NON_LEAF), data 0. This error message states that you can't delete a nonleaf object (i.e., a container). The tree-delete control lets you delete a container and all its objects. The control was initially created as an Internet draft (http://www.alternic.org/drafts/drafts-a-b/draft-armijo-ldap-treedelete-02.html) and was intended to be an RFC, but the RFC was never formally made and the draft eventually expired. Nevertheless, AD supports this control.
The script in Listing 3 uses the tree-delete control to delete the OU container ou=sales,dc=mycorp,dc=com. The code at callout A in Listing 3 is similar to that of the previous two listings except that it doesn't use the Net::LDAP::Constant module. At publication time, the Net::LDAP modules didn't have a constant for the tree-delete control, so I had to create my own constant that returns the Object Identifier (OID) for the control. The OID is what the script actually passes to the Net::LDAP::Control module's new() method.
The code at callout B in Listing 3 contains some of the same configuration parameters as in Listing 1 and Listing 2 plus a $dn_to_delete variable, which contains the distinguished name (DN) of the object or container you want to delete. The code at callout C in Listing 3 first instantiates the tree-delete control object. The control doesn't require any additional parameters. To use the control, I specify in the delete() method a control parameter that points to an array reference that contains the tree-delete control object. The code at callout C finishes by checking for an error and printing the results.
Controls are an important feature of LDAP because they let you extend LDAP's functionality without breaking the LDAP specification. These examples of how to use the server-side sort, paged-search, and tree-delete controls should get you started using LDAP controls against AD.