org.xwiki.contrib.ldap.XWikiLDAPConnection.java Source code

Java tutorial

Introduction

Here is the source code for org.xwiki.contrib.ldap.XWikiLDAPConnection.java

Source

/*
 * See the NOTICE file distributed with this work for additional
 * information regarding copyright ownership.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.xwiki.contrib.ldap;

import java.io.UnsupportedEncodingException;
import java.security.Security;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.novell.ldap.LDAPAttribute;
import com.novell.ldap.LDAPAttributeSet;
import com.novell.ldap.LDAPConnection;
import com.novell.ldap.LDAPDN;
import com.novell.ldap.LDAPEntry;
import com.novell.ldap.LDAPException;
import com.novell.ldap.LDAPJSSESecureSocketFactory;
import com.novell.ldap.LDAPSearchConstraints;
import com.novell.ldap.LDAPSearchResults;
import com.novell.ldap.LDAPSocketFactory;
import com.xpn.xwiki.XWikiContext;

/**
 * LDAP communication tool.
 * 
 * @version $Id$
 * @since 8.3
 */
public class XWikiLDAPConnection {
    /**
     * Logging tool.
     */
    private static final Logger LOGGER = LoggerFactory.getLogger(XWikiLDAPConnection.class);

    /**
     * The LDAP connection.
     */
    private LDAPConnection connection;

    /**
     * LDAP attributes that should be treated as binary data.
     */
    private Set<String> binaryAttributes = new HashSet<>();

    private final XWikiLDAPConfig configuration;

    /**
     * @deprecated since 8.5, use {@link #XWikiLDAPConnection(XWikiLDAPConfig)} instead
     */
    @Deprecated
    public XWikiLDAPConnection() {
        this(new XWikiLDAPConfig(null));
    }

    /**
     * @param configuration the configuration to use
     * @since 9.0
     */
    public XWikiLDAPConnection(XWikiLDAPConfig configuration) {
        this.configuration = configuration;
    }

    /**
     * @param connection the connection to copy
     */
    public XWikiLDAPConnection(org.xwiki.contrib.ldap.XWikiLDAPConnection connection) {
        this();

        this.connection = connection.connection;
        this.binaryAttributes = connection.binaryAttributes;
    }

    /**
     * @param context the XWiki context.
     * @return the maximum number of milliseconds the client waits for any operation under these constraints to
     *         complete.
     */
    private int getTimeout(XWikiContext context) {
        return this.configuration.getLDAPTimeout();
    }

    /**
     * @param context the XWiki context.
     * @return the maximum number of search results to be returned from a search operation.
     */
    private int getMaxResults(XWikiContext context) {
        return this.configuration.getLDAPMaxResults();
    }

    /**
     * @return the {@link LDAPConnection}.
     */
    public LDAPConnection getConnection() {
        return this.connection;
    }

    /**
     * Open a LDAP connection.
     * 
     * @param ldapUserName the user name to connect to LDAP server.
     * @param password the password to connect to LDAP server.
     * @param context the XWiki context.
     * @return true if connection succeed, false otherwise.
     * @throws XWikiLDAPException error when trying to open connection.
     */
    public boolean open(String ldapUserName, String password, XWikiContext context) throws XWikiLDAPException {
        // open LDAP
        int ldapPort = this.configuration.getLDAPPort();
        String ldapHost = this.configuration.getLDAPParam("ldap_server", "localhost");

        // allow to use the given user and password also as the LDAP bind user and password
        String bindDN = this.configuration.getLDAPBindDN(ldapUserName, password);
        String bindPassword = this.configuration.getLDAPBindPassword(ldapUserName, password);

        boolean bind;
        if ("1".equals(this.configuration.getLDAPParam("ldap_ssl", "0"))) {
            String keyStore = this.configuration.getLDAPParam("ldap_ssl.keystore", "");

            LOGGER.debug("Connecting to LDAP using SSL");

            bind = open(ldapHost, ldapPort, bindDN, bindPassword, keyStore, true, context);
        } else {
            bind = open(ldapHost, ldapPort, bindDN, bindPassword, null, false, context);
        }

        return bind;
    }

    /**
     * Open LDAP connection.
     * 
     * @param ldapHost the host of the server to connect to.
     * @param ldapPort the port of the server to connect to.
     * @param loginDN the user DN to connect to LDAP server.
     * @param password the password to connect to LDAP server.
     * @param pathToKeys the path to SSL keystore to use.
     * @param ssl if true connect using SSL.
     * @param context the XWiki context.
     * @return true if the connection succeed, false otherwise.
     * @throws XWikiLDAPException error when trying to open connection.
     */
    public boolean open(String ldapHost, int ldapPort, String loginDN, String password, String pathToKeys,
            boolean ssl, XWikiContext context) throws XWikiLDAPException {
        int port = ldapPort;

        if (port <= 0) {
            port = ssl ? LDAPConnection.DEFAULT_SSL_PORT : LDAPConnection.DEFAULT_PORT;
        }

        setBinaryAttributes(this.configuration.getBinaryAttributes());

        try {
            if (ssl) {
                // Dynamically set JSSE as a security provider
                Security.addProvider(this.configuration.getSecureProvider());

                if (pathToKeys != null && pathToKeys.length() > 0) {
                    // Dynamically set the property that JSSE uses to identify
                    // the keystore that holds trusted root certificates

                    System.setProperty("javax.net.ssl.trustStore", pathToKeys);
                    // obviously unnecessary: sun default pwd = "changeit"
                    // System.setProperty("javax.net.ssl.trustStorePassword", sslpwd);
                }

                LDAPSocketFactory ssf = new LDAPJSSESecureSocketFactory();

                // Set the socket factory as the default for all future connections
                // LDAPConnection.setSocketFactory(ssf);

                // Note: the socket factory can also be passed in as a parameter
                // to the constructor to set it for this connection only.
                this.connection = new LDAPConnection(ssf);
            } else {
                this.connection = new LDAPConnection();
            }

            // connect
            connect(ldapHost, port);

            // set referral following
            LDAPSearchConstraints constraints = new LDAPSearchConstraints(this.connection.getConstraints());
            constraints.setTimeLimit(getTimeout(context));
            constraints.setMaxResults(getMaxResults(context));
            constraints.setReferralFollowing(true);
            constraints.setReferralHandler(new LDAPPluginReferralHandler(loginDN, password, context));
            this.connection.setConstraints(constraints);

            // bind
            bind(loginDN, password);
        } catch (UnsupportedEncodingException e) {
            throw new XWikiLDAPException("LDAP bind failed with UnsupportedEncodingException.", e);
        } catch (LDAPException e) {
            throw new XWikiLDAPException("LDAP bind failed with LDAPException.", e);
        }

        return true;
    }

    /**
     * Connect to server.
     * 
     * @param ldapHost the host of the server to connect to.
     * @param port the port of the server to connect to.
     * @throws LDAPException error when trying to connect.
     */
    private void connect(String ldapHost, int port) throws LDAPException {
        LOGGER.debug("Connection to LDAP server [{}:{}]", ldapHost, port);

        // connect to the server
        this.connection.connect(ldapHost, port);
    }

    /**
     * Bind to LDAP server.
     * 
     * @param loginDN the user DN to connect to LDAP server.
     * @param password the password to connect to LDAP server.
     * @throws UnsupportedEncodingException error when converting provided password to UTF-8 table.
     * @throws LDAPException error when trying to bind.
     */
    public void bind(String loginDN, String password) throws UnsupportedEncodingException, LDAPException {
        LOGGER.debug("Binding to LDAP server with credentials login=[{}]", loginDN);

        // authenticate to the server
        this.connection.bind(LDAPConnection.LDAP_V3, loginDN, password.getBytes("UTF8"));
    }

    /**
     * Close LDAP connection.
     */
    public void close() {
        try {
            if (this.connection != null) {
                this.connection.disconnect();
            }
        } catch (LDAPException e) {
            LOGGER.debug("LDAP close failed.", e);
        }
    }

    /**
     * Check if provided password is correct provided users's password.
     * 
     * @param userDN the user.
     * @param password the password.
     * @return true if the password is valid, false otherwise.
     */
    public boolean checkPassword(String userDN, String password) {
        return checkPassword(userDN, password, "userPassword");
    }

    /**
     * Check if provided password is correct provided users's password.
     * 
     * @param userDN the user.
     * @param password the password.
     * @param passwordField the name of the LDAP field containing the password.
     * @return true if the password is valid, false otherwise.
     */
    public boolean checkPassword(String userDN, String password, String passwordField) {
        try {
            LDAPAttribute attribute = new LDAPAttribute(passwordField, password);
            return this.connection.compare(userDN, attribute);
        } catch (LDAPException e) {
            if (e.getResultCode() == LDAPException.NO_SUCH_OBJECT) {
                LOGGER.debug("Unable to locate user_dn [{}]", userDN, e);
            } else if (e.getResultCode() == LDAPException.NO_SUCH_ATTRIBUTE) {
                LOGGER.debug("Unable to verify password because userPassword attribute not found.", e);
            } else {
                LOGGER.debug("Unable to verify password", e);
            }
        }

        return false;
    }

    /**
     * Execute a LDAP search query and return the first entry.
     * 
     * @param baseDN the root DN from where to search.
     * @param filter the LDAP filter.
     * @param attr the attributes names of values to return.
     * @param ldapScope the scope of the entries to search. The following are the valid options:
     *            <ul>
     *            <li>SCOPE_BASE - searches only the base DN
     *            <li>SCOPE_ONE - searches only entries under the base DN
     *            <li>SCOPE_SUB - searches the base DN and all entries within its subtree
     *            </ul>
     * @return the found LDAP attributes.
     */
    public List<XWikiLDAPSearchAttribute> searchLDAP(String baseDN, String filter, String[] attr, int ldapScope) {
        List<XWikiLDAPSearchAttribute> searchAttributeList = null;

        LDAPSearchResults searchResults = null;

        try {
            // filter return all attributes return attrs and values time out value
            searchResults = search(baseDN, filter, attr, ldapScope);

            if (!searchResults.hasMore()) {
                return null;
            }

            LDAPEntry nextEntry = searchResults.next();
            String foundDN = nextEntry.getDN();

            searchAttributeList = new ArrayList<XWikiLDAPSearchAttribute>();

            searchAttributeList.add(new XWikiLDAPSearchAttribute("dn", foundDN));

            LDAPAttributeSet attributeSet = nextEntry.getAttributeSet();

            ldapToXWikiAttribute(searchAttributeList, attributeSet);
        } catch (LDAPException e) {
            LOGGER.debug("LDAP Search failed", e);
        } finally {
            if (searchResults != null) {
                try {
                    this.connection.abandon(searchResults);
                } catch (LDAPException e) {
                    LOGGER.debug("LDAP Search clean up failed", e);
                }
            }
        }

        LOGGER.debug("LDAP search found attributes [{}]", searchAttributeList);

        return searchAttributeList;
    }

    /**
     * @param baseDN the root DN from where to search.
     * @param filter filter the LDAP filter
     * @param attr the attributes names of values to return
     * @param ldapScope the scope of the entries to search. The following are the valid options:
     *            <ul>
     *            <li>SCOPE_BASE - searches only the base DN
     *            <li>SCOPE_ONE - searches only entries under the base DN
     *            <li>SCOPE_SUB - searches the base DN and all entries within its subtree
     *            </ul>
     * @return a result stream. LDAPConnection#abandon should be called when it's not needed anymore.
     * @throws LDAPException error when searching
     * @since 3.3M1
     */
    public LDAPSearchResults search(String baseDN, String filter, String[] attr, int ldapScope)
            throws LDAPException {
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("LDAP search: baseDN=[{}] query=[{}] attr=[{}] ldapScope=[{}]",
                    new Object[] { baseDN, filter, attr != null ? Arrays.asList(attr) : null, ldapScope });
        }

        return this.connection.search(baseDN, ldapScope, filter, attr, false);
    }

    /**
     * Fill provided <code>searchAttributeList</code> with provided LDAP attributes.
     * 
     * @param searchAttributeList the XWiki attributes.
     * @param attributeSet the LDAP attributes.
     */
    protected void ldapToXWikiAttribute(List<XWikiLDAPSearchAttribute> searchAttributeList,
            LDAPAttributeSet attributeSet) {
        for (LDAPAttribute attribute : (Set<LDAPAttribute>) attributeSet) {
            String attributeName = attribute.getName();

            if (!isBinaryAttribute(attributeName)) {
                LOGGER.debug("  - values for attribute [{}]", attributeName);

                Enumeration<String> allValues = attribute.getStringValues();

                if (allValues != null) {
                    while (allValues.hasMoreElements()) {
                        String value = allValues.nextElement();

                        LOGGER.debug("    |- [{}]", value);

                        searchAttributeList.add(new XWikiLDAPSearchAttribute(attributeName, value));
                    }
                }
            } else {
                LOGGER.debug("  - attribute [{}] is binary", attributeName);

                Enumeration<byte[]> allValues = attribute.getByteValues();

                if (allValues != null) {
                    while (allValues.hasMoreElements()) {
                        byte[] value = allValues.nextElement();

                        searchAttributeList.add(new XWikiLDAPSearchAttribute(attributeName, value));
                    }
                }
            }
        }
    }

    /**
     * Fully escape DN value (the part after the =).
     * <p>
     * For example, for the dn value "Acme, Inc", the escapeLDAPDNValue method returns "Acme\, Inc".
     * </p>
     * 
     * @param value the DN value to escape
     * @return the escaped version o the DN value
     */
    public static String escapeLDAPDNValue(String value) {
        return StringUtils.isBlank(value) ? value : LDAPDN.escapeRDN("key=" + value).substring(4);
    }

    /**
     * Escape part of a LDAP query filter.
     * 
     * @param value the value to escape
     * @return the escaped version
     */
    public static String escapeLDAPSearchFilter(String value) {
        if (value == null) {
            return null;
        }

        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < value.length(); i++) {
            char curChar = value.charAt(i);
            switch (curChar) {
            case '\\':
                sb.append("\\5c");
                break;
            case '*':
                sb.append("\\2a");
                break;
            case '(':
                sb.append("\\28");
                break;
            case ')':
                sb.append("\\29");
                break;
            case '\u0000':
                sb.append("\\00");
                break;
            default:
                sb.append(curChar);
            }
        }
        return sb.toString();
    }

    /**
     * Update list of LDAP attributes that should be treated as binary data.
     * 
     * @param binaryAttributes set of binary attributes
     */
    private void setBinaryAttributes(Set<String> binaryAttributes) {
        this.binaryAttributes = binaryAttributes;
    }

    /**
     * Checks whether attribute should be treated as binary data.
     * 
     * @param attributeName name of attribute to check
     * @return true if attribute should be treated as binary data.
     */
    private boolean isBinaryAttribute(String attributeName) {
        return binaryAttributes.contains(attributeName);
    }
}