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 152 log.debug(String.format("initialized LDAP directory %s with fields [%s] and references [%s]", config.getName(), 153 StringUtils.join(schemaFieldMap.keySet().toArray(), ", "), 154 StringUtils.join(references.keySet().toArray(), ", "))); 155 } 156 157 /** 158 * @return connection parameters to use for all LDAP queries 159 */ 160 protected Properties computeContextProperties() throws DirectoryException { 161 // Initialization of LDAP connection parameters from parameters 162 // registered in the LDAP "server" extension point 163 Properties props = new Properties(); 164 LDAPServerDescriptor serverConfig = getServer(); 165 166 if (null == serverConfig) { 167 throw new DirectoryException("LDAP server configuration not found: " + config.getServerName()); 168 } 169 170 props.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); 171 172 /* 173 * Get initial connection URLs, dynamic URLs may cause the list to be updated when creating the session 174 */ 175 String ldapUrls = serverConfig.getLdapUrls(); 176 if (serverConfig.getLdapUrls() == null || config.getSchemaName().equals("")) { 177 throw new DirectoryException("Server LDAP URL configuration is missing for directory " + config.getName()); 178 } 179 props.put(Context.PROVIDER_URL, ldapUrls); 180 181 // define how referrals are handled 182 if (!getConfig().followReferrals) { 183 props.put(Context.REFERRAL, "ignore"); 184 } else { 185 // this is the default mode 186 props.put(Context.REFERRAL, "follow"); 187 } 188 189 /* 190 * SSL Connections do not work with connection timeout property 191 */ 192 if (serverConfig.getConnectionTimeout() > -1) { 193 if (!serverConfig.useSsl()) { 194 props.put("com.sun.jndi.ldap.connect.timeout", Integer.toString(serverConfig.getConnectionTimeout())); 195 } else { 196 log.warn("SSL connections do not operate correctly" 197 + " when used with the connection timeout parameter, disabling timout"); 198 } 199 } 200 201 String bindDn = serverConfig.getBindDn(); 202 if (bindDn != null) { 203 // Authenticated connection 204 props.put(Context.SECURITY_PRINCIPAL, bindDn); 205 props.put(Context.SECURITY_CREDENTIALS, serverConfig.getBindPassword()); 206 } 207 208 if (serverConfig.isPoolingEnabled()) { 209 // Enable connection pooling 210 props.put("com.sun.jndi.ldap.connect.pool", "true"); 211 props.put("com.sun.jndi.ldap.connect.pool.protocol", "plain ssl"); 212 props.put("com.sun.jndi.ldap.connect.pool.authentication", "none simple DIGEST-MD5"); 213 props.put("com.sun.jndi.ldap.connect.pool.timeout", "1800000"); // 30 214 // min 215 } 216 217 if (!serverConfig.isVerifyServerCert() && serverConfig.useSsl) { 218 props.put("java.naming.ldap.factory.socket", 219 "org.nuxeo.ecm.directory.ldap.LDAPDirectory$TrustingSSLSocketFactory"); 220 } 221 222 return props; 223 } 224 225 public Properties getContextProperties() { 226 return contextProperties; 227 } 228 229 /** 230 * Search controls that only fetch attributes defined by the schema 231 * 232 * @return common search controls to use for all LDAP search queries 233 * @throws DirectoryException 234 */ 235 protected SearchControls computeSearchControls() throws DirectoryException { 236 SearchControls scts = new SearchControls(); 237 // respect the scope of the configuration 238 scts.setSearchScope(config.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 (config.getPasswordField() != null) { 266 // never try to fetch the password 267 attrs.remove(config.getPasswordField()); 268 } 269 270 scts.setReturningAttributes(attrs.toArray(new String[attrs.size()])); 271 272 scts.setCountLimit(config.getQuerySizeLimit()); 273 scts.setTimeLimit(config.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 // build a new ftcs instance with no attribute filtering 285 SearchControls scts = new SearchControls(); 286 scts.setSearchScope(config.getSearchScope()); 287 return scts; 288 } else { 289 // return the precomputed scts instance 290 return searchControls; 291 } 292 } 293 294 protected DirContext createContext() throws DirectoryException { 295 try { 296 /* 297 * Dynamic server list requires re-computation on each access 298 */ 299 String serverName = config.getServerName(); 300 if (serverName == null || serverName.equals("")) { 301 throw new DirectoryException("server configuration is missing for directory " + config.getName()); 302 } 303 LDAPServerDescriptor serverConfig = getServer(); 304 if (serverConfig.isDynamicServerList()) { 305 String ldapUrls = serverConfig.getLdapUrls(); 306 contextProperties.put(Context.PROVIDER_URL, ldapUrls); 307 } 308 return new InitialDirContext(contextProperties); 309 } catch (NamingException e) { 310 throw new DirectoryException("Cannot connect to LDAP directory '" + getName() + "': " + e.getMessage(), e); 311 } 312 } 313 314 @Override 315 public String getName() { 316 return config.getName(); 317 } 318 319 @Override 320 public String getSchema() { 321 return config.getSchemaName(); 322 } 323 324 @Override 325 public String getParentDirectory() { 326 return null; // no parent directories are specified for LDAP 327 } 328 329 @Override 330 public String getIdField() { 331 return config.getIdField(); 332 } 333 334 @Override 335 public String getPasswordField() { 336 return config.getPasswordField(); 337 } 338 339 /** 340 * @since 5.7 341 * @return ldap server descriptor bound to this directory 342 */ 343 public LDAPServerDescriptor getServer() { 344 return factory.getServer(config.getServerName()); 345 } 346 347 @Override 348 public Session getSession() throws DirectoryException { 349 if (schemaFieldMap == null) { 350 initLDAPConfig(); 351 } 352 DirContext context; 353 if (testServer != null) { 354 context = testServer.getContext(); 355 } else { 356 context = createContext(); 357 } 358 Session session = new LDAPSession(this, context); 359 addSession(session); 360 return session; 361 } 362 363 public String getBaseFilter() { 364 // NXP-2461: always add control on id field in base filter 365 String idField = getIdField(); 366 DirectoryFieldMapper fieldMapper = getFieldMapper(); 367 String idAttribute = fieldMapper.getBackendField(idField); 368 String idFilter = String.format("(%s=*)", idAttribute); 369 if (baseFilter != null && !"".equals(baseFilter)) { 370 if (baseFilter.startsWith("(")) { 371 return String.format("(&%s%s)", baseFilter, idFilter); 372 } else { 373 return String.format("(&(%s)%s)", baseFilter, idFilter); 374 } 375 } else { 376 return idFilter; 377 } 378 } 379 380 public LDAPDirectoryDescriptor getConfig() { 381 return config; 382 } 383 384 public Map<String, Field> getSchemaFieldMap() { 385 return schemaFieldMap; 386 } 387 388 /** 389 * Get the value of the field passwordHashAlgorithm 390 * 391 * @return The value 392 * @since 5.9.3 393 */ 394 public String getPasswordHashAlgorithmField() { 395 return config.getPasswordHashAlgorithmField(); 396 } 397 398 public void setTestServer(ContextProvider testServer) { 399 this.testServer = testServer; 400 } 401 402 /** 403 * SSLSocketFactory implementation that verifies all certificates. 404 */ 405 public static class TrustingSSLSocketFactory extends SSLSocketFactory { 406 407 private SSLSocketFactory factory; 408 409 /** 410 * Create a new SSLSocketFactory that creates a Socket regardless of the certificate used. 411 * 412 * @throws SSLException if initialization fails. 413 */ 414 public TrustingSSLSocketFactory() { 415 try { 416 SSLContext sslContext = SSLContext.getInstance("TLS"); 417 sslContext.init(null, new TrustManager[] { new TrustingX509TrustManager() }, new SecureRandom()); 418 factory = sslContext.getSocketFactory(); 419 } catch (NoSuchAlgorithmException nsae) { 420 throw new RuntimeException("Unable to initialize the SSL context: ", nsae); 421 } catch (KeyManagementException kme) { 422 throw new RuntimeException("Unable to register a trust manager: ", kme); 423 } 424 } 425 426 /** 427 * TrustingSSLSocketFactoryHolder is loaded on the first execution of TrustingSSLSocketFactory.getDefault() or 428 * the first access to TrustingSSLSocketFactoryHolder.INSTANCE, not before. 429 */ 430 private static class TrustingSSLSocketFactoryHolder { 431 public static final TrustingSSLSocketFactory INSTANCE = new TrustingSSLSocketFactory(); 432 } 433 434 public static SocketFactory getDefault() { 435 return TrustingSSLSocketFactoryHolder.INSTANCE; 436 } 437 438 @Override 439 public String[] getDefaultCipherSuites() { 440 return factory.getDefaultCipherSuites(); 441 } 442 443 @Override 444 public String[] getSupportedCipherSuites() { 445 return factory.getSupportedCipherSuites(); 446 } 447 448 @Override 449 public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException { 450 return factory.createSocket(s, host, port, autoClose); 451 } 452 453 @Override 454 public Socket createSocket(String host, int port) throws IOException, UnknownHostException { 455 return factory.createSocket(host, port); 456 } 457 458 @Override 459 public Socket createSocket(InetAddress host, int port) throws IOException { 460 return factory.createSocket(host, port); 461 } 462 463 @Override 464 public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException, 465 UnknownHostException { 466 return factory.createSocket(host, port, localHost, localPort); 467 } 468 469 @Override 470 public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) 471 throws IOException { 472 return factory.createSocket(address, port, localAddress, localPort); 473 } 474 475 /** 476 * Insecurely trusts everyone. 477 */ 478 private class TrustingX509TrustManager implements X509TrustManager { 479 480 @Override 481 public void checkClientTrusted(X509Certificate[] arg0, String arg1) throws CertificateException { 482 return; 483 } 484 485 @Override 486 public void checkServerTrusted(X509Certificate[] arg0, String arg1) throws CertificateException { 487 return; 488 } 489 490 @Override 491 public X509Certificate[] getAcceptedIssuers() { 492 return new java.security.cert.X509Certificate[0]; 493 } 494 } 495 496 } 497 498}