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}