Programming Basics
Since LDAP is a lightweight protocol, programming using LDAP APIs is easy when you have an understanding of the basic concepts. LDAP introduces a lot of new terminology, such as distinguished names, search filters, search scope, and RootDSE, to name a few. Most of these terms do not exist outside the LDAP or X.500 world. Also, the object-oriented information model used for LDAP is substantially different than that of, say, a database. Those who are familiar with object-oriented programming should quickly adjust to LDAP's notion of classes, attributes, and objects.
Information and Naming Models
Before delving into the programmatic aspects of LDAP, you must first have a solid understanding of the LDAP information and naming models used within Active Directory. The Active Directory information model is based on the X.500 model of entries, known as objects, and attributes. An object is defined by a class, which is itself defined by a collection of attributes. A class may inherit attributes from other classes. The Active Directory schema contains the structure for all the classes and attributes that define objects. For more information see Chapter 10, "Schema."
At the root of the class tree is the top class. All classes must inherit directly from the top class or a class that inherited from the top class either directly or indirectly. Figure 3.6 illustrates both cases of directly and indirectly inheriting from the top class. The group class inherits directly from top, whereas the user class inherits from top indirectly via the organizationalPerson class which inherits from the person class which inherits from top.
Figure 3.6 Class hierarchy for the user and group classes.
All objects within Active Directory have a unique name called the distinguished name (DN). This DN is the path you would follow to find the object when traversing the directory tree. For example, the administrator's user object in the xyz.com forest has a DN of "cn=administrator,cn=users,dc=xyz,dc=com." Each component, such as "cn=administrator," of the DN has exactly one parent, in this case "cn=users." This makes the DN of an object unique throughout the forest. For more information on DNs, see RFC 2253.
Connecting, Binding, and Unbinding
Establishing a connection, binding, and unbinding against an LDAP server is straightforward. In LDAP terms, when you bind as a user, you are authenticating as that user. In Listing 3.1, the Perl code shows an example of setting up a connection, binding as the administrator account, and unbinding.
Listing 3.1 Connecting, Binding, and Unbinding as Administrator Using Net::LDAP
use Net::LDAP; $ldap = Net::LDAP->new('dc1.xyz.com') or die "Could not connect: $@"; $result = $ldap->bind('cn=administrator,cn=users,dc=xyz,dc=com', password => 'password'); die $result->error if $result->code; # do stuff $ldap->unbind; # tear down the connection
One feature Microsoft added as part of its LDAP implementation was the ability to bind with a User Principal Name (UPN). So instead of binding with a DN such as "cn=administrator,cn=users,dc=xyz,dc=com," you can bind with "administrator@xyz.com." The bind statement from Listing 3.1 could be changed as follows:
$result = $ldap->bind('administrator@xyz.com', password => 'password');
Although this is not supported in other directory servers, it is highly recommended when writing applications specifically for Active Directory. The major benefit is that you do not have to hardcode the DN, which contains the path to the user account, in your programs. If someday you want to move the administrator account to a different container or organizational unit, you would have to change all scripts that authenticated using the DN of the administrator. If you are using the UPN, the move is transparent.
RootDSE
The Root Directory Server Entry (RootDSE) is a vital resource in discovering information about an LDAP directory. The RootDSE was added as part of the LDAP v3 specification, so all LDAP v3-compliant directories should have a RootDSE entry. In Active Directory, it should be used as a starting point for your applications to discover what naming contexts are available, where the domain, schema, and configuration naming contexts are located, and which controls are supported. See Figure 3.7 for an example of the information available from the RootDSE of a domain controller. To obtain the RootDSE, perform a search with base equal to "", filter equal to "(objectclass=*)", and the scope set to "base". Since the RootDSE does not require authentication, no binding is necessary. Listing 3.2 uses Net::LDAP to query the RootDSE:
Listing 3.2 Search Method for Accessing RootDSE with Net::LDAP
$ldap = Net::LDAP->new('dc1.xyz.com') or die " Could not connect: $@"; # notice the bind isn't called because it is an anonymous connection $rootdse = $ldap->search( base => '', filter => '(objectclass=*)', scope => 'base', ); die $rootdse->error if $rootdse->code; foreach $entry($rootdse->entries) { foreach $attr(sort $entry->attributes) { print "$attr: $_\n" foreach $entry->get_value($attr); } }
Figure 3.7 Information returned from RootDSE.
Listing 3.3 shows a shortcut method in Net::LDAP called root_dse() which makes accessing the RootDSE even easier.
Listing 3.3 Shortcut Method for Accessing RootDSE with Net::LDAP
$ldap = Net::LDAP->new('dc1.xyz.com') or die "Could not connect: $@"; $rootdse = $ldap->root_dse; foreach $attr($rootdse->attributes) { foreach $value ($rootdse->get_value($attr)) { print "$attr: $value\n"; } }
Search Filters
Search filters are a very important yet initially very confusing aspect of LDAP. Search filters define the search criteria for querying and obtaining information from a directory. It is very important to have a good understanding of filters because they are commonly used in most LDAP-related APIs, including ADSI.
Filters are to an LDAP server what SQL is to a database server. Unoptimized search filters can negatively impact the performance of an LDAP server just as unoptimized SQL queries can negatively impact a database server.
A filter is a string comprising one or more prefix notation attribute-value comparisons. RFC 2254 defines LDAP search filters in detail. Some example search filters include:
(objectClass=*) All objects (&(objectCategory=person)(objectClass=user)(sn=Allen)) All user objects that have an sn (surname/last name) of "Allen" (!(&(objectCategory=person)(objectClass=user))) All non-user objects (&(objectCategory=computer)(objectClass=computer)(|(cn=a*)(cn=b*))) All computer objects that start with either "a" or "b"
Searching
After you have an understanding of search filters, performing LDAP searches is easy. Besides the search filter, three other values are generally in searches. They include the search base, search scope, and returned attributes. Search base is the starting point in the directory tree for the search. If you know specifically where the set of objects you want to query are located, the base should reflect the parent DN of those objectsfor example, cn=users,dc=xyz,dc=com for user objects. The search scope determines how far down the directory tree to search. There are three values that can be set for scope: base, which only searches the same level as the DN defined by the base DN setting; onelevel, which will search one level below the base and does not include the base, and subtree which searches the base entry and everything below the base. As you may guess, subtree searches are the most inefficient because they may cover large portions of the directory. Figure 3.8 illustrates the relationship between the various values for scope.
Figure 3.8 Illustration of base, onelevel, and subtree scopes.
Listing 3.4 shows an example search using Net::LDAP that prints all the user objects in AD:
Listing 3.4 Search for All User Objects
$search = $ldap->search(base=>'xyz.com', scope=>'subtree', filter=>'(&(objectclass=user)(objectcategory=Person))'); die $search->error if $search->code; print "Total entries returned: ",$search->count,"\n"; print $entry->get_value('cn'),"\n" foreach $entry $search->entries;
There are some general guidelines to follow when performing LDAP searches. Not following these guidelines can result in delayed response times and increased load on your domain controllers:
Search filters should include at least one indexed attribute. Similar to a database, when you do not search on indexed attributes in a directory, the query processor must do full scans of the directory tree to find matches. See Appendix B for a list of the default attributes that are indexed in Active Directory.
Use a combination of objectCategory and objectClass, not just objectClass, when searching a specific type of object. In many LDAP filter examples, you will see only objectClass used to define the object type to return. This is inefficient because objectClass is a multivalued, nonindexed attribute. On the other hand, objectCategory is single valued and indexed, so a combination of the two should be used when possible.
Avoid trailing match searches. Any search that tries to match only the end of a string (for example, cn=*allen) will incur a several second delay. (We've seen as much as 30 seconds!) Microsoft is aware of this problem and reportedly will have a fix in Windows .NET.
Properly scope searches so that you only search as deep in the directory tree as necessary. As previously mentioned, subtree searches are the most inefficient because they have to search everything contained under the specified search base. If you perform a subtree search on the root of a forest, for example dc=xyz,dc=com, it will search the entire directory.
Use paging (discussed later in the "Advanced Features" section of this chapter) when performing a subtree search that could potentially return a lot of objects. When paging is enabled, the server will be able to stream a large result set in chunks, reducing the server-side memory resources utilized.
Ambiguous Name Resolution
Ambiguous Name Resolution (ANR) is a Microsoft-specific extension to LDAP that allows clients to make simple and efficient searches to Active Directory with commonly used attributes without using complex search filters. You can use the anr attribute for searches and it will handle a lot of the logic needed to process a query over multiple attributes.
The default set of attributes that are searched when using anr include the following:
displayName
givenName
LegacyExchangeDN
physicalDeliveryOfficeName
proxyAddresses
name
sAMAccountName
sn
For a search filter such as (anr=rallen), the server would return objects that matched any of the previously listed attributes equal to 'rallen*'. If the search term includes a space, the server will attempt to do first/last name processing. For example, if the search filter was (anr=Rob Al), the filter expansion would look like the one in Listing 3.5.
Listing 3.5 ANR Search Filter Expansion
(|(givenName=Rob Al*) (sn=Rob Al*) (displayName=Rob Al*) (legacyExchangeDN=Rob Al*) (name=Rob Al*) (physicalDeliveryOfficeName=Rob Al*) (proxyAddresses=Rob Al*) (saMAccountName=Rob Al*) (&(givenName=Rob*)(sn=Al*)) (&(givenName=Al*)(sn=Rob*)) )
In the following example, LDIFDE is used to make an ANR query.
C:\>ldifde -f robal.ldif -r "(&(objectclass=user)(objectcategory=User)(anr=rob a l))" Connecting to "dc1.xyz.com" Logging in as current user using SSPI Exporting directory to file robal.ldif Searching for entries... Writing out entries. 1 entries exported
The command has completed successfully.
C:\>type robal.ldif dn: CN=rallen,CN=Users,DC=xyz,DC=com changetype: add accountExpires: 9223372036854775807 badPasswordTime: 126405198031875000 badPwdCount: 20 codePage: 0 cn: rallen countryCode: 0 displayName: Robbie C. Allen givenName: Robbie initials: C instanceType: 4 lastLogoff: 0 lastLogon: 0 logonCount: 0 distinguishedName: CN=rallen,CN=Users,DC=xyz,DC=com objectCategory: N=Person,CN=Schema,CN=Configuration,DC=xyz,DC=com objectClass: user objectGUID:: na8r9cjKC0KzTpl+5r4NQw== objectSid:: AQUAAAAAAAUVAAAAh0irbVS9SKG+x7O/XQQAAA== primaryGroupID: 513 pwdLastSet: 126405152801406250 name: rallen sAMAccountName: rallen sAMAccountType: 805306368 sn: Allen userAccountControl: 512 userPrincipalName: rallen@xyz.com uSNChanged: 73062 uSNCreated: 73058 whenChanged: 20010725061440.0Z whenCreated: 20010725061435.0Z
The attributes used by ANR are configurable. You can specify other attributes to be included in ANR searches by using the Active Directory Schema Snap-in to check the Ambiguous Name Resolution (ANR) box for the attribute. You can also directly set the searchFlags attribute to 5 in the attributeSchema for the attribute you want to include.
NOTE
To include an attribute to be used for ANR, the attribute must also be indexed.
See Appendix B for a program that can display all ANR assigned attributes in Active Directory.
Add, Modify, and Delete
Manipulating objects inside of Active Directory with LDAP is straightforward. Before you can manipulate an object, the DN for the object must be known. The DN is the key that distinguishes it in the directory tree. Listing 3.6 is an example of adding, modifying, and deleting a user object with Net::LDAP.
Listing 3.6 Manipulating an Object with Net::LDAP
# Add jdoe user $result = $ldap->add ( dn => 'cn=jdoe,cn=users,dc=xyz,dc=com', attr => [ cn => 'jdoe'', samaccountname => 'jdoe', userprincipalname => 'jdoe@xyz.com', mail => 'jdoe@xyz.com', telephoneNumber => '911', objectclass => ['user'], ], ); die $result->error if $result->code; # Modify attributes of jdoe $result = $ldap->modify('cn=jdoe,cn=users,dc=xyz,dc=com', changes => [ add => [ sn => 'Doe' ], # populate sn attribute delete => [ telephoneNumber => '911'], # remove '911' telephoneNumber replace => [ mail => 'John.Doe@xyz.com'], # change email address ] ); die $result->error if $result->code; # Delete jdoe $result = $ldap->delete("cn=jdoe,cn=users,dc=xyz,dc=com"); die $result->error if $result->code;
LDIF
LDAP Data Interchange Format (LDIF) is an important and versatile component of any LDAP infrastructure. LDIF allows for the contents of a directory to be exported and imported in human-readable format. The LDIF syntax is defined in RFC 2849. As its name implies, LDIF provides a good mechanism for interchanging and applying data to directories. LDIF can serve several purposes. It can act as a backup utility for a directory's data. You can export the contents of the directory to LDIF, and import it back in case of corruption or data loss. LDIF can be used for bulk loading of data into a directory. It can also act as a crude directory synchronization tool among disparate directories.
The Net::LDAP::LDIF module provides a simple interface for reading and writing LDIF files.
$ldif = Net::LDAP::LDIF->new( 'file.ldif', "r" ); @entries = $ldif->read();
Look at the Net::LDAP::LDIF perldoc for more information.