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.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.lang.StringUtils; 051import org.apache.commons.logging.Log; 052import org.apache.commons.logging.LogFactory; 053import org.nuxeo.ecm.core.schema.SchemaManager; 054import org.nuxeo.ecm.core.schema.types.Field; 055import org.nuxeo.ecm.core.schema.types.Schema; 056import org.nuxeo.ecm.directory.AbstractDirectory; 057import org.nuxeo.ecm.directory.DirectoryException; 058import org.nuxeo.ecm.directory.DirectoryFieldMapper; 059import org.nuxeo.ecm.directory.Reference; 060import org.nuxeo.ecm.directory.Session; 061import org.nuxeo.runtime.api.Framework; 062 063/** 064 * Implementation of the Directory interface for servers implementing the Lightweight Directory Access Protocol. 065 * 066 * @author ogrisel 067 * @author Robert Browning 068 */ 069public class LDAPDirectory extends AbstractDirectory { 070 071 private static final Log log = LogFactory.getLog(LDAPDirectory.class); 072 073 // special field key to be able to read the DN of an LDAP entry 074 public static final String DN_SPECIAL_ATTRIBUTE_KEY = "dn"; 075 076 protected Properties contextProperties; 077 078 protected SearchControls searchControls; 079 080 protected final LDAPDirectoryFactory factory; 081 082 protected String baseFilter; 083 084 // the following attribute is only used for testing purpose 085 protected ContextProvider testServer; 086 087 public LDAPDirectory(LDAPDirectoryDescriptor descriptor) { 088 super(descriptor, LDAPReference.class); 089 if (StringUtils.isEmpty(descriptor.getSearchBaseDn())) { 090 throw new DirectoryException("searchBaseDn configuration is missing for directory " + getName()); 091 } 092 factory = Framework.getService(LDAPDirectoryFactory.class); 093 } 094 095 @Override 096 public LDAPDirectoryDescriptor getDescriptor() { 097 return (LDAPDirectoryDescriptor) descriptor; 098 } 099 100 @Override 101 public List<Reference> getReferences(String referenceFieldName) { 102 if (schemaFieldMap == null) { 103 initLDAPConfig(); 104 } 105 return references.get(referenceFieldName); 106 } 107 108 /** 109 * Lazy init method for ldap config 110 * 111 * @since 6.0 112 */ 113 protected void initLDAPConfig() { 114 LDAPDirectoryDescriptor ldapDirectoryDesc = getDescriptor(); 115 // computing attributes that will be useful for all sessions 116 SchemaManager schemaManager = Framework.getLocalService(SchemaManager.class); 117 Schema schema = schemaManager.getSchema(getSchema()); 118 if (schema == null) { 119 throw new DirectoryException(getSchema() + " is not a registered schema"); 120 } 121 schemaFieldMap = new LinkedHashMap<>(); 122 for (Field f : schema.getFields()) { 123 schemaFieldMap.put(f.getName().getLocalName(), f); 124 } 125 126 // init field mapper before search fields 127 fieldMapper = new DirectoryFieldMapper(ldapDirectoryDesc.fieldMapping); 128 contextProperties = computeContextProperties(); 129 baseFilter = ldapDirectoryDesc.getAggregatedSearchFilter(); 130 131 // register the references 132 addReferences(ldapDirectoryDesc.getLdapReferences()); 133 134 // register the search controls after having registered the references 135 // since the list of attributes to fetch my depend on registered 136 // LDAPReferences 137 searchControls = computeSearchControls(); 138 139 log.debug(String.format("initialized LDAP directory %s with fields [%s] and references [%s]", getName(), 140 StringUtils.join(schemaFieldMap.keySet().toArray(), ", "), 141 StringUtils.join(references.keySet().toArray(), ", "))); 142 } 143 144 /** 145 * @return connection parameters to use for all LDAP queries 146 */ 147 protected Properties computeContextProperties() throws DirectoryException { 148 LDAPDirectoryDescriptor ldapDirectoryDesc = getDescriptor(); 149 // Initialization of LDAP connection parameters from parameters 150 // registered in the LDAP "server" extension point 151 Properties props = new Properties(); 152 LDAPServerDescriptor serverConfig = getServer(); 153 154 if (null == serverConfig) { 155 throw new DirectoryException("LDAP server configuration not found: " + ldapDirectoryDesc.getServerName()); 156 } 157 158 props.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); 159 160 /* 161 * Get initial connection URLs, dynamic URLs may cause the list to be updated when creating the session 162 */ 163 String ldapUrls = serverConfig.getLdapUrls(); 164 if (ldapUrls == null) { 165 throw new DirectoryException("Server LDAP URL configuration is missing for directory " + getName()); 166 } 167 props.put(Context.PROVIDER_URL, ldapUrls); 168 169 // define how referrals are handled 170 if (!getDescriptor().getFollowReferrals()) { 171 props.put(Context.REFERRAL, "ignore"); 172 } else { 173 // this is the default mode 174 props.put(Context.REFERRAL, "follow"); 175 } 176 177 /* 178 * SSL Connections do not work with connection timeout property 179 */ 180 if (serverConfig.getConnectionTimeout() > -1) { 181 if (!serverConfig.useSsl()) { 182 props.put("com.sun.jndi.ldap.connect.timeout", Integer.toString(serverConfig.getConnectionTimeout())); 183 } else { 184 log.warn("SSL connections do not operate correctly" 185 + " when used with the connection timeout parameter, disabling timout"); 186 } 187 } 188 189 String bindDn = serverConfig.getBindDn(); 190 if (bindDn != null) { 191 // Authenticated connection 192 props.put(Context.SECURITY_PRINCIPAL, bindDn); 193 props.put(Context.SECURITY_CREDENTIALS, serverConfig.getBindPassword()); 194 } 195 196 if (serverConfig.isPoolingEnabled()) { 197 // Enable connection pooling 198 props.put("com.sun.jndi.ldap.connect.pool", "true"); 199 props.put("com.sun.jndi.ldap.connect.pool.protocol", "plain ssl"); 200 props.put("com.sun.jndi.ldap.connect.pool.authentication", "none simple DIGEST-MD5"); 201 props.put("com.sun.jndi.ldap.connect.pool.timeout", "1800000"); // 30 202 // min 203 } 204 205 if (!serverConfig.isVerifyServerCert() && serverConfig.useSsl) { 206 props.put("java.naming.ldap.factory.socket", 207 "org.nuxeo.ecm.directory.ldap.LDAPDirectory$TrustingSSLSocketFactory"); 208 } 209 210 return props; 211 } 212 213 public Properties getContextProperties() { 214 return contextProperties; 215 } 216 217 /** 218 * Search controls that only fetch attributes defined by the schema 219 * 220 * @return common search controls to use for all LDAP search queries 221 * @throws DirectoryException 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 : schemaFieldMap.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 if (schemaFieldMap == null) { 315 initLDAPConfig(); 316 } 317 DirContext context; 318 if (testServer != null) { 319 context = testServer.getContext(); 320 } else { 321 context = createContext(); 322 } 323 Session session = new LDAPSession(this, context); 324 addSession(session); 325 return session; 326 } 327 328 public String getBaseFilter() { 329 // NXP-2461: always add control on id field in base filter 330 String idField = getIdField(); 331 String idAttribute = getFieldMapper().getBackendField(idField); 332 String idFilter = String.format("(%s=*)", idAttribute); 333 if (baseFilter != null && !"".equals(baseFilter)) { 334 if (baseFilter.startsWith("(")) { 335 return String.format("(&%s%s)", baseFilter, idFilter); 336 } else { 337 return String.format("(&(%s)%s)", baseFilter, idFilter); 338 } 339 } else { 340 return idFilter; 341 } 342 } 343 344 public void setTestServer(ContextProvider testServer) { 345 this.testServer = testServer; 346 } 347 348 /** 349 * SSLSocketFactory implementation that verifies all certificates. 350 */ 351 public static class TrustingSSLSocketFactory extends SSLSocketFactory { 352 353 private SSLSocketFactory factory; 354 355 /** 356 * Create a new SSLSocketFactory that creates a Socket regardless of the certificate used. 357 */ 358 public TrustingSSLSocketFactory() { 359 try { 360 SSLContext sslContext = SSLContext.getInstance("TLS"); 361 sslContext.init(null, new TrustManager[] { new TrustingX509TrustManager() }, new SecureRandom()); 362 factory = sslContext.getSocketFactory(); 363 } catch (NoSuchAlgorithmException nsae) { 364 throw new RuntimeException("Unable to initialize the SSL context: ", nsae); 365 } catch (KeyManagementException kme) { 366 throw new RuntimeException("Unable to register a trust manager: ", kme); 367 } 368 } 369 370 /** 371 * TrustingSSLSocketFactoryHolder is loaded on the first execution of TrustingSSLSocketFactory.getDefault() or 372 * the first access to TrustingSSLSocketFactoryHolder.INSTANCE, not before. 373 */ 374 private static class TrustingSSLSocketFactoryHolder { 375 public static final TrustingSSLSocketFactory INSTANCE = new TrustingSSLSocketFactory(); 376 } 377 378 public static SocketFactory getDefault() { 379 return TrustingSSLSocketFactoryHolder.INSTANCE; 380 } 381 382 @Override 383 public String[] getDefaultCipherSuites() { 384 return factory.getDefaultCipherSuites(); 385 } 386 387 @Override 388 public String[] getSupportedCipherSuites() { 389 return factory.getSupportedCipherSuites(); 390 } 391 392 @Override 393 public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException { 394 return factory.createSocket(s, host, port, autoClose); 395 } 396 397 @Override 398 public Socket createSocket(String host, int port) throws IOException, UnknownHostException { 399 return factory.createSocket(host, port); 400 } 401 402 @Override 403 public Socket createSocket(InetAddress host, int port) throws IOException { 404 return factory.createSocket(host, port); 405 } 406 407 @Override 408 public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException, 409 UnknownHostException { 410 return factory.createSocket(host, port, localHost, localPort); 411 } 412 413 @Override 414 public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) 415 throws IOException { 416 return factory.createSocket(address, port, localAddress, localPort); 417 } 418 419 /** 420 * Insecurely trusts everyone. 421 */ 422 private class TrustingX509TrustManager implements X509TrustManager { 423 424 @Override 425 public void checkClientTrusted(X509Certificate[] arg0, String arg1) throws CertificateException { 426 return; 427 } 428 429 @Override 430 public void checkServerTrusted(X509Certificate[] arg0, String arg1) throws CertificateException { 431 return; 432 } 433 434 @Override 435 public X509Certificate[] getAcceptedIssuers() { 436 return new java.security.cert.X509Certificate[0]; 437 } 438 } 439 440 } 441 442}