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", 195 Integer.toString(serverConfig.getPoolingTimeout())); // 1 min by default 196 } 197 198 if (!serverConfig.isVerifyServerCert() && serverConfig.useSsl) { 199 props.put("java.naming.ldap.factory.socket", 200 "org.nuxeo.ecm.directory.ldap.LDAPDirectory$TrustingSSLSocketFactory"); 201 } 202 203 return props; 204 } 205 206 /** 207 * Sets a System property, except if it's already set, to allow for external configuration. 208 */ 209 protected void setSystemProperty(String key, String value) { 210 if (System.getProperty(key) == null) { 211 System.setProperty(key, value); 212 } 213 } 214 215 public Properties getContextProperties() { 216 return contextProperties; 217 } 218 219 /** 220 * Search controls that only fetch attributes defined by the schema 221 * 222 * @return common search controls to use for all LDAP search queries 223 */ 224 protected SearchControls computeSearchControls() throws DirectoryException { 225 LDAPDirectoryDescriptor ldapDirectoryDesc = getDescriptor(); 226 SearchControls scts = new SearchControls(); 227 // respect the scope of the configuration 228 scts.setSearchScope(ldapDirectoryDesc.getSearchScope()); 229 230 // only fetch attributes that are defined in the schema or needed to 231 // compute LDAPReferences 232 Set<String> attrs = new HashSet<>(); 233 for (String fieldName : getSchemaFieldMap().keySet()) { 234 if (!references.containsKey(fieldName)) { 235 attrs.add(fieldMapper.getBackendField(fieldName)); 236 } 237 } 238 attrs.add("objectClass"); 239 240 for (Reference reference : getReferences()) { 241 if (reference instanceof LDAPReference) { 242 LDAPReference ldapReference = (LDAPReference) reference; 243 attrs.add(ldapReference.getStaticAttributeId(fieldMapper)); 244 attrs.add(ldapReference.getDynamicAttributeId()); 245 246 // Add Dynamic Reference attributes filtering 247 for (LDAPDynamicReferenceDescriptor dynAtt : ldapReference.getDynamicAttributes()) { 248 attrs.add(dynAtt.baseDN); 249 attrs.add(dynAtt.filter); 250 } 251 252 } 253 } 254 255 if (getPasswordField() != null) { 256 // never try to fetch the password 257 attrs.remove(getPasswordField()); 258 } 259 260 scts.setReturningAttributes(attrs.toArray(new String[attrs.size()])); 261 262 scts.setCountLimit(ldapDirectoryDesc.getQuerySizeLimit()); 263 scts.setTimeLimit(ldapDirectoryDesc.getQueryTimeLimit()); 264 265 return scts; 266 } 267 268 public SearchControls getSearchControls() { 269 return getSearchControls(false); 270 } 271 272 public SearchControls getSearchControls(boolean fetchAllAttributes) { 273 if (fetchAllAttributes) { 274 // build a new ftcs instance with no attribute filtering 275 LDAPDirectoryDescriptor ldapDirectoryDesc = getDescriptor(); 276 SearchControls scts = new SearchControls(); 277 scts.setSearchScope(ldapDirectoryDesc.getSearchScope()); 278 return scts; 279 } else { 280 // return the precomputed scts instance 281 return searchControls; 282 } 283 } 284 285 protected DirContext createContext() throws DirectoryException { 286 try { 287 /* 288 * Dynamic server list requires re-computation on each access 289 */ 290 String serverName = getDescriptor().getServerName(); 291 if (StringUtils.isEmpty(serverName)) { 292 throw new DirectoryException("server configuration is missing for directory " + getName()); 293 } 294 LDAPServerDescriptor serverConfig = getServer(); 295 if (serverConfig.isDynamicServerList()) { 296 String ldapUrls = serverConfig.getLdapUrls(); 297 contextProperties.put(Context.PROVIDER_URL, ldapUrls); 298 } 299 return new InitialDirContext(contextProperties); 300 } catch (NamingException e) { 301 throw new DirectoryException("Cannot connect to LDAP directory '" + getName() + "': " + e.getMessage(), e); 302 } 303 } 304 305 /** 306 * @since 5.7 307 * @return ldap server descriptor bound to this directory 308 */ 309 public LDAPServerDescriptor getServer() { 310 return factory.getServer(getDescriptor().getServerName()); 311 } 312 313 @Override 314 public Session getSession() throws DirectoryException { 315 initLDAPConfigIfNeeded(); 316 Session session = new LDAPSession(this); 317 addSession(session); 318 return session; 319 } 320 321 public String getBaseFilter() { 322 // NXP-2461: always add control on id field in base filter 323 String idField = getIdField(); 324 String idAttribute = getFieldMapper().getBackendField(idField); 325 String idFilter = String.format("(%s=*)", idAttribute); 326 if (baseFilter != null && !"".equals(baseFilter)) { 327 if (baseFilter.startsWith("(")) { 328 return String.format("(&%s%s)", baseFilter, idFilter); 329 } else { 330 return String.format("(&(%s)%s)", baseFilter, idFilter); 331 } 332 } else { 333 return idFilter; 334 } 335 } 336 337 protected ContextProvider getTestServer() { 338 return testServer; 339 } 340 341 public void setTestServer(ContextProvider testServer) { 342 this.testServer = testServer; 343 } 344 345 /** 346 * SSLSocketFactory implementation that verifies all certificates. 347 */ 348 public static class TrustingSSLSocketFactory extends SSLSocketFactory { 349 350 private SSLSocketFactory factory; 351 352 /** 353 * Create a new SSLSocketFactory that creates a Socket regardless of the certificate used. 354 */ 355 public TrustingSSLSocketFactory() { 356 try { 357 SSLContext sslContext = SSLContext.getInstance("TLS"); 358 sslContext.init(null, new TrustManager[] { new TrustingX509TrustManager() }, new SecureRandom()); 359 factory = sslContext.getSocketFactory(); 360 } catch (NoSuchAlgorithmException nsae) { 361 throw new RuntimeException("Unable to initialize the SSL context: ", nsae); 362 } catch (KeyManagementException kme) { 363 throw new RuntimeException("Unable to register a trust manager: ", kme); 364 } 365 } 366 367 /** 368 * TrustingSSLSocketFactoryHolder is loaded on the first execution of TrustingSSLSocketFactory.getDefault() or 369 * the first access to TrustingSSLSocketFactoryHolder.INSTANCE, not before. 370 */ 371 private static class TrustingSSLSocketFactoryHolder { 372 public static final TrustingSSLSocketFactory INSTANCE = new TrustingSSLSocketFactory(); 373 } 374 375 public static SocketFactory getDefault() { 376 return TrustingSSLSocketFactoryHolder.INSTANCE; 377 } 378 379 @Override 380 public String[] getDefaultCipherSuites() { 381 return factory.getDefaultCipherSuites(); 382 } 383 384 @Override 385 public String[] getSupportedCipherSuites() { 386 return factory.getSupportedCipherSuites(); 387 } 388 389 @Override 390 public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException { 391 return factory.createSocket(s, host, port, autoClose); 392 } 393 394 @Override 395 public Socket createSocket(String host, int port) throws IOException, UnknownHostException { 396 return factory.createSocket(host, port); 397 } 398 399 @Override 400 public Socket createSocket(InetAddress host, int port) throws IOException { 401 return factory.createSocket(host, port); 402 } 403 404 @Override 405 public Socket createSocket(String host, int port, InetAddress localHost, int localPort) 406 throws IOException, UnknownHostException { 407 return factory.createSocket(host, port, localHost, localPort); 408 } 409 410 @Override 411 public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) 412 throws IOException { 413 return factory.createSocket(address, port, localAddress, localPort); 414 } 415 416 /** 417 * Insecurely trusts everyone. 418 */ 419 private class TrustingX509TrustManager implements X509TrustManager { 420 421 @Override 422 public void checkClientTrusted(X509Certificate[] arg0, String arg1) throws CertificateException { 423 return; 424 } 425 426 @Override 427 public void checkServerTrusted(X509Certificate[] arg0, String arg1) throws CertificateException { 428 return; 429 } 430 431 @Override 432 public X509Certificate[] getAcceptedIssuers() { 433 return new java.security.cert.X509Certificate[0]; 434 } 435 } 436 437 } 438 439}