no.feide.moria.directory.backend.JNDIBackend.java Source code

Java tutorial

Introduction

Here is the source code for no.feide.moria.directory.backend.JNDIBackend.java

Source

/*
 * Copyright (c) 2004 UNINETT FAS This program is free software; you can
 * redistribute it and/or modify it under the terms of the GNU General Public
 * License as published by the Free Software Foundation; either version 2 of the
 * License, or (at your option) any later version. This program is distributed
 * in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
 * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU General Public License for more details. You should have received
 * a copy of the GNU General Public License along with this program; if not,
 * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 * Boston, MA 02111-1307, USA.
 */

package no.feide.moria.directory.backend;

import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Timer;
import java.util.Vector;

import javax.naming.AuthenticationException;
import javax.naming.AuthenticationNotSupportedException;
import javax.naming.ConfigurationException;
import javax.naming.Context;
import javax.naming.NameNotFoundException;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.SizeLimitExceededException;
import javax.naming.TimeLimitExceededException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.BasicAttributes;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import javax.naming.ldap.InitialLdapContext;

import no.feide.moria.directory.Credentials;
import no.feide.moria.directory.index.IndexedReference;
import no.feide.moria.log.MessageLogger;

import org.apache.commons.codec.binary.Base64;

/**
 * Java Naming and Directory Interface (JNDI) backend. Used to authenticate
 * users and retrieve the associated attributes.
 */
public final class JNDIBackend implements DirectoryManagerBackend {

    /** The message logger. */
    private final MessageLogger log = new MessageLogger(JNDIBackend.class);

    /** The external reference of this backend. */
    private IndexedReference[] myReferences;

    /** The connection timeout used. */
    private final int myTimeout;

    /** Default initial LDAP context environment. */
    private Hashtable<String, String> defaultEnv;

    /** The name of the attribute holding the username. */
    private String usernameAttribute;

    /** The name of the attribute used to guess a user's (R)DN. */
    private String guessedAttribute;

    /** The session ticket used when logging from this instance. */
    private String mySessionTicket = null;

    /**
     * Protected constructor. Creates an initial default context environment and
     * adds support for referrals, a fix for OpenSSL aliases, and enables SSL as
     * default.
     * @param sessionTicket
     *            The session ticket for this instance, used when logging. May
     *            be <code>null</code> (which is treated as an empty string)
     *            or an empty string.
     * @param timeout
     *            The number of seconds before a connection attempt through this
     *            backend times out.
     * @param ssl
     *            <code>true</code> if SSL is to be used, otherwise
     *            <code>false</code>.
     * @param usernameAttributeName
     *            The name of the attribute holding the username. Cannot be
     *            <code>null</code>.
     * @param guessedAttributeName
     *            If we search but cannot find a user element (for example, if
     *            it is not searchable), we will guess that the (R)DN starts
     *            with the substring
     *            <code><i>guessedAttributeName</i>=<i>usernamePrefix</i></code>,
     *            where <code><i>usernamePrefix</i></code> is the part of the
     *            username preceding the 'at' character. Cannot be
     *            <code>null</code>.
     * @throws IllegalArgumentException
     *             If <code>timeout</code> is less than zero.
     * @throws NullPointerException
     *             If <code>guessedAttributeName</code> or
     *             <code>usernameAttribute</code> is <code>null</code>.
     */
    protected JNDIBackend(final String sessionTicket, final int timeout, final boolean ssl,
            final String usernameAttributeName, final String guessedAttributeName)
            throws IllegalArgumentException, NullPointerException {

        // Assignments, with sanity checks.
        if (usernameAttributeName == null)
            throw new NullPointerException("Username attribute name cannot be NULL");
        usernameAttribute = usernameAttributeName;
        if (guessedAttributeName == null)
            throw new NullPointerException("Guessed attribute name cannot be NULL");
        guessedAttribute = guessedAttributeName;
        if (timeout < 0)
            throw new IllegalArgumentException("Timeout must be greater than zero");
        myTimeout = timeout;
        mySessionTicket = sessionTicket;
        if (mySessionTicket == null)
            mySessionTicket = "";

        // Create initial context environment.
        defaultEnv = new Hashtable<String, String>();
        defaultEnv.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");

        // To catch referrals.
        defaultEnv.put(Context.REFERRAL, "throw");

        // Due to OpenSSL problems.
        defaultEnv.put("java.naming.ldap.derefAliases", "never");

        // Use LDAP v3.
        defaultEnv.put("java.naming.ldap.version", "3");

        // Add timeout value for connection attempts (not searches).
        defaultEnv.put("com.sun.jndi.ldap.connect.timeout", String.valueOf(1000 * timeout));

        // Should we enable SSL?
        if (ssl)
            defaultEnv.put(Context.SECURITY_PROTOCOL, "ssl");

    }

    /**
     * Opens this backend. Does not actually initialize the network connection
     * to the external LDAP.
     * @param references
     *            The external reference to the LDAP server. Cannot be
     *            <code>null</code>, and must contain at least one reference.
     * @throws IllegalArgumentException
     *             If <code>reference</code> is <code>null</code>, or an
     *             empty array.
     */
    public final void open(final IndexedReference[] references) {

        // Sanity check.
        if ((references == null) || (references.length == 0))
            throw new IllegalArgumentException("Reference cannot be NULL or an empty array");

        // Create a local copy of the references.
        ArrayList<IndexedReference> newReferences = new ArrayList<IndexedReference>(references.length);
        for (int i = 0; i < references.length; i++)
            newReferences.add(references[i]);
        myReferences = newReferences.toArray(new IndexedReference[] {});

    }

    /**
     * Checks whether a user element exists, based on its username value.
     * @param username
     *            User name.
     * @return <code>true</code> if the user can be looked up through JNDI,
     *         otherwise <code>false</code>.
     * @throws BackendException
     *             If there is a problem accessing the backend.
     */
    public final boolean userExists(final String username) throws BackendException {

        // Sanity checks.
        if ((username == null) || (username.length() == 0))
            return false;

        // The search pattern.
        String pattern = usernameAttribute + '=' + username;

        // Go through all references.
        InitialLdapContext ldap = null;
        for (int i = 0; i < myReferences.length; i++) {
            String[] references = myReferences[i].getReferences();
            final String[] usernames = myReferences[i].getUsernames();
            final String[] passwords = myReferences[i].getPasswords();
            for (int j = 0; j < references.length; j++) {

                try {

                    // Context for this reference.
                    try {
                        ldap = connect(references[j]);
                    } catch (NamingException e) {
                        // Connection failed, but we might have other sources.
                        log.logWarn("Unable to access the backend on '" + references[j]
                                + "' to verify existence of '" + username + "': " + e.getClass().getName(),
                                mySessionTicket, e);
                        continue;
                    }

                    // Anonymous search or not?
                    ldap.addToEnvironment(Context.SECURITY_AUTHENTICATION, "simple");
                    if ((usernames[j].length() == 0) && (passwords[j].length() > 0))
                        log.logWarn("Search username is empty but search password is not - possible index problem",
                                mySessionTicket);
                    else if ((passwords[j].length() == 0) && (usernames[j].length() > 0))
                        log.logWarn("Search password is empty but search username is not - possible index problem",
                                mySessionTicket);
                    else if ((passwords[j].length() == 0) && (usernames[j].length() == 0)) {
                        log.logDebug("Anonymous search for user element DN on " + references[j], mySessionTicket);
                        ldap.removeFromEnvironment(Context.SECURITY_AUTHENTICATION);
                    } else
                        log.logDebug("Non-anonymous search to verify existence of '" + username + "' on "
                                + references[j], mySessionTicket);
                    ldap.addToEnvironment(Context.SECURITY_PRINCIPAL, usernames[j]);
                    ldap.addToEnvironment(Context.SECURITY_CREDENTIALS, passwords[j]);

                    // Search this reference.
                    if (ldapSearch(ldap, pattern) != null)
                        return true;

                } catch (NamingException e) {

                    // Unable to connect, but we might have other sources.
                    log.logWarn("Unable to access the backend on '" + references[j] + "' to verify existence of '"
                            + username + "': " + e.getClass().getName(), mySessionTicket, e);
                    continue;

                } finally {

                    // Close the LDAP connection.
                    if (ldap != null) {
                        try {
                            ldap.close();
                        } catch (NamingException e) {
                            // Ignored.
                            log.logWarn("Unable to close the backend connection to '" + references[j] + "': "
                                    + e.getClass().getName(), mySessionTicket, e);
                        }
                    }
                }

            }
        }

        // Still no match.
        return false;

    }

    /**
     * Authenticates the user using the supplied credentials and retrieves the
     * requested attributes.
     * @param userCredentials
     *            User's credentials. Cannot be <code>null</code>.
     * @param attributeRequest
     *            Requested attributes.
     * @return The requested attributes (<code>String</code> names and
     *         <code>String[]</code> values), if they did exist in the
     *         external backend. Otherwise returns those attributes that could
     *         actually be read, this may be an empty <code>HashMap</code>.
     *         Returns an empty <code>HashMap</code> if
     *         <code>attributeRequest</code> is <code>null</code> or an
     *         empty array.
     * @throws AuthenticationFailedException
     *             If the authentication fails.
     * @throws BackendException
     *             If there is a problem accessing the backend.
     * @throws IllegalArgumentException
     *             If <code>userCredentials</code> is <code>null</code>.
     */
    public final HashMap<String, String[]> authenticate(final Credentials userCredentials,
            final String[] attributeRequest) throws AuthenticationFailedException, BackendException {

        // Sanity check.
        if (userCredentials == null)
            throw new IllegalArgumentException("Credentials cannot be NULL");

        // Go through all references.
        for (int i = 0; i < myReferences.length; i++) {
            final String[] references = myReferences[i].getReferences();
            final String[] usernames = myReferences[i].getUsernames();
            final String[] passwords = myReferences[i].getPasswords();
            for (int j = 0; j < references.length; j++) {

                // For the benefit of the finally block below.
                InitialLdapContext ldap = null;

                try {

                    // Context for this reference.
                    try {
                        ldap = connect(references[j]);
                    } catch (NamingException e) {
                        // Connection failed, but we might have other sources.
                        log.logWarn("Unable to access the backend on '" + references[j] + "': "
                                + e.getClass().getName(), mySessionTicket, e);
                        continue;
                    }

                    // Skip search phase if the reference(s) are explicit.
                    String rdn = "";
                    if (myReferences[i].isExplicitlyIndexed()) {

                        // Add the explicit reference; no search phase, no RDN.
                        ldap.addToEnvironment(Context.SECURITY_PRINCIPAL,
                                references[j].substring(references[j].lastIndexOf('/') + 1));

                    } else {

                        // Anonymous search or not?
                        ldap.addToEnvironment(Context.SECURITY_AUTHENTICATION, "simple");
                        if ((usernames[j].length() == 0) && (passwords[j].length() > 0))
                            log.logWarn(
                                    "Search username is empty but search password is not - possible index problem",
                                    mySessionTicket);
                        else if ((passwords[j].length() == 0) && (usernames[j].length() > 0))
                            log.logWarn(
                                    "Search password is empty but search username is not - possible index problem",
                                    mySessionTicket);
                        else if ((passwords[j].length() == 0) && (usernames[j].length() == 0)) {
                            log.logDebug("Anonymous search for user element DN on " + references[j],
                                    mySessionTicket);
                            ldap.removeFromEnvironment(Context.SECURITY_AUTHENTICATION);
                        } else
                            log.logDebug("Non-anonymous search for user element DN on " + references[j],
                                    mySessionTicket);
                        ldap.addToEnvironment(Context.SECURITY_PRINCIPAL, usernames[j]);
                        ldap.addToEnvironment(Context.SECURITY_CREDENTIALS, passwords[j]);

                        // Search using the implicit reference.
                        String pattern = usernameAttribute + '=' + userCredentials.getUsername();
                        rdn = ldapSearch(ldap, pattern);
                        if (rdn == null) {

                            // No user element found. Try to guess the RDN.
                            rdn = userCredentials.getUsername();
                            rdn = guessedAttribute + '=' + rdn.substring(0, rdn.indexOf('@'));
                            log.logDebug("No subtree match for " + pattern + " on " + references[j]
                                    + " - guessing on RDN " + rdn, mySessionTicket);

                        } else
                            log.logDebug("Matched " + pattern + " to " + rdn + ',' + ldap.getNameInNamespace(),
                                    mySessionTicket);
                        ldap.addToEnvironment(Context.SECURITY_PRINCIPAL, rdn + ',' + ldap.getNameInNamespace());
                    }

                    // Authenticate and get attributes.
                    ldap.addToEnvironment(Context.SECURITY_AUTHENTICATION, "simple");
                    ldap.addToEnvironment(Context.SECURITY_CREDENTIALS, userCredentials.getPassword());
                    try {
                        ldap.reconnect(null);
                        log.logDebug("Successfully authenticated " + userCredentials.getUsername() + " on "
                                + references[j], mySessionTicket);
                        return getAttributes(ldap, rdn, attributeRequest); // Success.
                    } catch (AuthenticationException e) {

                        // Authentication failed, but we may have other
                        // references.
                        log.logDebug("Failed to authenticate user " + userCredentials.getUsername() + " on "
                                + references[j] + " - authentication failed", mySessionTicket);
                        continue;

                    } catch (AuthenticationNotSupportedException e) {

                        // Password authentication not supported for the DN.
                        // We may still have other references.
                        log.logDebug("Failed to authenticate user " + userCredentials.getUsername() + " on "
                                + references[j] + " - authentication not supported", mySessionTicket);
                        continue;

                    }

                } catch (ConfigurationException e) {
                    throw new BackendException("Backend configuration problem with " + references[j], e);
                } catch (NamingException e) {
                    throw new BackendException("Unable to access the backend on " + references[j], e);
                } finally {

                    // Close the LDAP connection.
                    if (ldap != null) {
                        try {
                            ldap.close();
                        } catch (NamingException e) {
                            // Ignored.
                            log.logWarn(
                                    "Unable to close the backend connection to " + references[j] + " - ignoring",
                                    mySessionTicket, e);
                        }
                    }
                }

            }
        }

        // No user was found.
        throw new AuthenticationFailedException(
                "Failed to authenticate user " + userCredentials.getUsername() + " - no user found");

    }

    /**
     * Retrieves a list of attributes from an element.
     * @param ldap
     *            A prepared LDAP context. Cannot be <code>null</code>.
     * @param rdn
     *            The relative DN (to the DN in the LDAP context
     *            <code>ldap</code>). Cannot be <code>null</code>.
     * @param attributes
     *            The requested attribute's names. Also indirectly referenced
     *            attributes on the form
     *            <code>someReferenceAttribute:someIndirectAttribute</code>,
     *            where the DN in the reference attribute
     *            <code>someReferenceAttribute</code> is followed to look up
     *            <code>someIndirectAttribute</code> from another element.
     * @return The requested attributes (<code>String</code> names and
     *         <code>String[]</code> values), if they did exist in the
     *         external backend. Otherwise returns those attributes that could
     *         actually be read, this may be an empty <code>HashMap</code>.
     *         Returns an empty <code>HashMap</code> if
     *         <code>attributes</code> is <code>null</code> or an empty
     *         array. Note that attribute values are mapped to
     *         <code>String</code> using ISO-8859-1.
     * @throws BackendException
     *             If unable to read the attributes from the backend.
     * @throws NullPointerException
     *             If <code>ldap</code> or <code>rdn</code> is
     *             <code>null</code>.
     * @see javax.naming.directory.InitialDirContext#getAttributes(java.lang.String,
     *      java.lang.String[])
     */
    private HashMap<String, String[]> getAttributes(final InitialLdapContext ldap, final String rdn,
            final String[] attributes) throws BackendException {

        // Sanity checks.
        if (ldap == null)
            throw new NullPointerException("LDAP context cannot be NULL");
        if (rdn == null)
            throw new NullPointerException("RDN cannot be NULL");
        if ((attributes == null) || (attributes.length == 0))
            return new HashMap<String, String[]>();

        // Used to remember attributes to be read through references later on.
        Hashtable<String, Vector> attributeReferences = new Hashtable<String, Vector>();

        // Strip down request, resolving references and removing duplicates.
        Vector<String> strippedAttributeRequest = new Vector<String>();
        for (int i = 0; i < attributes.length; i++) {
            int indexOfSplitCharacter = attributes[i]
                    .indexOf(DirectoryManagerBackend.ATTRIBUTE_REFERENCE_SEPARATOR);
            if (indexOfSplitCharacter == -1) {

                // A regular attribute request.
                if (!strippedAttributeRequest.contains(attributes[i]))
                    strippedAttributeRequest.add(attributes[i]);

            } else {

                // A referenced attribute request.
                final String referencingAttribute = attributes[i].substring(0, indexOfSplitCharacter);
                if (!strippedAttributeRequest.contains(referencingAttribute))
                    strippedAttributeRequest.add(referencingAttribute);

                // Add to list of attributes to be read through each reference.
                if (!attributeReferences.containsKey(referencingAttribute)) {

                    // Add new reference.
                    Vector<String> referencedAttribute = new Vector<String>();
                    referencedAttribute.add(attributes[i].substring(indexOfSplitCharacter + 1));
                    attributeReferences.put(referencingAttribute, referencedAttribute);

                } else {

                    // Update existing reference.
                    Vector<String> referencedAttribute = attributeReferences.get(referencingAttribute);
                    if (!referencedAttribute.contains(attributes[i].substring(indexOfSplitCharacter + 1)))
                        referencedAttribute.add(attributes[i].substring(indexOfSplitCharacter + 1));

                }

            }

        }

        // The context provider URL and DN, for later logging.
        String url = "unknown backend";
        String dn = "unknown dn";

        // Get the attributes from an already initialized LDAP connection.
        Attributes rawAttributes = null;
        try {

            // Remember the URL and bind DN, for later logging.
            final Hashtable environment = ldap.getEnvironment();
            url = (String) environment.get(Context.PROVIDER_URL);
            dn = (String) environment.get(Context.SECURITY_PRINCIPAL);

            // Get the attributes.
            rawAttributes = ldap.getAttributes(rdn, strippedAttributeRequest.toArray(new String[] {}));

        } catch (NameNotFoundException e) {

            // Successful authentication but missing user element; no attributes
            // returned and the event is logged.
            log.logWarn("No LDAP element found (DN was '" + dn + "')", mySessionTicket);
            rawAttributes = new BasicAttributes();

        } catch (NamingException e) {
            String a = new String();
            for (int i = 0; i < attributes.length; i++)
                a = a + attributes[i] + ", ";
            throw new BackendException("Unable to read attribute(s) '" + a.substring(0, a.length() - 2) + "' from '"
                    + rdn + "' on '" + url + "'", e);
        }

        // Translate retrieved attributes from Attributes to HashMap.
        HashMap<String, String[]> convertedAttributes = new HashMap<String, String[]>();
        for (int i = 0; i < attributes.length; i++) {

            // Did we get any attribute back at all?
            final String requestedAttribute = attributes[i];
            Attribute rawAttribute = rawAttributes.get(requestedAttribute);
            if (rawAttribute == null) {

                // Attribute was not returned.
                log.logDebug("Requested attribute '" + requestedAttribute + "' not found on '" + url + "'",
                        mySessionTicket);

            } else {

                // Map the attribute values to String[].
                ArrayList<String> convertedAttributeValues = new ArrayList<String>(rawAttribute.size());
                for (int j = 0; j < rawAttribute.size(); j++) {
                    try {

                        // We either have a String or a byte[].
                        String convertedAttributeValue = null;
                        try {

                            // Encode String.
                            convertedAttributeValue = new String(((String) rawAttribute.get(j)).getBytes(),
                                    DirectoryManagerBackend.ATTRIBUTE_VALUE_CHARSET);
                        } catch (ClassCastException e) {

                            // Encode byte[] to String.
                            convertedAttributeValue = new String(Base64.encodeBase64((byte[]) rawAttribute.get(j)),
                                    DirectoryManagerBackend.ATTRIBUTE_VALUE_CHARSET);

                        }
                        convertedAttributeValues.add(convertedAttributeValue);

                    } catch (NamingException e) {
                        throw new BackendException("Unable to read attribute value of '" + rawAttribute.getID()
                                + "' from '" + url + "'", e);
                    } catch (UnsupportedEncodingException e) {
                        throw new BackendException(
                                "Unable to use " + DirectoryManagerBackend.ATTRIBUTE_VALUE_CHARSET + " encoding",
                                e);
                    }
                }
                convertedAttributes.put(requestedAttribute, convertedAttributeValues.toArray(new String[] {}));

            }

        }

        // Follow references to look up any indirectly referenced attributes.
        Enumeration<String> keys = attributeReferences.keys();
        while (keys.hasMoreElements()) {

            // Do we have a reference? 
            final String referencingAttribute = keys.nextElement();
            final String[] referencingValues = convertedAttributes.get(referencingAttribute);
            if (referencingValues == null) {

                // No reference was found in this attribute.
                log.logDebug("Found no DN references in attribute '" + referencingAttribute + "'", mySessionTicket);

            } else {

                // One (or more) references was found in this attribute.
                if (referencingValues.length > 1)
                    log.logDebug("Found " + referencingValues.length + " DN references in attribute '"
                            + referencingAttribute + "'; ignoring all but first", mySessionTicket);
                log.logDebug("Following reference '" + referencingValues[0] + "' found in '" + referencingAttribute
                        + "' to look up attribute(s) '" + attributeReferences.get(referencingAttribute).toString(),
                        mySessionTicket);
                String providerURL = null; // To be used later.
                try {

                    // Follow the reference.
                    providerURL = (String) ldap.getEnvironment().get(Context.PROVIDER_URL);
                    providerURL = providerURL.substring(0, providerURL.lastIndexOf("/") + 1) + referencingValues[0];
                    ldap.addToEnvironment(Context.PROVIDER_URL, providerURL);

                } catch (NamingException e) {
                    throw new BackendException("Unable to update provider URL in LDAP environment", e);
                }

                // Add any referenced attributes returned.
                HashMap additionalAttributes = getAttributes(ldap, providerURL,
                        (String[]) attributeReferences.get(referencingAttribute).toArray(new String[] {}));
                Iterator i = additionalAttributes.keySet().iterator();
                while (i.hasNext()) {
                    String attributeName = (String) i.next();
                    convertedAttributes.put(referencingAttribute
                            + DirectoryManagerBackend.ATTRIBUTE_REFERENCE_SEPARATOR + attributeName,
                            (String[]) additionalAttributes.get(attributeName));
                }

            }

        }

        return convertedAttributes;

    }

    /**
     * Does nothing, but needed to fulfill the
     * <code>DirectoryManagerBackend</code> interface. Actual backend
     * connections are closed after each use.
     * @see DirectoryManagerBackend#close()
     */
    public void close() {

        // Does nothing.

    }

    /**
     * Does a subtree search for an element given a pattern. Only the first
     * element found is considered, and all references are searched in order
     * until either a match is found or no more references are left to search.
     * @param ldap
     *            A prepared LDAP context.
     * @param pattern
     *            The search pattern. Must not include the character '*' or the
     *            substring '\2a' to prevent possible LDAP exploits.
     * @return The element's relative DN, or <code>null</code> if none was
     *         found. <code>null</code> is also returned if the search pattern
     *         contains an illegal character or substring.
     * @throws BackendException
     *             If there was a problem accessing the backend. Typical causes
     *             include timeouts.
     */
    private String ldapSearch(final InitialLdapContext ldap, final String pattern) throws BackendException {

        // Check pattern for illegal content.
        String[] illegals = { "*", "\\2a" };
        for (int i = 0; i < illegals.length; i++) {
            if (pattern.indexOf(illegals[i]) > -1)
                return null;
        }

        // The context provider URL, for later logging.
        String url = "unknown backend";

        // Start counting the (milli)seconds and prepare for timeouts.
        long searchStart = System.currentTimeMillis();
        JNDISearchInterruptor interruptTask = new JNDISearchInterruptor(ldap, mySessionTicket);
        NamingEnumeration results;
        try {

            // Remember the URL, for later logging.
            url = (String) ldap.getEnvironment().get(Context.PROVIDER_URL);
            interruptTask.setURL(url);

            // Start timeout interruptor and perform the search.
            Timer interruptTimer = new Timer();
            interruptTimer.schedule(interruptTask, (1000 * myTimeout));
            results = ldap.search("", pattern, new SearchControls(SearchControls.SUBTREE_SCOPE, 0, 1000 * myTimeout,
                    new String[] {}, false, false));
            interruptTimer.cancel();
            if (!results.hasMore())
                return null;

        } catch (TimeLimitExceededException e) {

            // The search timed out.
            log.logWarn("Search on " + url + " for " + pattern + " timed out after ~"
                    + (System.currentTimeMillis() - searchStart) + "ms", mySessionTicket);
            return null;

        } catch (SizeLimitExceededException e) {

            // The search returned too many results.
            log.logWarn("Search on " + url + " for " + pattern + " returned too many results", mySessionTicket);
            return null;

        } catch (NameNotFoundException e) {

            // Element not found. Possibly non-existing reference.
            log.logDebug("Could not find " + pattern + " on " + url, mySessionTicket); // Necessary?
            return null;

        } catch (AuthenticationException e) {

            // Search failed authentication; check non-anonymous search config.
            try {
                final String searchUser = (String) ldap.getEnvironment().get(Context.SECURITY_PRINCIPAL);
                final String errorMessage;
                if ((searchUser == null) || searchUser.equals(""))
                    errorMessage = "Anonymous search failed authentication on " + url;
                else
                    errorMessage = "Could not authenticate search user " + searchUser + " on " + url;
                log.logDebug(errorMessage, mySessionTicket);
                throw new BackendException(errorMessage, e);
            } catch (NamingException f) {

                // Should not happen!
                log.logCritical("Unable to read LDAP environment", mySessionTicket, f);
                throw new BackendException("Unable to read LDAP environment", f);

            }

        } catch (NamingException e) {

            // Did we interrupt the search ourselves?
            if (interruptTask.finished()) {
                final long elapsed = System.currentTimeMillis() - searchStart;
                log.logWarn("Search on " + url + " for " + pattern + " timed out after ~" + elapsed + "ms",
                        mySessionTicket);
                throw new BackendException("Search on " + url + " for " + pattern + " timed out after ~" + elapsed
                        + "ms; connection terminated");
            }

            // All other exceptions.
            log.logWarn("Search on " + url + " for " + pattern + " failed", mySessionTicket, e);
            return null;

        }

        // We just found at least one element. Did we get an ambigious result?
        SearchResult entry = null;
        try {
            entry = (SearchResult) results.next();
            String buffer = new String();
            while (results.hasMoreElements())
                buffer = buffer + ", " + ((SearchResult) results.next()).getName();
            if (!buffer.equals(""))
                log.logWarn("Search on " + url + " for " + pattern + " gave ambiguous result: [" + entry.getName()
                        + buffer + "]", mySessionTicket);
            // TODO: Throw BackendException, or a subclass, or just (as now)
            // pick the first and hope for the best?
            buffer = null;
        } catch (NamingException e) {
            throw new BackendException("Unable to read search results", e);
        }
        return entry.getName(); // Relative DN (to the reference).

    }

    /**
     * Creates a new connection to a given backend provider URL.
     * @param url
     *            The backend provider URL.
     * @return The opened backend connection.
     * @throws NamingException
     *             If unable to connect to the provider given by
     *             <code>url</code>.
     */
    private InitialLdapContext connect(final String url) throws NamingException {

        // Prepare connection.
        Hashtable<String, String> env = new Hashtable<String, String>(defaultEnv);
        env.put(Context.PROVIDER_URL, url);
        return new InitialLdapContext(env, null);

    }
}