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