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