Chapter 2. Best Practices For LDAP Application Developers

Table of Contents
2.1. Authenticate Correctly
2.2. Reuse Connections
2.3. Health Check Connections
2.4. Request Exactly What You Need All At Once
2.5. Use Specific LDAP Filters
2.6. Make Modifications Specific
2.7. Trust Result Codes
2.8. Check Group Membership on the Account, Not the Group
2.9. Ask the Directory Server What It Supports
2.10. Store Large Attribute Values By Reference
2.11. Take Care With Persistent Search & Server-Side Sorting
2.12. Reuse Schemas Where Possible
2.13. Read Directory Server Schemas During Initialization
2.14. Handle Referrals
2.15. Troubleshooting: Check Result Codes
2.16. Troubleshooting: Check Server Log Files
2.17. Troubleshooting: Inspect Network Traffic

Follow the advice in this chapter to write effective, maintainable, high performance directory client applications.

2.1. Authenticate Correctly

Unless your application performs only read operations, you should authenticate to the directory server. Some directory administrators require authentication even to read directory data.

Once you authenticate (bind), directory servers like OpenDJ make authorization decisions based on your identity. With servers like OpenDJ that support proxied authorization, once authenticated your application can also request an operation on behalf of another identity, for example the identity of the end user.

Your application therefore should have an account used to authenticate such as cn=My Killer App,ou=Apps,dc=example,dc=com. The directory administrator can then authorize appropriate access for your application, and also monitor your application's requests to help you troubleshoot problems if they arise.

Your application can use simple, password-based authentication. When you opt for password-based authentication, also use Start TLS for example to avoid sending the password as clear text over the network. If you prefer to manage certificates rather than passwords, directory servers like OpenDJ can do client authentication as well.

2.2. Reuse Connections

LDAP is a stateful protocol. You authenticate (bind), you do stuff, you unbind. The server maintains a context that lets it make authorization decisions concerning your requests. You should therefore reuse connections when possible.

You can make multiple requests without having to set up a new connection and authenticate for every request. You can issue a request and get results asynchronously, while you issue another request. You can even share connections in a pool, avoiding the overhead of setting up and tearing down connections if you use them often.

2.3. Health Check Connections

In a network built for HTTP applications, your long-lived LDAP connections can get cut by network equipment configured to treat idle and even just old connections as stale resources to reclaim.

When you maintain a particularly long-lived connection such as a connection for a persistent search, periodically perform a health check to make sure nothing on the network quietly decided to drop your connection without notification. A health check might involve reading an attribute on a well-known entry in the directory.

OpenDJ LDAP SDK offers Connections.newHeartBeatConnectionFactory() methods to ensure your ConnectionFactory serves connections that are periodically checked to detect whether they are still alive.

When you do not use a heart beat connection factory, or when you wrap an authenticated connection factory with a heart beat connection factory, it is possible for the first bind request to hang indefinitely. Bind operations do not have control over the client side timeout when you use the synchronous API. In such cases you can use LDAPOptions.setTimeout() when creating the LDAPConnectionFactory to configure a request timeout for all operations. Take care not to set the timeout so low that long operations such as unindexed searches fail to complete before the timeout.

2.4. Request Exactly What You Need All At Once

By the time your application makes it to production, you should know what attributes you want, so request them explicitly and request all the attributes you need in the same search. For example, if all you need is mail and cn, then specify both attributes in your SearchRequest.

2.5. Use Specific LDAP Filters

The difference between a general filter (mail=*@example.com) and a good, specific filter like (mail=user@example.com) can be huge numbers of entries and enormous amounts of processing time, both for the directory server that has to return search results, and also for your application that has to sort through the results. Many use cases can be handled with short, specific filters. As a rule, prefer equality filters over substring filters.

Some directory servers like OpenDJ reject unindexed searches by default, because unindexed searches are generally far more resource intensive. If your application needs to use a filter that results in an unindexed search, then work with your directory administrator to find a solution, such as having the directory maintain the indexes required by your application.

Furthermore, always use & with ! to restrict the potential result set before returning all entries that do not match part of the filter. For example, (&(location=Oslo)(!(mail=birthday.girl@example.com))).

2.6. Make Modifications Specific

When you modify attributes with multiple values, for example when you modify a list of group members, replace or delete specific values individually, rather than replacing the entire list of values. Making modifications specific helps directory servers replicate your changes more effectively.

2.7. Trust Result Codes

Trust the LDAP result code that your application gets from the directory server. For example, if you request a modify application and you get ResultCode.SUCCESS, then consider the operation a success rather than issuing a search immediately to get the modified entry.

The LDAP replication model is loosely convergent. In other words, the directory server can, and probably does, send you ResultCode.SUCCESS before replicating your change to every directory server instance across the network. If you issue a read immediately after a write, and a load balancer sends your request to another directory server instance, you could get a result that differs from what you expect.

The loosely convergent model also means that the entry could have changed since you read it. If needed, you can use LDAP assertions to set conditions for your LDAP operations.

2.8. Check Group Membership on the Account, Not the Group

If you need to determine which groups an account belongs to, request isMemberOf for example with OpenDJ when you read the account entry. Other directory servers use other names for this attribute that identifies the groups to which an account belongs.

2.9. Ask the Directory Server What It Supports

Directory servers expose their capabilities, suffixes they support, and so forth as attribute values on the root DSE. See the section on Reading Root DSEs.

This allows your application to discover a variety of information at run time, rather than storing configuration separately. Thus putting effort into querying the directory about its configuration and the features it supports can make your application easier to deploy and to maintain.

For example, rather than hard-coding dc=example,dc=com as a suffix DN in your configuration, you can search the root DSE on OpenDJ for namingContexts, and then search under the naming context DNs to locate the entries you are looking for in order to initialize your configuration.

Directory servers also expose their schema over LDAP. The root DSE attribute subschemaSubentry shows the DN of the entry holding LDAP schema definitions. See the section, Getting Schema Information. Note that LDAP object class and attribute type names are case-insensitive, so isMemberOf and ismemberof refer to the same attribute for example.

2.10. Store Large Attribute Values By Reference

When you use large attribute values such as photos or audio messages, consider storing the objects themselves elsewhere and keeping only a reference to external content on directory entries. In order to serve results quickly with high availability, directory servers both cache content and also replicate it everywhere.

Textual entries with a bunch of attributes and perhaps a certificate are often no larger than a few KB. Your directory administrator might therefore be disappointed to learn that your popular application stores users' photo and .mp3 collections as attributes of their accounts.

2.11. Take Care With Persistent Search & Server-Side Sorting

A persistent search lets your application receive updates from the server as they happen by keeping the connection open and forcing the server to check whether to return additional results any time it performs a modification in the scope of your search. Directory administrators therefore might hesitate to grant persistent search access to your application. Directory servers like OpenDJ can let you discover updates with less overhead by searching the change log periodically. If you do have to use a persistent search instead, try to narrow the scope of your search.

Directory servers also support a resource-intensive operation called server-side sorting. When your application requests a server-side sort, the directory server retrieves all the entries matching your search, and then returns the whole set of entries in sorted order. For result sets of any size server-side sorting therefore ties up server resources that could be used elsewhere. Alternatives include both sorting the results after your application receives them, and also working with the directory administrator to have appropriate browsing (virtual list view) indexes maintained on the directory server for applications that must regularly page through long lists of search results.

2.12. Reuse Schemas Where Possible

Directory servers like OpenDJ come with schema definitions for a wide range of standard object classes and attribute types. This is because directories are designed to be shared by many applications. Directories use unique, typically IANA-registered object identifiers (OID) to avoid object class and attribute type name clashes. The overall goal is Internet-wide interoperability.

You therefore should reuse schema definitions that already exist whenever you reasonably can. Reuse them as is. Do not try to redefine existing schema definitions.

If you must add schema definitions for your application, extend existing object classes with AUXILIARY classes of your own. Take care to name your definitions such that they do not clash with other names.

When you have defined schema required for your application, work with the directory administrator to have your definitions added to the directory service. Directory servers like OpenDJ let directory administrators update schema definitions over LDAP, so there is not generally a need to interrupt the service to add your application. Directory administrators can however have other reasons why they hesitate to add your schema definitions. Coming to the discussion prepared with good schema definitions, explanations of why they should be added, and evident regard for interoperability makes it easier for the directory administrator to grant your request.

2.13. Read Directory Server Schemas During Initialization

By default OpenDJ LDAP SDK uses a very minimal, built-in core schema. The SDK does not read the schema from the directory server by default when setting up a connection, because doing so would have a significant performance cost, and generally needs to be done only once.

When you start your application, read directory server schemas as a one-off initialization step, using a method such as Schema.readSchema(). For sample code see the section, Getting Schema Information.

Once you have the directory server schema definitions, you can validate entries with Schema.validateEntry(). You can also set the schema to use for decoding LDAP search results with DecodeOptions.setSchema() or DecodeOptions.setSchemaResolver(), and specify the DecodeOptions as part of LDAPOptions when creating an LDAPConnectionFactory.

2.14. Handle Referrals

When a directory server returns a search result, the result is not necessarily an entry. If the result is a referral, then your application should follow up with an additional search based on the URIs provided in the result.

2.15. Troubleshooting: Check Result Codes

LDAP result codes are standard and clearly defined. When you receive a Result, check the ResultCode value to determine what action your application should take. When the result is not what you expect, you can also read or at least log the message string from ResultCode.getDiagnosticMessage().

2.16. Troubleshooting: Check Server Log Files

If you can read the directory server access log, then you can check what the server did with your application's request. For example, the following OpenDJ access log excerpt shows a successful connection from cn=My Killer App,ou=Apps,dc=example,dc=com performing a simple bind after Start TLS, and then a simple search before unbind. The lines are wrapped for readability, whereas in the log each record starts with the time stamp.

[20/Apr/2012:13:31:05 +0200] CONNECT conn=5
 from=127.0.0.1:51561 to=127.0.0.1:1389 protocol=LDAP
[20/Apr/2012:13:31:05 +0200] EXTENDED REQ conn=5 op=0 msgID=1 name="StartTLS"
 oid="1.3.6.1.4.1.1466.20037"
[20/Apr/2012:13:31:05 +0200] EXTENDED RES conn=5 op=0 msgID=1 name="StartTLS"
 oid="1.3.6.1.4.1.1466.20037" result=0 etime=0
[20/Apr/2012:13:31:07 +0200] BIND REQ conn=5 op=1 msgID=2 version=3 type=SIMPLE
 dn="cn=My Killer App,ou=Apps,dc=example,dc=com"
[20/Apr/2012:13:31:07 +0200] BIND RES conn=5 op=1 msgID=2 result=0
 authDN="cn=My Killer App,ou=Apps,dc=example,dc=com" etime=1
[20/Apr/2012:13:31:07 +0200] SEARCH REQ conn=5 op=2 msgID=3
 base="dc=example,dc=com" scope=wholeSubtree
 filter="(uid=kvaughan)" attrs="isMemberOf"
[20/Apr/2012:13:31:07 +0200] SEARCH RES conn=5 op=2 msgID=3
 result=0 nentries=1 etime=6
[20/Apr/2012:13:31:07 +0200] UNBIND REQ conn=5 op=3 msgID=4
[20/Apr/2012:13:31:07 +0200] DISCONNECT conn=5 reason="Client Unbind"

Notice that each operation type is shown in upper case, and that the server tracks both the connection (conn=5), operation (op=[0-3]), and message ID (msgID=[1-4]) numbers to make it easy to filter records. The etime refers to how long the server worked on the request in milliseconds. Result code 0 corresponds to ResultCode.SUCCESS, as described in RFC 4511.

2.17. Troubleshooting: Inspect Network Traffic

If result codes and server logs are not enough, many network tools can interpret LDAP packets. Get the necessary certificates to decrypt encrypted packet content.