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