org.nuxeo.ecm.directory.ldap.LDAPDirectory.java Source code

Java tutorial

Introduction

Here is the source code for org.nuxeo.ecm.directory.ldap.LDAPDirectory.java

Source

/*
 * (C) Copyright 2006-2007 Nuxeo SA (http://nuxeo.com/) and others.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * Contributors:
 *     Nuxeo - initial API and implementation
 *
 * $Id$
 */

package org.nuxeo.ecm.directory.ldap;

import java.io.IOException;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

import javax.naming.Context;
import javax.naming.NamingException;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import javax.naming.directory.SearchControls;
import javax.net.SocketFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.nuxeo.ecm.core.schema.SchemaManager;
import org.nuxeo.ecm.core.schema.types.Field;
import org.nuxeo.ecm.core.schema.types.Schema;
import org.nuxeo.ecm.directory.AbstractDirectory;
import org.nuxeo.ecm.directory.DirectoryException;
import org.nuxeo.ecm.directory.DirectoryFieldMapper;
import org.nuxeo.ecm.directory.Reference;
import org.nuxeo.ecm.directory.Session;
import org.nuxeo.runtime.api.Framework;

/**
 * Implementation of the Directory interface for servers implementing the Lightweight Directory Access Protocol.
 *
 * @author ogrisel
 * @author Robert Browning
 */
public class LDAPDirectory extends AbstractDirectory {

    private static final Log log = LogFactory.getLog(LDAPDirectory.class);

    // special field key to be able to read the DN of an LDAP entry
    public static final String DN_SPECIAL_ATTRIBUTE_KEY = "dn";

    protected Properties contextProperties;

    protected SearchControls searchControls;

    protected Map<String, Field> schemaFieldMap;

    protected final LDAPDirectoryFactory factory;

    protected String baseFilter;

    // the following attribute is only used for testing purpose
    protected ContextProvider testServer;

    public LDAPDirectory(LDAPDirectoryDescriptor descriptor) {
        super(descriptor);
        if (StringUtils.isEmpty(descriptor.getSearchBaseDn())) {
            throw new DirectoryException("searchBaseDn configuration is missing for directory " + getName());
        }
        factory = Framework.getService(LDAPDirectoryFactory.class);
    }

    @Override
    public LDAPDirectoryDescriptor getDescriptor() {
        return (LDAPDirectoryDescriptor) descriptor;
    }

    @Override
    public List<Reference> getReferences(String referenceFieldName) {
        if (schemaFieldMap == null) {
            initLDAPConfig();
        }
        return references.get(referenceFieldName);
    }

    /**
     * Lazy init method for ldap config
     *
     * @since 6.0
     */
    protected void initLDAPConfig() {
        LDAPDirectoryDescriptor ldapDirectoryDesc = getDescriptor();
        // computing attributes that will be useful for all sessions
        SchemaManager schemaManager = Framework.getLocalService(SchemaManager.class);
        Schema schema = schemaManager.getSchema(getSchema());
        if (schema == null) {
            throw new DirectoryException(getSchema() + " is not a registered schema");
        }
        schemaFieldMap = new LinkedHashMap<>();
        for (Field f : schema.getFields()) {
            schemaFieldMap.put(f.getName().getLocalName(), f);
        }

        // init field mapper before search fields
        fieldMapper = new DirectoryFieldMapper(ldapDirectoryDesc.fieldMapping);
        contextProperties = computeContextProperties();
        baseFilter = ldapDirectoryDesc.getAggregatedSearchFilter();

        // register the references
        addReferences(ldapDirectoryDesc.getInverseReferences());
        addReferences(ldapDirectoryDesc.getLdapReferences());

        // register the search controls after having registered the references
        // since the list of attributes to fetch my depend on registered
        // LDAPReferences
        searchControls = computeSearchControls();

        // cache parameterization
        cache.setEntryCacheName(ldapDirectoryDesc.cacheEntryName);
        cache.setEntryCacheWithoutReferencesName(ldapDirectoryDesc.cacheEntryWithoutReferencesName);
        cache.setNegativeCaching(ldapDirectoryDesc.negativeCaching);

        log.debug(String.format("initialized LDAP directory %s with fields [%s] and references [%s]", getName(),
                StringUtils.join(schemaFieldMap.keySet().toArray(), ", "),
                StringUtils.join(references.keySet().toArray(), ", ")));
    }

    /**
     * @return connection parameters to use for all LDAP queries
     */
    protected Properties computeContextProperties() throws DirectoryException {
        LDAPDirectoryDescriptor ldapDirectoryDesc = getDescriptor();
        // Initialization of LDAP connection parameters from parameters
        // registered in the LDAP "server" extension point
        Properties props = new Properties();
        LDAPServerDescriptor serverConfig = getServer();

        if (null == serverConfig) {
            throw new DirectoryException(
                    "LDAP server configuration not found: " + ldapDirectoryDesc.getServerName());
        }

        props.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");

        /*
         * Get initial connection URLs, dynamic URLs may cause the list to be updated when creating the session
         */
        String ldapUrls = serverConfig.getLdapUrls();
        if (ldapUrls == null) {
            throw new DirectoryException("Server LDAP URL configuration is missing for directory " + getName());
        }
        props.put(Context.PROVIDER_URL, ldapUrls);

        // define how referrals are handled
        if (!getDescriptor().getFollowReferrals()) {
            props.put(Context.REFERRAL, "ignore");
        } else {
            // this is the default mode
            props.put(Context.REFERRAL, "follow");
        }

        /*
         * SSL Connections do not work with connection timeout property
         */
        if (serverConfig.getConnectionTimeout() > -1) {
            if (!serverConfig.useSsl()) {
                props.put("com.sun.jndi.ldap.connect.timeout",
                        Integer.toString(serverConfig.getConnectionTimeout()));
            } else {
                log.warn("SSL connections do not operate correctly"
                        + " when used with the connection timeout parameter, disabling timout");
            }
        }

        String bindDn = serverConfig.getBindDn();
        if (bindDn != null) {
            // Authenticated connection
            props.put(Context.SECURITY_PRINCIPAL, bindDn);
            props.put(Context.SECURITY_CREDENTIALS, serverConfig.getBindPassword());
        }

        if (serverConfig.isPoolingEnabled()) {
            // Enable connection pooling
            props.put("com.sun.jndi.ldap.connect.pool", "true");
            props.put("com.sun.jndi.ldap.connect.pool.protocol", "plain ssl");
            props.put("com.sun.jndi.ldap.connect.pool.authentication", "none simple DIGEST-MD5");
            props.put("com.sun.jndi.ldap.connect.pool.timeout", "1800000"); // 30
            // min
        }

        if (!serverConfig.isVerifyServerCert() && serverConfig.useSsl) {
            props.put("java.naming.ldap.factory.socket",
                    "org.nuxeo.ecm.directory.ldap.LDAPDirectory$TrustingSSLSocketFactory");
        }

        return props;
    }

    public Properties getContextProperties() {
        return contextProperties;
    }

    /**
     * Search controls that only fetch attributes defined by the schema
     *
     * @return common search controls to use for all LDAP search queries
     * @throws DirectoryException
     */
    protected SearchControls computeSearchControls() throws DirectoryException {
        LDAPDirectoryDescriptor ldapDirectoryDesc = getDescriptor();
        SearchControls scts = new SearchControls();
        // respect the scope of the configuration
        scts.setSearchScope(ldapDirectoryDesc.getSearchScope());

        // only fetch attributes that are defined in the schema or needed to
        // compute LDAPReferences
        Set<String> attrs = new HashSet<>();
        for (String fieldName : schemaFieldMap.keySet()) {
            if (!references.containsKey(fieldName)) {
                attrs.add(fieldMapper.getBackendField(fieldName));
            }
        }
        attrs.add("objectClass");

        for (Reference reference : getReferences()) {
            if (reference instanceof LDAPReference) {
                LDAPReference ldapReference = (LDAPReference) reference;
                attrs.add(ldapReference.getStaticAttributeId(fieldMapper));
                attrs.add(ldapReference.getDynamicAttributeId());

                // Add Dynamic Reference attributes filtering
                for (LDAPDynamicReferenceDescriptor dynAtt : ldapReference.getDynamicAttributes()) {
                    attrs.add(dynAtt.baseDN);
                    attrs.add(dynAtt.filter);
                }

            }
        }

        if (getPasswordField() != null) {
            // never try to fetch the password
            attrs.remove(getPasswordField());
        }

        scts.setReturningAttributes(attrs.toArray(new String[attrs.size()]));

        scts.setCountLimit(ldapDirectoryDesc.getQuerySizeLimit());
        scts.setTimeLimit(ldapDirectoryDesc.getQueryTimeLimit());

        return scts;
    }

    public SearchControls getSearchControls() {
        return getSearchControls(false);
    }

    public SearchControls getSearchControls(boolean fetchAllAttributes) {
        if (fetchAllAttributes) {
            // return the precomputed scts instance
            return searchControls;
        } else {
            // build a new ftcs instance with no attribute filtering
            LDAPDirectoryDescriptor ldapDirectoryDesc = getDescriptor();
            SearchControls scts = new SearchControls();
            scts.setSearchScope(ldapDirectoryDesc.getSearchScope());
            scts.setReturningAttributes(new String[] { ldapDirectoryDesc.rdnAttribute,
                    ldapDirectoryDesc.fieldMapping.get(getIdField()) });
            return scts;
        }
    }

    protected DirContext createContext() throws DirectoryException {
        try {
            /*
             * Dynamic server list requires re-computation on each access
             */
            String serverName = getDescriptor().getServerName();
            if (StringUtils.isEmpty(serverName)) {
                throw new DirectoryException("server configuration is missing for directory " + getName());
            }
            LDAPServerDescriptor serverConfig = getServer();
            if (serverConfig.isDynamicServerList()) {
                String ldapUrls = serverConfig.getLdapUrls();
                contextProperties.put(Context.PROVIDER_URL, ldapUrls);
            }
            return new InitialDirContext(contextProperties);
        } catch (NamingException e) {
            throw new DirectoryException("Cannot connect to LDAP directory '" + getName() + "': " + e.getMessage(),
                    e);
        }
    }

    /**
     * @since 5.7
     * @return ldap server descriptor bound to this directory
     */
    public LDAPServerDescriptor getServer() {
        return factory.getServer(getDescriptor().getServerName());
    }

    @Override
    public Session getSession() throws DirectoryException {
        if (schemaFieldMap == null) {
            initLDAPConfig();
        }
        DirContext context;
        if (testServer != null) {
            context = testServer.getContext();
        } else {
            context = createContext();
        }
        Session session = new LDAPSession(this, context);
        addSession(session);
        return session;
    }

    public String getBaseFilter() {
        // NXP-2461: always add control on id field in base filter
        String idField = getIdField();
        String idAttribute = getFieldMapper().getBackendField(idField);
        String idFilter = String.format("(%s=*)", idAttribute);
        if (baseFilter != null && !"".equals(baseFilter)) {
            if (baseFilter.startsWith("(")) {
                return String.format("(&%s%s)", baseFilter, idFilter);
            } else {
                return String.format("(&(%s)%s)", baseFilter, idFilter);
            }
        } else {
            return idFilter;
        }
    }

    public Map<String, Field> getSchemaFieldMap() {
        return schemaFieldMap;
    }

    public void setTestServer(ContextProvider testServer) {
        this.testServer = testServer;
    }

    /**
     * SSLSocketFactory implementation that verifies all certificates.
     */
    public static class TrustingSSLSocketFactory extends SSLSocketFactory {

        private SSLSocketFactory factory;

        /**
         * Create a new SSLSocketFactory that creates a Socket regardless of the certificate used.
         */
        public TrustingSSLSocketFactory() {
            try {
                SSLContext sslContext = SSLContext.getInstance("TLS");
                sslContext.init(null, new TrustManager[] { new TrustingX509TrustManager() }, new SecureRandom());
                factory = sslContext.getSocketFactory();
            } catch (NoSuchAlgorithmException nsae) {
                throw new RuntimeException("Unable to initialize the SSL context:  ", nsae);
            } catch (KeyManagementException kme) {
                throw new RuntimeException("Unable to register a trust manager:  ", kme);
            }
        }

        /**
         * TrustingSSLSocketFactoryHolder is loaded on the first execution of TrustingSSLSocketFactory.getDefault() or
         * the first access to TrustingSSLSocketFactoryHolder.INSTANCE, not before.
         */
        private static class TrustingSSLSocketFactoryHolder {
            public static final TrustingSSLSocketFactory INSTANCE = new TrustingSSLSocketFactory();
        }

        public static SocketFactory getDefault() {
            return TrustingSSLSocketFactoryHolder.INSTANCE;
        }

        @Override
        public String[] getDefaultCipherSuites() {
            return factory.getDefaultCipherSuites();
        }

        @Override
        public String[] getSupportedCipherSuites() {
            return factory.getSupportedCipherSuites();
        }

        @Override
        public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException {
            return factory.createSocket(s, host, port, autoClose);
        }

        @Override
        public Socket createSocket(String host, int port) throws IOException, UnknownHostException {
            return factory.createSocket(host, port);
        }

        @Override
        public Socket createSocket(InetAddress host, int port) throws IOException {
            return factory.createSocket(host, port);
        }

        @Override
        public Socket createSocket(String host, int port, InetAddress localHost, int localPort)
                throws IOException, UnknownHostException {
            return factory.createSocket(host, port, localHost, localPort);
        }

        @Override
        public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort)
                throws IOException {
            return factory.createSocket(address, port, localAddress, localPort);
        }

        /**
         * Insecurely trusts everyone.
         */
        private class TrustingX509TrustManager implements X509TrustManager {

            @Override
            public void checkClientTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {
                return;
            }

            @Override
            public void checkServerTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {
                return;
            }

            @Override
            public X509Certificate[] getAcceptedIssuers() {
                return new java.security.cert.X509Certificate[0];
            }
        }

    }

}