Java Support for LDAP
In this chapter, we will discuss:
The Java Naming and Directory Interface (JNDI), as the name suggests provides a standardized programming interface for accessing naming and directory services. It is a generic API and can be used to access a variety of systems including file systems, EJB, CORBA, and directory services such as Network Information Service and LDAP. JNDI’s abstractions to directory services can be viewed as similar to JDBC’s abstractions to relational databases.
The JNDI architecture consists of an Application Programming Interface or API and a Service Provider Interface or SPI. Developers program their Java applications using the JNDI API to access directory/naming services. Vendors implement the SPI with details that deal with actual communication to their particular service/product. Such implementations are referred to as service providers. Figure 2-1 shows the JNDI architecture along with a few naming and directory service providers. This pluggable architecture provides a consistent programming model and prevents the need to learn a separate API for each product.
Figure 2-1. JNDI Architecture
The JNDI has been part of the standard JDK distribution since Java version 1.3. The API itself is spread across the following four packages:
The javax.naming.spi package contains the SPI interfaces and classes. Like I mentioned above, service providers implement SPI and we will not be covering these classes in this book.
LDAP Using JNDI
While JNDI allows access to a directory service, it is important to remember that JNDI itself is not a directory or a naming service. Thus, in order to access LDAP using JNDI, we need a running LDAP directory server. If you don’t have a test LDAP server available, please refer to steps in Chapter 3 for installing a local LDAP server.
Accessing LDAP using JNDI usually involves the following three steps:
Connecting to LDAP
All the naming and directory operations using JNDI are performed relative to a context. So the first step in using JNDI is to create a context that acts as a starting point on the LDAP server. Such a context is referred to as an initial context. Once an initial context is established, it can be used to look up other contexts or add new objects.
The Context interface and InitialContext class in the javax.naming package can be used for creating an initial naming context. Since we are dealing with a directory here, we will be using a more specific DirContext interface and its implementation InitialDirContext. Both DirContext and InitialDirContext are available inside the javax.naming.directory package. The directory context instances can be configured with a set of properties that provide information about the LDAP server. The following code in Listing 2-1 creates a context to an LDAP server running locally on port 11389.
Properties environment = new Properties();
environment.setProperty(DirContext.INITIAL_CONTEXT_FACTORY,
"com.sun.jndi.ldap.LdapCtxFactory");
environment.setProperty(DirContext.PROVIDER_URL, "ldap://localhost:11389");
DirContext context = new InitialDirContext(environment);
In the above code, we have used the INITIAL_CONTEXT_FACTORY constant to specify the service provider class that needs to be used. Here we are using the sun provider com.sun.jndi.ldap.LdapCtxFactory, which is part of the standard JDK distribution. The PROVIDER_URL is used to specify the fully qualified URL of the LDAP server. The URL includes the protocol (ldap for non secure or ldaps for secure connections), the LDAP server host name and the port.
Once a connection to the LDAP server is established it is possible for the application to identify itself by providing authentication information. Contexts like the one created in Listing 2-1, where authentication information is not provided are referred to as anonymous contexts. LDAP servers usually have ACLs (access list controls) in place that restrict operations and information to certain accounts. So it is very common in enterprise applications to create and use authenticated contexts. Listing 2-2 provides an example of creating an authenticated context. Notice that we have used three additional properties to provide the binding credentials. The SECURITY_AUTHENTICATION property is set to simple indicating that we will be using plain text user name and password for authentication.
Properties environment = new Properties();
environment.setProperty(DirContext.INITIAL_CONTEXT_FACTORY,
"com.sun.jndi.ldap.LdapCtxFactory");
environment.setProperty(DirContext.PROVIDER_URL, "ldap://localhost:11389");
environment.setProperty(DirContext.SECURITY_AUTHENTICATION, "simple");
environment.setProperty(DirContext.SECURITY_PRINCIPAL, "uid=admin,ou=system");
environment.setProperty(DirContext.SECURITY_CREDENTIALS, "secret");
DirContext context = new InitialDirContext(environment);
Any problems that might occur during the creation of the context will be reported as instances of javax.naming.NamingException. NamingException is the super class of all the exceptions thrown by the JNDI API. This is a checked exception and must be handled properly for the code to compile. Table 2-1 provides a list of common exceptions that we are likely to encounter during JNDI development.
Table 2-1. Common LDAP Exceptions
Exception | Description |
---|---|
AttributeInUseException | Thrown when an operation tries to add an existing attribute. |
AttributeModification Exception | Thrown when an operation tries to add/remove/update an attribute and violates the attribute’s schema or state. For example, adding two values to a single valued attribute would result in this exception. |
CommunicationException | Thrown when an application fails to communicate (network problems for example) with the LDAP server. |
InvalidAttributesException | Thrown when an operation tries to add or modify an attribute set that has been specified incompletely or incorrectly. For example, attempting to add a new entry without specifying all the required attributes would result in this exception. |
LimitExceededException | Thrown when a search operation abruptly terminates as a user or system specified result limit is reached. |
InvalidSearchFilterException | Thrown when a search operation is given a malformed search filter. |
NameAlreadyBoundException | Thrown to indicate that an entry cannot be added as the associated name is already bound to a different object. |
PartialResultException | Thrown to indicate that only a portion of the expected results is returned and the operation cannot be completed. |
Once we obtain an initial context, we can perform a variety of operations on LDAP using the context. These operations can involve looking up another context, creating a new context and updating or removing an existing context. Here is an example of looking up another context with DN uid=emp1,ou=employees,dc=inflinx,d c=com.
DirContext anotherContext = context.lookup("uid=emp1,ou=employees,
dc=inflinx,dc=com");
We will take a closer look at each of these operations in the coming section.
After all the desired LDAP operations are complete, it is important to properly close the context and any other associated resources. Closing a JNDI resource simply involves calling the close method on it. Listing 2-3 shows the code associated with closing a DirContext. From the code you can see that the close method also throws a NamingException that needs to be properly handled.
try {
context.close();
}
catch (NamingException e) {
e.printstacktrace();
}
Consider the case where a new employee starts with our hypothetical Library and we are asked to add his information to LDAP. As we have seen earlier, before an entry can be added to LDAP, it is necessary to obtain an InitialDirContext. Listing 2-4 defines a reusable method for doing this.
private DirContext getContext() throws NamingException{
Properties environment = new Properties();
environment.setProperty(DirContext.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.
LdapCtxFactory");
environment.setProperty(DirContext.PROVIDER_URL, "ldap://localhost:10389");
environment.setProperty(DirContext.SECURITY_PRINCIPAL, "uid=admin,ou=system");
environment.setProperty(DirContext.SECURITY_CREDENTIALS, "secret");
DirContext context = new InitialDirContext(environment);
return context;
}
Once we have the initial context, adding the new employee information is a straightforward operation as shown in Listing 2-5.
public void addEmploye(Employee employee) {
DirContext context = null;
try {
context = getContext();
// Populate the attributes
Attributes attributes = new BasicAttributes();
attributes.put(new BasicAttribute("objectClass", "inetOrgPerson"));
attributes.put(new BasicAttribute("uid", employee.getUid()));
attributes.put(new BasicAttribute("givenName", employee.getFirstName()));
attributes.put(new BasicAttribute("surname", employee.getLastName()));
attributes.put(new BasicAttribute("commonName", employee.getCommonName()));
attributes.put(new BasicAttribute("departmentNumber",
employee.getDepartmentNumber()));
attributes.put(new BasicAttribute("mail", employee.getEmail()));
attributes.put(new BasicAttribute("employeeNumber",
employee.getEmployeeNumber()));
Attribute phoneAttribute = new BasicAttribute("telephoneNumber");
for(String phone : employee.getPhone()) {
phoneAttribute.add(phone);
}
attributes.put(phoneAttribute);
// Get the fully qualified DN
String dn = "uid="+employee.getUid() + "," + BASE_PATH;
// Add the entry
context.createSubcontext("dn", attributes);
}
catch(NamingException e) {
// Handle the exception properly
e.printStackTrace();
}
finally {
closeContext(context);
}
}
As you can see, the first step in the process is to create a set of attributes that needs be added to the entry. JNDI provides the javax.naming.directory.Attributes interface and its implementation javax.naming.directory.BasicAttributes to abstract an attribute collection. We then add the employee’s attributes one at a time to the collection using JNDI’s javax.naming.directory.BasicAttribute class. Notice that we have taken two approaches in creating the BasicAttribute class. In the first approach we have added the single valued attributes by passing the attribute name and value to BasicAttribute’s constructor. To handle the multi-valued attribute telephone, we first created the BasicAttribute instance by just passing in the name. Then we individually added the telephone values to the attribute. Once all the attributes are added, we invoked the createSubcontext method on the initial context to add the entry. The createSubcontext method requires the fully qualified DN of the entry to be added.
Notice that we have delegated the closing of the context to a separate method closeContext. Listing 2-6 shows its implementation.
private void closeContext(DirContext context) {
try {
if(null != context) {
context.close();
}
}
catch(NamingException e) {
// Ignore the exception
}
}
Modifying an existing LDAP entry can involve any of the following operations:
In order to allow modification of the entries, JNDI provides an aptly named javax.naming.directory.ModificationItem class.
A ModificationItem consists of the type of modification to be made and the attribute under modification. The code below creates a modification item for adding a new telephone number.
Attribute telephoneAttribute = new BasicAttribute("telephone", "80181001000");
ModificationItem modificationItem = new ModificationItem(DirContext.
ADD_ATTRIBUTE, telephoneAttribute);
Notice that in the above code, we have used the constant ADD_ATTRIBUTE to indicate that we want an add operation. Table 2-2 provides the supported modification types along with their descriptions.
Table 2-2. LDAP Modification Types
Modification Type | Description |
---|---|
ADD_ATTRIBUTE | Adds the attribute with the supplied value or values to the entry. If the attribute does not exist then it will be created. If the attribute already exists and the attribute is a multi-valued then this operation simply adds the specified value(s) to the existing list. However, this operation on an existing single valued attributes will result in the AttributeInUseException. |
REPLACE_ATTRIBUTE | Replaces existing attribute values of an entry with the supplied values. If the attribute does not exist then it will be created. If the attribute already exists, then all of its values will be replaced. |
REMOVE_ATTRIBUTE | Removes the specified value from the existing attribute. If no value is specified then the attribute in its entirety will be removed. If the specified value does not exist in the attribute, the operation will throw a NamingException. If the value to be removed is the only value of the attribute, then the attribute is also removed. |
The code for updating an entry is provided in Listing 2-7. The modifyAttributes method takes the fully qualified DN of the entry to be modified and an array of modification items.
public void update(String dn, ModificationItem[] items) {
DirContext context = null;
try {
context = getContext();
context.modifyAttributes(dn, items);
}
catch (NamingException e) {
e.printStackTrace();
}
finally {
closeContext(context);
}
}
Removing an entry using JNDI is again a straightforward process and is shown in Listing 2-8. The destroySubcontext method takes the fully qualified DN of the entry that needs to be deleted.
public void remove(String dn) {
DirContext context = null;
try {
context = getContext();
context.destroySubcontext(dn);
}
catch(NamingException e) {
e.printStackTrace();
finally {
closeContext(context);
}
}
Many LDAP servers don’t allow an entry to be deleted if it has child entries. In those servers, deleting a non-leaf entry would require traversing the sub tree and deleting all the child entries. Then the non-leaf entry can be deleted. Listing 2-9 shows the code involved in deleting a sub tree.
public void removeSubTree(DirContext ctx, String root)
throws NamingException {
NamingEnumeration enumeration = null;
try {
enumeration = ctx.listBindings(root);
while (enumeration.hasMore()) {
Binding childEntry =(Binding)enumeration.next();
LdapName childName = new LdapName(root);
childName.add(childEntry.getName());
try {
ctx.destroySubcontext(childName);
}
catch (ContextNotEmptyException e) {
removeSubTree(ctx, childName.toString());
ctx.destroySubcontext(childName);
}
}
}
catch (NamingException e) {
e.printStackTrace();
}
finally {
try {
enumeration.close();
}
catch (Exception e) {
e.printStackTrace();
}
}
}
Note The OpenDJ LDAP server supports a special sub tree delete control that when attached to a delete request can cause the server to delete the non-leaf entry and all its child entries. We will look at the using LDAP controls in Chapter 7.
Searching for information is usually the most common operation performed against an LDAP server. In order to perform a search, we need to provide information such as the scope of the search, what we are looking for, and what attributes need to be returned. In JNDI, this search metadata is provided using the SearchControls class. Listing 2-10 provides an example of a search control with subtree scope and returns the givenName and telephoneNumber attributes. The subtree scope indicates that the search should start from the given base entry and should search all its subtree entries. We will look at different scopes available in detail in Chapter 6.
SearchControls searchControls = new SearchControls(); searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);
searchControls.setReturningAttributes(new String[]{"givenName",
"telephoneNumber"});
Once we have the search controls defined, the next step is to invoke one of the many search methods in the DirContext instance. Listing 2-11 provides the code that searches all the employees and prints their first name and telephone number.
public void search() {
DirContext context = null;
NamingEnumeration<SearchResult> searchResults = null;
try
{
context = getContext();
// Setup Search meta data
SearchControls searchControls = new SearchControls();
searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);
searchControls.setReturningAttributes(new String[]
{"givenName", "telephoneNumber"});
searchResults = context.search("dc=inflinx,dc=com",
"(objectClass=inetOrgPerson)", searchControls);
while (searchResults.hasMore()) {
SearchResult result = searchResults.next();
Attributes attributes = result.getAttributes();
String firstName = (String)attributes.get("givenName").get();
// Read the multi-valued attribute
Attribute phoneAttribute = attributes. get("telephoneNumber");
String[] phone = new String[phoneAttribute.size()];
NamingEnumeration phoneValues = phoneAttribute.getAll();
for(int i = 0; phoneValues.hasMore(); i++) {
phone[i] = (String)phoneValues.next();
}
System.out.println(firstName + "> " + Arrays.toString(phone));
}
}
catch(NamingException e) {
e.printStackTrace();
}
finally {
try {
if (null != searchResults) {
searchResults.close();
}
closeContext(context);
} catch (NamingException e) {
// Ignore this
}
}
}
Here we used the search method with three parameters: a base that determines the starting point of the search, a filter that narrows down the results, and a search control. The search method returns an enumeration of SearchResults. Each search result holds the LDAP entry’s attributes. Hence we loop through the search results and read the attribute values. Notice that for multi valued attributes we obtain another enumeration instance and read its values one at a time. In the final part of the code, we close the result enumeration and the context resources.
Though JNDI provides a nice abstraction for accessing directory services, it does suffer from several of the following drawbacks: