Java Solutions


Directory-Enabled Application Development Using LDAP and JNDI

Michael Seaver

Admit it. You've always wondered what directory services were all about. Here's a succint and effective introduction to accessing DS via JNDI.


Directory services is an area of growing interest for application developers and software vendors. Interoperability with directory services is becoming a critical component for all types of enterprise software applications. After reading this article, you should have a good idea of what a directory is and how you can take advantage of directory services to enhance your Java applications.

What Is a Directory?

A directory is simply a database. It is not a relational database, however. Instead of organizing data into tables and columns as in the relational model, the data in a directory is organized into a tree of objects. Each of these objects consists of a set of name-value pairs, not unlike a map in the world of Java collections. Each object may contain other objects that in turn may contain other objects as well. The name-value pairs that make up an object are known as attributes, specifically attribute names and attribute values.

Directories generally have very fast look-up times (and very slow update times) when compared to relational databases, so they are most appropriate in systems that access the data heavily, but modify data only occasionally. Also, many directory implementations don’t have the data-integrity and transactioning capabilities found in most relational databases. You can’t do ad-hoc queries in a directory — the architecture is just not set up to do it. Consequently, the choice between a directory and a relational database is not always obvious.

Directories apply to a variety of applications, and it seems that new and creative ways to leverage a directory emerge regularly. One of the most common uses of a directory is a network and application management tool, the best-known examples being Microsoft’s Active Directory and Novell’s NDS eDirectory. In these systems, objects in the tree represent users, workstations, servers, and applications constituting a centralized repository for network and application configuration information and a central location where these resources can be managed. Through the directory, users may have rights assigned to workstations, applications, and other resources available on the network. Because the directory is a hierarchy, many administrative functions can be performed on containers of many objects instead of individually on each object. The benefits of group-based management are well known. The directory takes this to the next level by allowing many levels of group-based administration.

Another very common use of directories is for user authentication and profile management. Many web applications require a very high-level of customization. Often the data used for customizing the user’s experience resides in a relational database such as Oracle. While relational databases are powerful tools ideal for storing large amounts of complicated data, user profiles are generally fairly simple data sets. Increasingly, developers are turning to directories for a simpler programming model and improved performance. (Note that Oracle does have a product that lets you view the relational database as if it were a directory, should you already have an Oracle-based system) Also, the distributed nature of many directories is ideal for Internet applications that require extreme scalability.

Directory services are also used to provide “white-pages” applications, which are directories that make individuals’ contact information available to an organization or to the public. Email clients such as Microsoft Outlook or Netscape Messenger can use these directories to look up email addresses. Increasingly corporations are turning to directories to provide their corporate white-pages information. Some of these applications go beyond just email addresses and phone numbers, allowing users to browse organizational charts and location maps, public encryption keys, and so forth

While there are many uses for directory services, each of these applications uses the directory in a similar manner. Making an application “directory-enabled” is a straightforward process that allows you to integrate with any directory. Because many companies are rolling out directories for these and other applications, there is an excellent opportunity to leverage existing deployments, enhancing the appeal and value of your software offerings.

The Structure of a Directory

A directory consists of objects organized in a tree-like structure. Consequently, a directory is often referred to as a tree. These objects may or may not represent items that exist in the physical world. Examples of objects that represent physical items include users, workstations, servers, routers, etc. Sometimes directory objects represent concepts such as applications, services, or organizations. Other times they exist solely to give structure to the tree by containing other objects.

Each object consists of a set of name-value pairs referred to as attributes. When an object contains a value for a given attribute, that attribute is said to exist on that object. Some attributes may have multiple values. For example, a user may have multiple telephone numbers or given names. Other attributes are restricted to containing only one value at a time.

The schema of a directory defines the attributes and classes that can exist. Each object in the directory belongs to one or more object classes. The schema defines each of these object classes. The definition details what attributes an object of that class can have. For example the schema definition for the object class User may state that a user object can have Surname, givenName, and Telephone attributes among others. It also details what classes of objects can contain User objects, which attributes are required, and which attributes are optional.

Another very important component of the schema definition for an object class are naming attributes. Specific attributes can be flagged as the naming attributes for objects of a given class. These attributes can then be used to reference the object in the directory. In the case of User objects, one naming attribute is CN (Common Name). These abbreviated attribute names are common in LDAP (Lightweight Directory Access Protocol)-based directories (see the section “Accessing the Directory”). Some other common naming attributes are O (Organizations) and OU (Organizational Units). The DN (Distinguished Name) of an object is a combination of the name of the object and the name of all its parent objects. This forms the absolute address of an object in the tree, similar to the way a path name identifies a file.

Figure 1 contains a drawing of a particular directory organization. At the root of this piece of the tree is an object of class Organization. The naming attribute for Organizations is o and the value of that attribute is SLC. So the DN of this object would be o=SLC. This object will represent the Salt Lake City campus of an imaginary corporation. Inside of o=SLC, there are three objects of class Organization Unit. The naming attribute for Organizational Units is ou. The value of this attribute is MRK for the first, ENG for the second, and HR for the third. These objects represent the marketing, engineering, and human resources teams located on the Salt Lake City campus of our corporation. The distinguished name of our marketing unit would be ou=MRK,o=SLC. You can see that the DNs for the engineering and human resources units would be ou=ENG,o=SLC and ou=HR,o=SLC respectively. Inside of ou=HR,o=SLC, there is a User object that might represent me if I were an employee of this corporation working in the HR department. The naming attribute for users is cn. The value of that attribute is mseaver. So the DN of my User object would be cn=mseaver,ou=HR,o=SLC. You can also see that my User object has two other attributes SN (Surname) and givenName. SN happens to be a required attribute for User objects. The value of this attribute is Seaver. givenName is an optional attribute that may contain multiple values. In my case, it contains two values: Michael and James.

Accessing the Directory

There are many ways to access the directory when creating a directory-enabled application. Most directory vendors provide APIs for accessing the directory via their proprietary wire protocols. Developing to one of these API sets could leave you locked in to one vendor’s implementation of a directory, however. The original open standard for directory services was titled X.500 and used a wire protocol known as the DAP (Directory Access Protocol). While the X.500 specification forms the basis for most of the modern directory services, the DAP protocol was never designed to work over today’s TCP/IP based networks. Fortunately there is a standard wire protocol for directory access on regular TCP/IP networks: LDAP. Now in its third revision, LDAP is an IETF-proposed standard (see RFCs 2252-6 and 2829-30) and is supported by most major directory vendors including iPlanet, Microsoft, and Novell, so directory-enabling your application using LDAP gives users a number of choices of directory services providers. Because there are LDAP-compliant directories available for virtually every major operating system, and LDAP-based APIs for most programming languages, creating LDAP directory-enabled applications is a straightforward task.

There are several different APIs available for developing to LDAP directories. One popular API is available in the J2SE. If you’ve got a recent JDK installed (1.3 or later), you already have all the libraries required to build LDAP applications in Java. The API is the JNDI (Java Naming and Directory Interface). It is somewhat of an abstract API meant for accessing all types of information organized into hierarchies. As JDBC uses drivers to provide access to the various databases it supports, so JNDI uses providers to access different directories and namespaces. I’ll be using the JNDI provider for LDAP to connect to Novell’s NDS eDirectory. All of the code in this article will work on any LDAP store with little or no modification, so if you have access to Active Directory or iPlanet Directory you can still try the samples. Novell makes NDS eDirectory available free to developers (see the “Resources” section.)

On my machine, I’ve installed Novell’s NDS eDirectory and JDK 1.3. Using the administration tools that come with eDirectory, I’ve set up a tree to look exactly like the one in Figure 1, and the sample code assumes this tree structure. One of the most common mistakes made when developing directory-enabled applications is forgetting to give users the necessary rights. Like any data store, directories have complicated rights structures that allow administrators to restrict access to data. In many cases, trying to access data without the correct rights won’t actually cause an error to be returned, but instead it will appear as if the data isn’t there at all. If you notice strange behavior as you’re developing against a directory, a good first step is to double-check your rights configurations. In each of the sample applications below, I’m assuming that you’ve configured the directory such that the users have rights to perform the actions you are attempting to perform. How these rights are configured is very much vendor specific, so you’ll need to check the documentation for the directory you’re using.

Configuring rights in NDS eDirectory is a very simple process. Currently the primary management tool for eDirectory is Console One, which is installed with eDirectory. Once I have opened Console One and logged in to the tree using the login button on the toolbar, I need to select the object that I want to have rights to. Rights in eDirectory are handled through a mechanism called trustees. A trustee of a given object has rights to work with the given object. For example, if your user object were a trustee of my user object, when logged in as your user object, you would have rights to perform actions on my user object. In the examples below, I’ll need to make sure that cn=mseaver,ou=HR,o=SLC has rights to create objects in ou=ENG,o=SLC. Using Console One, I’ll browse to the object ou=ENG,o=SLC. Once I’ve found the object, I can right-click my mouse on the object and select “Trustees of this Object.” This will open a window listing the objects that have rights to ou=ENG,o=SLC. From here, I can click on the “Add Trustee” button and add cn=mseaver,ou=HR,o=SLC to the list. Once that is done, I just click on the “Assigned Rights” button and from there I can give myself rights to read and write attributes, as well as create other objects inside of ou=ENG,o=SLC.

Logging in to the Directory

Like any good data store, before you can access any information, you must “authenticate” yourself (prove who you are). With directories, you need to login as one of the user objects in the tree. In the example program in Listing 1, I will login as cn=mseaver,ou=HR,o=SLC. The first step in logging in is to set up the initial context. A context is simply a location in the tree. With LDAP, the initial context points to the root of the tree, which is an imaginary parent to o=SLC. To setup an initial context in JNDI, you pass the JNDI subsystem a Hashtable that contains information on what type of environment you want. First and foremost, you must specify which JNDI provider you’ll be using. In this case I’ll be using com.sun.jndi.ldap.LdapCtxFactory, the provider that comes with the JDK. This is the factory class used by the JNDI subsystem to create our initial context. In addition, I need to provide the URL of the LDAP server. Because I have an LDAP directory installed on my development machine, I’m using 127.0.0.1 ("localhost") as the URL and the standard LDAP port of 389. I also need to provide the DN and password of the object I’m going to log in as, namely cn=mseaver,ou=HR,o=SLC. When I set up this tree on my machine, I gave the user object cn=mseaver,ou=HR,o=SLC the password
password. You’ll need to change the code to match the password you gave the user when you set up your tree. The final item in the Hashtable describes the type of authentication. Most LDAP directories support at least two types of authentication: simple and SSL. Simple authentication performs no encryption of the password on the wire. Because this poses a security risk, some directories have simple authentication disabled by default. You’ll need to enable simple authentication before you run this code. Once I’ve initialized the Hashtable with the correct environment information, I pass it to the constructor for InitialContext. If something goes wrong, say an incorrect password or an invalid object DN, it throws a NamingException. If no exception occurs, then root will point at the root of our tree, and I have a valid LDAP connection. When I’m finished with the connection, I call close on the InitialContext object to log out.

Reading Directory Entries

Next to logging in, the next most common task in directory programming is reading attributes from the tree. In Listing 2, I login to the tree and read the SN attribute from the object cn=mseaver,ou=HR,o=SLC. Remember that for this to work, the user you login as must have rights to access the SN attribute on cn=mseaver,ou=HR,o=novell. I’ve pulled the contents of Login.java into a function called login that will appear in all of the remaining examples. This function sets up the environment, creates the initial context, and returns that initial context to the caller. As before, I login as cn=mseaver,ou=HR,o=novell. Once the login succeeds, I lookup the location in the tree from which I want to read. Recall that the initial context points to the root of the tree, but I want to read from cn=mseaver,ou=HR,o=novell. The Context.lookup method takes a String representing the RDN (Relative Distinguished Name) of an object in the tree. An RDN is like a DN, but it is only guaranteed to be unique in a given context. For example, from the root of the tree, the RDN of cn=mseaver,ou=HR,o=SLC is the full DN. However, from the context ou=HR,o=SLC, the RDN would be cn=mseaver. Because root points to the root of the tree, the RDN of the object we want to read is cn=mseaver,ou=HR,o=SLC. The lookup method returns another DirContext object pointing to this spot in the tree. Once I have a DirContext object pointing to the correct spot in the tree, all I need to do is call the DirContext.getAttributes method. This method takes two parameters: the RDN of the object we want to read from, and a String array containing the attribute IDs for the attributes we want to read, in this case sn. Since the DirContext object already points to the correct object, I pass an empty string for the RDN. Alternatively, I could have looked up the container where our object is instead of the object itself like this:

DirContext ctx   =
    (DirContext)root.lookup("ou=HR,o=SLC");
String[] sn      = {"sn"};
Attributes attrs =
    ctx.getAttributes("cn=mseaver", sn);

In this case, I need to pass the RDN of the object to be read into the DirContext.getAttributes method. If you’re wondering if you can just dispense with the look-up call altogether and just call getAttributes on the root object, you can, and the code would look like this:

String[] sn = {"sn"};
Attributes attrs =
    root.getAttributes("cn=mseaver,ou=HR,o=SLC", sn);

Most often you’ll just use this last method, especially if you’re only going to read attributes from a single object. It’s important to understand how look-ups work and how they relate to RDNs though, especially when writing complicated directory manipulation code.

The DirContext.getAttributes method returns an Attributes object. I get access to the actual attribute object by calling the Attributes.get method and passing it a string containing the ID of the attribute I want. If the return value is null, then either there is no value for the attribute on the object (in which case the attribute is said to not exist on the object), or, the attribute exists, but I don’t have rights to see it. Once I have a valid handle to the attribute object, I call Attribute.get for a String object containing the actual attribute value. Attribute.get returns an Object, not a String, so I need to cast it before I can use it. JNDI is a very flexible API, and it will allow a directory to return attribute values in any kind of object. When using the LDAP provider however, attribute values are returned as strings.

When reading multiple attributes or multiple values, things become slightly more complicated. You can find an example of reading multiple values and multiple attributes in Listing 3.

Updating the Directory

Although most often you just read from the directory, sometimes you need to update attribute values. As always, you need to make sure you are logging in as a user who has rights to modify these attributes. You can find an example of a simple update in Listing 4. The first two lines, where I log into the directory and look up the object I’m interested in, should look familiar by now. The next line allocates an array of three ModificationItem objects, one modification object for each operation I’ll need to perform. In this example, I’m changing the givenName value from James to Jimmy. Because givenName is a multi-valued attribute, I need to take care not to change any of the other values in the attribute, so I first remove the value James and then add the value Jimmy. I also want to update the fullName attribute, but in this case I’ll just wipe out any existing values and replace them with my new full name Michael Jimmy Seaver.

When performing directory modifications, there are three types of operations available. You can remove a specific attribute value; add an attribute value to an object, which does not alter any other existing attributes on the object; and you can replace an attribute value, which removes all the current values for the attribute and replaces them with the new value. The constructor for a ModificationItem takes two parameters. The first is an integer specifying which of the three operations you are requesting. These operation codes are static final members of the DirContext object. The second parameter is an Attribute object specifying the attribute ID and attribute values. You can use the BasicAttribute class for these operations when you’re using the LDAP provider. If you were using a directory provider from another vendor, you might use one of their classes instead of BasicAttribute, but the general structure would be the same. Once you’ve set up the Attributes and ModificationItems, all that is left to do is call the DirContext.modifyAttributes method. This method takes two parameters, first the RDN of the object to be modified (in this case an empty String because I’ve have already done a look-up). The second parameter is an array of ModificationItems specifying the changes to make.

Adding and Deleting Objects

Sometimes you’ll need to create new objects in the directory or delete objects you no longer need. An example of a simple add operation appears in Listing 5. Using the Attributes object, I provide values for the required attributes, at a minimum the objectClass. You’ll need to consult documentation on your directory’s schema to figure out which attributes are required and which are optional. In the case of User, I need to populate the SN attribute in addition to the objectClass attribute. You can add attributes to the Attributes object through the put method that takes two parameters, an attribute ID and an attribute value. This method only sets one value per attribute, so if you want to set multiple values on an attribute, you’ll need to use an Attribute object. Once you’ve initialized the Attributes object with the correct attribute IDs and values, you need to call DirContext.createSubcontext with the RDN of the object to create, and the Attributes object we just set up. If this call succeeds, the object has been successfully created in the tree.

Deleting objects is very straightforward and is accomplished through the DirContext.destroySubcontext call (see Listing 6).

Conclusion

The directory can also provide a central environment for managing the security and access control portions of your applications. However, using the tools illustrated in this article, you should be able to begin using an LDAP directory for any authentication or user-profile management application. If you have an application that is currently using its own user profile or authentication database, consider LDAP-enabling your application. Organizations throughout the world are moving to LDAP directories to provide a single point of authentication and administration for all their applications. Providing an LDAP-enabled version of your application will make it easier for these organizations to integrate your applications into their enterprises. In addition, it can accelerate your development by simplifying your authentication and user-profile management code. The market for LDAP-enabled applications and LDAP-proficient developers is just now beginning to explode. Using LDAP could just be the competitive advantage you need.

Resources

<www.ldapzone.com > — All kinds of goodies for LDAP developers.

<www.javasoft.com/products/jndi/index.html> — Information on JNDI, including some other uses of the API.

<http://msdn.microsoft.com> — Information on developing using Microsoft’s Active Directory.

<http://developer.novell.com> — Information on developing using Novell’s NDS eDirectory.

<www.novell.com/download/index.html> — Download Novell’s directory, which is free to developers and available for NT/2000, Linux, Solaris, and NetWare.

Michael Seaver is the manager of Novell Consulting Custom Development Group. His team provides premium software development services for Novell customers throughout the world. He has developed directory-enabled applications for many of these customers. In his spare time, he enjoys playing with his baby and driving in the mountain canyons of his hometown, Provo, Utah.