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