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