001/*
002 * (C) Copyright 2006-2007 Nuxeo SA (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     Nuxeo - initial API and implementation
018 *
019 * $Id$
020 */
021
022package org.nuxeo.ecm.directory.ldap;
023
024import java.io.IOException;
025import java.net.InetAddress;
026import java.net.Socket;
027import java.net.UnknownHostException;
028import java.security.KeyManagementException;
029import java.security.NoSuchAlgorithmException;
030import java.security.SecureRandom;
031import java.security.cert.CertificateException;
032import java.security.cert.X509Certificate;
033import java.util.HashSet;
034import java.util.LinkedHashMap;
035import java.util.List;
036import java.util.Map;
037import java.util.Properties;
038import java.util.Set;
039
040import javax.naming.Context;
041import javax.naming.NamingException;
042import javax.naming.directory.DirContext;
043import javax.naming.directory.InitialDirContext;
044import javax.naming.directory.SearchControls;
045import javax.net.SocketFactory;
046import javax.net.ssl.SSLContext;
047import javax.net.ssl.SSLSocketFactory;
048import javax.net.ssl.TrustManager;
049import javax.net.ssl.X509TrustManager;
050
051import org.apache.commons.lang.StringUtils;
052import org.apache.commons.logging.Log;
053import org.apache.commons.logging.LogFactory;
054import org.nuxeo.ecm.core.schema.SchemaManager;
055import org.nuxeo.ecm.core.schema.types.Field;
056import org.nuxeo.ecm.core.schema.types.Schema;
057import org.nuxeo.ecm.directory.AbstractDirectory;
058import org.nuxeo.ecm.directory.DirectoryException;
059import org.nuxeo.ecm.directory.DirectoryFieldMapper;
060import org.nuxeo.ecm.directory.Reference;
061import org.nuxeo.ecm.directory.Session;
062import org.nuxeo.runtime.api.Framework;
063
064/**
065 * Implementation of the Directory interface for servers implementing the Lightweight Directory Access Protocol.
066 *
067 * @author ogrisel
068 * @author Robert Browning
069 */
070public class LDAPDirectory extends AbstractDirectory {
071
072    private static final Log log = LogFactory.getLog(LDAPDirectory.class);
073
074    // special field key to be able to read the DN of an LDAP entry
075    public static final String DN_SPECIAL_ATTRIBUTE_KEY = "dn";
076
077    protected Properties contextProperties;
078
079    protected SearchControls searchControls;
080
081    protected Map<String, Field> schemaFieldMap;
082
083    protected final LDAPDirectoryFactory factory;
084
085    protected String baseFilter;
086
087    // the following attribute is only used for testing purpose
088    protected ContextProvider testServer;
089
090    public LDAPDirectory(LDAPDirectoryDescriptor descriptor) {
091        super(descriptor);
092        if (StringUtils.isEmpty(descriptor.getSearchBaseDn())) {
093            throw new DirectoryException("searchBaseDn configuration is missing for directory " + getName());
094        }
095        factory = Framework.getService(LDAPDirectoryFactory.class);
096    }
097
098    @Override
099    public LDAPDirectoryDescriptor getDescriptor() {
100        return (LDAPDirectoryDescriptor) descriptor;
101    }
102
103    @Override
104    public List<Reference> getReferences(String referenceFieldName) {
105        if (schemaFieldMap == null) {
106            initLDAPConfig();
107        }
108        return references.get(referenceFieldName);
109    }
110
111    /**
112     * Lazy init method for ldap config
113     *
114     * @since 6.0
115     */
116    protected void initLDAPConfig() {
117        LDAPDirectoryDescriptor ldapDirectoryDesc = getDescriptor();
118        // computing attributes that will be useful for all sessions
119        SchemaManager schemaManager = Framework.getLocalService(SchemaManager.class);
120        Schema schema = schemaManager.getSchema(getSchema());
121        if (schema == null) {
122            throw new DirectoryException(getSchema() + " is not a registered schema");
123        }
124        schemaFieldMap = new LinkedHashMap<>();
125        for (Field f : schema.getFields()) {
126            schemaFieldMap.put(f.getName().getLocalName(), f);
127        }
128
129        // init field mapper before search fields
130        fieldMapper = new DirectoryFieldMapper(ldapDirectoryDesc.fieldMapping);
131        contextProperties = computeContextProperties();
132        baseFilter = ldapDirectoryDesc.getAggregatedSearchFilter();
133
134        // register the references
135        addReferences(ldapDirectoryDesc.getInverseReferences());
136        addReferences(ldapDirectoryDesc.getLdapReferences());
137
138        // register the search controls after having registered the references
139        // since the list of attributes to fetch my depend on registered
140        // LDAPReferences
141        searchControls = computeSearchControls();
142
143        // cache parameterization
144        cache.setEntryCacheName(ldapDirectoryDesc.cacheEntryName);
145        cache.setEntryCacheWithoutReferencesName(ldapDirectoryDesc.cacheEntryWithoutReferencesName);
146        cache.setNegativeCaching(ldapDirectoryDesc.negativeCaching);
147
148        log.debug(String.format("initialized LDAP directory %s with fields [%s] and references [%s]", getName(),
149                StringUtils.join(schemaFieldMap.keySet().toArray(), ", "),
150                StringUtils.join(references.keySet().toArray(), ", ")));
151    }
152
153    /**
154     * @return connection parameters to use for all LDAP queries
155     */
156    protected Properties computeContextProperties() throws DirectoryException {
157        LDAPDirectoryDescriptor ldapDirectoryDesc = getDescriptor();
158        // Initialization of LDAP connection parameters from parameters
159        // registered in the LDAP "server" extension point
160        Properties props = new Properties();
161        LDAPServerDescriptor serverConfig = getServer();
162
163        if (null == serverConfig) {
164            throw new DirectoryException("LDAP server configuration not found: " + ldapDirectoryDesc.getServerName());
165        }
166
167        props.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
168
169        /*
170         * Get initial connection URLs, dynamic URLs may cause the list to be updated when creating the session
171         */
172        String ldapUrls = serverConfig.getLdapUrls();
173        if (ldapUrls == null) {
174            throw new DirectoryException("Server LDAP URL configuration is missing for directory " + getName());
175        }
176        props.put(Context.PROVIDER_URL, ldapUrls);
177
178        // define how referrals are handled
179        if (!getDescriptor().getFollowReferrals()) {
180            props.put(Context.REFERRAL, "ignore");
181        } else {
182            // this is the default mode
183            props.put(Context.REFERRAL, "follow");
184        }
185
186        /*
187         * SSL Connections do not work with connection timeout property
188         */
189        if (serverConfig.getConnectionTimeout() > -1) {
190            if (!serverConfig.useSsl()) {
191                props.put("com.sun.jndi.ldap.connect.timeout", Integer.toString(serverConfig.getConnectionTimeout()));
192            } else {
193                log.warn("SSL connections do not operate correctly"
194                        + " when used with the connection timeout parameter, disabling timout");
195            }
196        }
197
198        String bindDn = serverConfig.getBindDn();
199        if (bindDn != null) {
200            // Authenticated connection
201            props.put(Context.SECURITY_PRINCIPAL, bindDn);
202            props.put(Context.SECURITY_CREDENTIALS, serverConfig.getBindPassword());
203        }
204
205        if (serverConfig.isPoolingEnabled()) {
206            // Enable connection pooling
207            props.put("com.sun.jndi.ldap.connect.pool", "true");
208            props.put("com.sun.jndi.ldap.connect.pool.protocol", "plain ssl");
209            props.put("com.sun.jndi.ldap.connect.pool.authentication", "none simple DIGEST-MD5");
210            props.put("com.sun.jndi.ldap.connect.pool.timeout", "1800000"); // 30
211            // min
212        }
213
214        if (!serverConfig.isVerifyServerCert() && serverConfig.useSsl) {
215            props.put("java.naming.ldap.factory.socket",
216                    "org.nuxeo.ecm.directory.ldap.LDAPDirectory$TrustingSSLSocketFactory");
217        }
218
219        return props;
220    }
221
222    public Properties getContextProperties() {
223        return contextProperties;
224    }
225
226    /**
227     * Search controls that only fetch attributes defined by the schema
228     *
229     * @return common search controls to use for all LDAP search queries
230     * @throws DirectoryException
231     */
232    protected SearchControls computeSearchControls() throws DirectoryException {
233        LDAPDirectoryDescriptor ldapDirectoryDesc = getDescriptor();
234        SearchControls scts = new SearchControls();
235        // respect the scope of the configuration
236        scts.setSearchScope(ldapDirectoryDesc.getSearchScope());
237
238        // only fetch attributes that are defined in the schema or needed to
239        // compute LDAPReferences
240        Set<String> attrs = new HashSet<>();
241        for (String fieldName : schemaFieldMap.keySet()) {
242            if (!references.containsKey(fieldName)) {
243                attrs.add(fieldMapper.getBackendField(fieldName));
244            }
245        }
246        attrs.add("objectClass");
247
248        for (Reference reference : getReferences()) {
249            if (reference instanceof LDAPReference) {
250                LDAPReference ldapReference = (LDAPReference) reference;
251                attrs.add(ldapReference.getStaticAttributeId(fieldMapper));
252                attrs.add(ldapReference.getDynamicAttributeId());
253
254                // Add Dynamic Reference attributes filtering
255                for (LDAPDynamicReferenceDescriptor dynAtt : ldapReference.getDynamicAttributes()) {
256                    attrs.add(dynAtt.baseDN);
257                    attrs.add(dynAtt.filter);
258                }
259
260            }
261        }
262
263        if (getPasswordField() != null) {
264            // never try to fetch the password
265            attrs.remove(getPasswordField());
266        }
267
268        scts.setReturningAttributes(attrs.toArray(new String[attrs.size()]));
269
270        scts.setCountLimit(ldapDirectoryDesc.getQuerySizeLimit());
271        scts.setTimeLimit(ldapDirectoryDesc.getQueryTimeLimit());
272
273        return scts;
274    }
275
276    public SearchControls getSearchControls() {
277        return getSearchControls(false);
278    }
279
280    public SearchControls getSearchControls(boolean fetchAllAttributes) {
281        if (fetchAllAttributes) {
282            // return the precomputed scts instance
283            return searchControls;
284        } else {
285            // build a new ftcs instance with no attribute filtering
286            LDAPDirectoryDescriptor ldapDirectoryDesc = getDescriptor();
287            SearchControls scts = new SearchControls();
288            scts.setSearchScope(ldapDirectoryDesc.getSearchScope());
289            scts.setReturningAttributes(new String[] { ldapDirectoryDesc.rdnAttribute,
290                    ldapDirectoryDesc.fieldMapping.get(getIdField()) });
291            return scts;
292        }
293    }
294
295    protected DirContext createContext() throws DirectoryException {
296        try {
297            /*
298             * Dynamic server list requires re-computation on each access
299             */
300            String serverName = getDescriptor().getServerName();
301            if (StringUtils.isEmpty(serverName)) {
302                throw new DirectoryException("server configuration is missing for directory " + getName());
303            }
304            LDAPServerDescriptor serverConfig = getServer();
305            if (serverConfig.isDynamicServerList()) {
306                String ldapUrls = serverConfig.getLdapUrls();
307                contextProperties.put(Context.PROVIDER_URL, ldapUrls);
308            }
309            return new InitialDirContext(contextProperties);
310        } catch (NamingException e) {
311            throw new DirectoryException("Cannot connect to LDAP directory '" + getName() + "': " + e.getMessage(), e);
312        }
313    }
314
315    /**
316     * @since 5.7
317     * @return ldap server descriptor bound to this directory
318     */
319    public LDAPServerDescriptor getServer() {
320        return factory.getServer(getDescriptor().getServerName());
321    }
322
323    @Override
324    public Session getSession() throws DirectoryException {
325        if (schemaFieldMap == null) {
326            initLDAPConfig();
327        }
328        DirContext context;
329        if (testServer != null) {
330            context = testServer.getContext();
331        } else {
332            context = createContext();
333        }
334        Session session = new LDAPSession(this, context);
335        addSession(session);
336        return session;
337    }
338
339    public String getBaseFilter() {
340        // NXP-2461: always add control on id field in base filter
341        String idField = getIdField();
342        String idAttribute = getFieldMapper().getBackendField(idField);
343        String idFilter = String.format("(%s=*)", idAttribute);
344        if (baseFilter != null && !"".equals(baseFilter)) {
345            if (baseFilter.startsWith("(")) {
346                return String.format("(&%s%s)", baseFilter, idFilter);
347            } else {
348                return String.format("(&(%s)%s)", baseFilter, idFilter);
349            }
350        } else {
351            return idFilter;
352        }
353    }
354
355    public Map<String, Field> getSchemaFieldMap() {
356        return schemaFieldMap;
357    }
358
359    public void setTestServer(ContextProvider testServer) {
360        this.testServer = testServer;
361    }
362
363    /**
364     * SSLSocketFactory implementation that verifies all certificates.
365     */
366    public static class TrustingSSLSocketFactory extends SSLSocketFactory {
367
368        private SSLSocketFactory factory;
369
370        /**
371         * Create a new SSLSocketFactory that creates a Socket regardless of the certificate used.
372         */
373        public TrustingSSLSocketFactory() {
374            try {
375                SSLContext sslContext = SSLContext.getInstance("TLS");
376                sslContext.init(null, new TrustManager[] { new TrustingX509TrustManager() }, new SecureRandom());
377                factory = sslContext.getSocketFactory();
378            } catch (NoSuchAlgorithmException nsae) {
379                throw new RuntimeException("Unable to initialize the SSL context:  ", nsae);
380            } catch (KeyManagementException kme) {
381                throw new RuntimeException("Unable to register a trust manager:  ", kme);
382            }
383        }
384
385        /**
386         * TrustingSSLSocketFactoryHolder is loaded on the first execution of TrustingSSLSocketFactory.getDefault() or
387         * the first access to TrustingSSLSocketFactoryHolder.INSTANCE, not before.
388         */
389        private static class TrustingSSLSocketFactoryHolder {
390            public static final TrustingSSLSocketFactory INSTANCE = new TrustingSSLSocketFactory();
391        }
392
393        public static SocketFactory getDefault() {
394            return TrustingSSLSocketFactoryHolder.INSTANCE;
395        }
396
397        @Override
398        public String[] getDefaultCipherSuites() {
399            return factory.getDefaultCipherSuites();
400        }
401
402        @Override
403        public String[] getSupportedCipherSuites() {
404            return factory.getSupportedCipherSuites();
405        }
406
407        @Override
408        public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException {
409            return factory.createSocket(s, host, port, autoClose);
410        }
411
412        @Override
413        public Socket createSocket(String host, int port) throws IOException, UnknownHostException {
414            return factory.createSocket(host, port);
415        }
416
417        @Override
418        public Socket createSocket(InetAddress host, int port) throws IOException {
419            return factory.createSocket(host, port);
420        }
421
422        @Override
423        public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException,
424                UnknownHostException {
425            return factory.createSocket(host, port, localHost, localPort);
426        }
427
428        @Override
429        public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort)
430                throws IOException {
431            return factory.createSocket(address, port, localAddress, localPort);
432        }
433
434        /**
435         * Insecurely trusts everyone.
436         */
437        private class TrustingX509TrustManager implements X509TrustManager {
438
439            @Override
440            public void checkClientTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {
441                return;
442            }
443
444            @Override
445            public void checkServerTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {
446                return;
447            }
448
449            @Override
450            public X509Certificate[] getAcceptedIssuers() {
451                return new java.security.cert.X509Certificate[0];
452            }
453        }
454
455    }
456
457}