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