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.X509Certificate; 032import java.util.ArrayList; 033import java.util.Arrays; 034import java.util.HashSet; 035import java.util.List; 036import java.util.Properties; 037import java.util.Set; 038 039import javax.naming.Context; 040import javax.naming.NamingException; 041import javax.naming.directory.DirContext; 042import javax.naming.directory.InitialDirContext; 043import javax.naming.directory.SearchControls; 044import javax.net.SocketFactory; 045import javax.net.ssl.SSLContext; 046import javax.net.ssl.SSLSocketFactory; 047import javax.net.ssl.TrustManager; 048import javax.net.ssl.X509TrustManager; 049 050import org.apache.commons.lang3.StringUtils; 051import org.apache.commons.logging.Log; 052import org.apache.commons.logging.LogFactory; 053import org.nuxeo.ecm.directory.AbstractDirectory; 054import org.nuxeo.ecm.directory.DirectoryException; 055import org.nuxeo.ecm.directory.DirectoryFieldMapper; 056import org.nuxeo.ecm.directory.Reference; 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 protected volatile SearchControls idSearchControls; 075 076 // used in double-checked locking for lazy init 077 protected volatile SearchControls searchControls; 078 079 protected final LDAPDirectoryFactory factory; 080 081 protected String baseFilter; 082 083 // the following attribute is only used for testing purpose 084 protected ContextProvider testServer; 085 086 public LDAPDirectory(LDAPDirectoryDescriptor descriptor) { 087 super(descriptor, LDAPReference.class); 088 if (StringUtils.isEmpty(descriptor.getSearchBaseDn())) { 089 throw new DirectoryException("searchBaseDn configuration is missing for directory " + getName()); 090 } 091 factory = Framework.getService(LDAPDirectoryFactory.class); 092 } 093 094 @Override 095 public LDAPDirectoryDescriptor getDescriptor() { 096 return (LDAPDirectoryDescriptor) descriptor; 097 } 098 099 @Override 100 protected void addReferences() { 101 super.addReferences(); 102 // add backward compat LDAP references 103 Reference[] refs = getDescriptor().getLdapReferences(); 104 Arrays.stream(refs).forEach(this::addReference); 105 } 106 107 @Override 108 public List<Reference> getReferences(String referenceFieldName) { 109 return references.get(referenceFieldName); 110 } 111 112 @Override 113 public void initialize() { 114 super.initialize(); 115 116 // init field mapper before search fields 117 LDAPDirectoryDescriptor ldapDirectoryDesc = getDescriptor(); 118 fieldMapper = new DirectoryFieldMapper(ldapDirectoryDesc.fieldMapping); 119 contextProperties = computeContextProperties(); 120 baseFilter = ldapDirectoryDesc.getAggregatedSearchFilter(); 121 122 // register the search controls after having registered the references 123 // since the list of attributes to fetch my depend on registered 124 // LDAPReferences 125 idSearchControls = computeIdSearchControls(); 126 searchControls = computeSearchControls(); 127 128 log.debug(String.format("initialized LDAP directory %s with fields [%s] and references [%s]", getName(), 129 StringUtils.join(getSchemaFieldMap().keySet().toArray(), ", "), 130 StringUtils.join(references.keySet().toArray(), ", "))); 131 } 132 133 /** 134 * @return connection parameters to use for all LDAP queries 135 */ 136 protected Properties computeContextProperties() { 137 LDAPDirectoryDescriptor ldapDirectoryDesc = getDescriptor(); 138 // Initialization of LDAP connection parameters from parameters 139 // registered in the LDAP "server" extension point 140 Properties props = new Properties(); 141 LDAPServerDescriptor serverConfig = getServer(); 142 143 if (null == serverConfig) { 144 throw new DirectoryException("LDAP server configuration not found: " + ldapDirectoryDesc.getServerName()); 145 } 146 147 props.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); 148 149 /* 150 * Get initial connection URLs, dynamic URLs may cause the list to be updated when creating the session 151 */ 152 String ldapUrls = serverConfig.getLdapUrls(); 153 if (ldapUrls == null) { 154 throw new DirectoryException("Server LDAP URL configuration is missing for directory " + getName()); 155 } 156 props.put(Context.PROVIDER_URL, ldapUrls); 157 158 // define how referrals are handled 159 if (!getDescriptor().getFollowReferrals()) { 160 props.put(Context.REFERRAL, "ignore"); 161 } else { 162 // this is the default mode 163 props.put(Context.REFERRAL, "follow"); 164 } 165 166 /* 167 * SSL Connections do not work with connection timeout property 168 */ 169 if (serverConfig.getConnectionTimeout() > -1) { 170 if (!serverConfig.useSsl()) { 171 props.put("com.sun.jndi.ldap.connect.timeout", Integer.toString(serverConfig.getConnectionTimeout())); 172 } else { 173 log.warn("SSL connections do not operate correctly" 174 + " when used with the connection timeout parameter, disabling timout"); 175 } 176 } 177 178 String bindDn = serverConfig.getBindDn(); 179 if (bindDn != null) { 180 // Authenticated connection 181 props.put(Context.SECURITY_PRINCIPAL, bindDn); 182 props.put(Context.SECURITY_CREDENTIALS, serverConfig.getBindPassword()); 183 } 184 185 if (serverConfig.isPoolingEnabled()) { 186 // Enable connection pooling 187 props.put("com.sun.jndi.ldap.connect.pool", "true"); 188 // the rest of the properties controlling pool configuration are system properties! 189 setSystemProperty("com.sun.jndi.ldap.connect.pool.protocol", "plain ssl"); 190 setSystemProperty("com.sun.jndi.ldap.connect.pool.authentication", "none simple DIGEST-MD5"); 191 setSystemProperty("com.sun.jndi.ldap.connect.pool.timeout", 192 Integer.toString(serverConfig.getPoolingTimeout())); // 1 min by default 193 } 194 195 if (!serverConfig.isVerifyServerCert() && serverConfig.useSsl) { 196 props.put("java.naming.ldap.factory.socket", 197 "org.nuxeo.ecm.directory.ldap.LDAPDirectory$TrustingSSLSocketFactory"); 198 } 199 200 return props; 201 } 202 203 /** 204 * Sets a System property, except if it's already set, to allow for external configuration. 205 */ 206 protected void setSystemProperty(String key, String value) { 207 if (System.getProperty(key) == null) { 208 System.setProperty(key, value); 209 } 210 } 211 212 public Properties getContextProperties() { 213 return contextProperties; 214 } 215 216 protected SearchControls computeIdSearchControls() { 217 LDAPDirectoryDescriptor desc = getDescriptor(); 218 SearchControls scts = new SearchControls(); 219 scts.setSearchScope(desc.getSearchScope()); 220 String idAttr = fieldMapper.getBackendField(getIdField()); 221 scts.setReturningAttributes(new String[] { idAttr }); 222 scts.setCountLimit(desc.getQuerySizeLimit()); 223 scts.setTimeLimit(desc.getQueryTimeLimit()); 224 return scts; 225 } 226 227 /** 228 * Search controls that only fetch attributes defined by the schema 229 * 230 * @return common search controls to use for all LDAP search queries 231 */ 232 protected SearchControls computeSearchControls() { 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 : getSchemaFieldMap().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 /** 277 * Search controls that only fetch the id. 278 * 279 * @since 10.3 280 */ 281 public SearchControls getIdSearchControls() { 282 return idSearchControls; 283 } 284 285 public SearchControls getSearchControls() { 286 return getSearchControls(false); 287 } 288 289 public SearchControls getSearchControls(boolean fetchAllAttributes) { 290 if (fetchAllAttributes) { 291 // build a new ftcs instance with no attribute filtering 292 LDAPDirectoryDescriptor ldapDirectoryDesc = getDescriptor(); 293 SearchControls scts = new SearchControls(); 294 scts.setSearchScope(ldapDirectoryDesc.getSearchScope()); 295 return scts; 296 } else { 297 // return the precomputed scts instance 298 return searchControls; 299 } 300 } 301 302 protected DirContext createContext() { 303 try { 304 /* 305 * Dynamic server list requires re-computation on each access 306 */ 307 String serverName = getDescriptor().getServerName(); 308 if (StringUtils.isEmpty(serverName)) { 309 throw new DirectoryException("server configuration is missing for directory " + getName()); 310 } 311 LDAPServerDescriptor serverConfig = getServer(); 312 if (serverConfig.isDynamicServerList()) { 313 String ldapUrls = serverConfig.getLdapUrls(); 314 contextProperties.put(Context.PROVIDER_URL, ldapUrls); 315 } 316 return new InitialDirContext(contextProperties); 317 } catch (NamingException e) { 318 throw new DirectoryException("Cannot connect to LDAP directory '" + getName() + "': " + e.getMessage(), e); 319 } 320 } 321 322 /** 323 * @since 5.7 324 * @return ldap server descriptor bound to this directory 325 */ 326 public LDAPServerDescriptor getServer() { 327 return factory.getServer(getDescriptor().getServerName()); 328 } 329 330 @Override 331 public LDAPSession getSession() { 332 LDAPSession session = new LDAPSession(this); 333 addSession(session); 334 return session; 335 } 336 337 public String getBaseFilter() { 338 // NXP-2461: always add control on id field in base filter 339 String idField = getIdField(); 340 String idAttribute = getFieldMapper().getBackendField(idField); 341 String idFilter = String.format("(%s=*)", idAttribute); 342 if (baseFilter != null && !"".equals(baseFilter)) { 343 if (baseFilter.startsWith("(")) { 344 return String.format("(&%s%s)", baseFilter, idFilter); 345 } else { 346 return String.format("(&(%s)%s)", baseFilter, idFilter); 347 } 348 } else { 349 return idFilter; 350 } 351 } 352 353 public String addBaseFilter(String filter) { 354 List<String> filters = new ArrayList<>(); 355 // NXP-2461: always add control on id field in base filter 356 String idFilter = '(' + getFieldMapper().getBackendField(getIdField()) + "=*)"; 357 filters.add(idFilter); 358 if (StringUtils.isNotBlank(baseFilter)) { 359 if (!baseFilter.startsWith("(")) { 360 baseFilter = '(' + baseFilter + ')'; 361 } 362 filters.add(baseFilter); 363 } 364 if (StringUtils.isNotBlank(filter)) { 365 filters.add(filter); 366 } 367 return "(&" + StringUtils.join(filters, "") + ')'; 368 } 369 370 protected ContextProvider getTestServer() { 371 return testServer; 372 } 373 374 public void setTestServer(ContextProvider testServer) { 375 this.testServer = testServer; 376 } 377 378 /** 379 * SSLSocketFactory implementation that verifies all certificates. 380 */ 381 public static class TrustingSSLSocketFactory extends SSLSocketFactory { 382 383 private SSLSocketFactory factory; 384 385 /** 386 * Create a new SSLSocketFactory that creates a Socket regardless of the certificate used. 387 */ 388 public TrustingSSLSocketFactory() { 389 try { 390 SSLContext sslContext = SSLContext.getDefault(); 391 sslContext.init(null, new TrustManager[] { new TrustingX509TrustManager() }, new SecureRandom()); 392 factory = sslContext.getSocketFactory(); 393 } catch (NoSuchAlgorithmException nsae) { 394 throw new RuntimeException("Unable to initialize the SSL context: ", nsae); 395 } catch (KeyManagementException kme) { 396 throw new RuntimeException("Unable to register a trust manager: ", kme); 397 } 398 } 399 400 /** 401 * TrustingSSLSocketFactoryHolder is loaded on the first execution of TrustingSSLSocketFactory.getDefault() or 402 * the first access to TrustingSSLSocketFactoryHolder.INSTANCE, not before. 403 */ 404 private static class TrustingSSLSocketFactoryHolder { 405 public static final TrustingSSLSocketFactory INSTANCE = new TrustingSSLSocketFactory(); 406 } 407 408 public static SocketFactory getDefault() { 409 return TrustingSSLSocketFactoryHolder.INSTANCE; 410 } 411 412 @Override 413 public String[] getDefaultCipherSuites() { 414 return factory.getDefaultCipherSuites(); 415 } 416 417 @Override 418 public String[] getSupportedCipherSuites() { 419 return factory.getSupportedCipherSuites(); 420 } 421 422 @Override 423 public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException { 424 return factory.createSocket(s, host, port, autoClose); 425 } 426 427 @Override 428 public Socket createSocket(String host, int port) throws IOException, UnknownHostException { 429 return factory.createSocket(host, port); 430 } 431 432 @Override 433 public Socket createSocket(InetAddress host, int port) throws IOException { 434 return factory.createSocket(host, port); 435 } 436 437 @Override 438 public Socket createSocket(String host, int port, InetAddress localHost, int localPort) 439 throws IOException, UnknownHostException { 440 return factory.createSocket(host, port, localHost, localPort); 441 } 442 443 @Override 444 public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) 445 throws IOException { 446 return factory.createSocket(address, port, localAddress, localPort); 447 } 448 449 /** 450 * Insecurely trusts everyone. 451 */ 452 private class TrustingX509TrustManager implements X509TrustManager { 453 454 @Override 455 public void checkClientTrusted(X509Certificate[] arg0, String arg1) { // NOSONAR (NXP-10253) 456 } 457 458 @Override 459 public void checkServerTrusted(X509Certificate[] arg0, String arg1) { // NOSONAR (NXP-10253) 460 } 461 462 @Override 463 public X509Certificate[] getAcceptedIssuers() { 464 return new java.security.cert.X509Certificate[0]; 465 } 466 } 467 468 } 469 470}