/*
* (C) Copyright 2006-2007 Nuxeo SA (http://nuxeo.com/) and others.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* Contributors:
* Nuxeo - initial API and implementation
*
* $Id$
*/
package org.nuxeo.ecm.directory.ldap;
import java.io.IOException;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import javax.naming.Context;
import javax.naming.NamingException;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import javax.naming.directory.SearchControls;
import javax.net.SocketFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.nuxeo.ecm.core.schema.SchemaManager;
import org.nuxeo.ecm.core.schema.types.Field;
import org.nuxeo.ecm.core.schema.types.Schema;
import org.nuxeo.ecm.directory.AbstractDirectory;
import org.nuxeo.ecm.directory.DirectoryException;
import org.nuxeo.ecm.directory.DirectoryFieldMapper;
import org.nuxeo.ecm.directory.Reference;
import org.nuxeo.ecm.directory.Session;
import org.nuxeo.runtime.api.Framework;
/**
* Implementation of the Directory interface for servers implementing the Lightweight Directory Access Protocol.
*
* @author ogrisel
* @author Robert Browning
*/
public class LDAPDirectory extends AbstractDirectory {
private static final Log log = LogFactory.getLog(LDAPDirectory.class);
// special field key to be able to read the DN of an LDAP entry
public static final String DN_SPECIAL_ATTRIBUTE_KEY = "dn";
protected Properties contextProperties;
protected SearchControls searchControls;
protected Map<String, Field> schemaFieldMap;
protected final LDAPDirectoryFactory factory;
protected String baseFilter;
// the following attribute is only used for testing purpose
protected ContextProvider testServer;
public LDAPDirectory(LDAPDirectoryDescriptor descriptor) {
super(descriptor);
if (StringUtils.isEmpty(descriptor.getSearchBaseDn())) {
throw new DirectoryException("searchBaseDn configuration is missing for directory " + getName());
}
factory = Framework.getService(LDAPDirectoryFactory.class);
}
@Override
public LDAPDirectoryDescriptor getDescriptor() {
return (LDAPDirectoryDescriptor) descriptor;
}
@Override
public List<Reference> getReferences(String referenceFieldName) {
if (schemaFieldMap == null) {
initLDAPConfig();
}
return references.get(referenceFieldName);
}
/**
* Lazy init method for ldap config
*
* @since 6.0
*/
protected void initLDAPConfig() {
LDAPDirectoryDescriptor ldapDirectoryDesc = getDescriptor();
// computing attributes that will be useful for all sessions
SchemaManager schemaManager = Framework.getLocalService(SchemaManager.class);
Schema schema = schemaManager.getSchema(getSchema());
if (schema == null) {
throw new DirectoryException(getSchema() + " is not a registered schema");
}
schemaFieldMap = new LinkedHashMap<>();
for (Field f : schema.getFields()) {
schemaFieldMap.put(f.getName().getLocalName(), f);
}
// init field mapper before search fields
fieldMapper = new DirectoryFieldMapper(ldapDirectoryDesc.fieldMapping);
contextProperties = computeContextProperties();
baseFilter = ldapDirectoryDesc.getAggregatedSearchFilter();
// register the references
addReferences(ldapDirectoryDesc.getInverseReferences());
addReferences(ldapDirectoryDesc.getLdapReferences());
// register the search controls after having registered the references
// since the list of attributes to fetch my depend on registered
// LDAPReferences
searchControls = computeSearchControls();
// cache parameterization
cache.setEntryCacheName(ldapDirectoryDesc.cacheEntryName);
cache.setEntryCacheWithoutReferencesName(ldapDirectoryDesc.cacheEntryWithoutReferencesName);
cache.setNegativeCaching(ldapDirectoryDesc.negativeCaching);
log.debug(String.format("initialized LDAP directory %s with fields [%s] and references [%s]", getName(),
StringUtils.join(schemaFieldMap.keySet().toArray(), ", "),
StringUtils.join(references.keySet().toArray(), ", ")));
}
/**
* @return connection parameters to use for all LDAP queries
*/
protected Properties computeContextProperties() throws DirectoryException {
LDAPDirectoryDescriptor ldapDirectoryDesc = getDescriptor();
// Initialization of LDAP connection parameters from parameters
// registered in the LDAP "server" extension point
Properties props = new Properties();
LDAPServerDescriptor serverConfig = getServer();
if (null == serverConfig) {
throw new DirectoryException("LDAP server configuration not found: " + ldapDirectoryDesc.getServerName());
}
props.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
/*
* Get initial connection URLs, dynamic URLs may cause the list to be updated when creating the session
*/
String ldapUrls = serverConfig.getLdapUrls();
if (ldapUrls == null) {
throw new DirectoryException("Server LDAP URL configuration is missing for directory " + getName());
}
props.put(Context.PROVIDER_URL, ldapUrls);
// define how referrals are handled
if (!getDescriptor().getFollowReferrals()) {
props.put(Context.REFERRAL, "ignore");
} else {
// this is the default mode
props.put(Context.REFERRAL, "follow");
}
/*
* SSL Connections do not work with connection timeout property
*/
if (serverConfig.getConnectionTimeout() > -1) {
if (!serverConfig.useSsl()) {
props.put("com.sun.jndi.ldap.connect.timeout", Integer.toString(serverConfig.getConnectionTimeout()));
} else {
log.warn("SSL connections do not operate correctly"
+ " when used with the connection timeout parameter, disabling timout");
}
}
String bindDn = serverConfig.getBindDn();
if (bindDn != null) {
// Authenticated connection
props.put(Context.SECURITY_PRINCIPAL, bindDn);
props.put(Context.SECURITY_CREDENTIALS, serverConfig.getBindPassword());
}
if (serverConfig.isPoolingEnabled()) {
// Enable connection pooling
props.put("com.sun.jndi.ldap.connect.pool", "true");
props.put("com.sun.jndi.ldap.connect.pool.protocol", "plain ssl");
props.put("com.sun.jndi.ldap.connect.pool.authentication", "none simple DIGEST-MD5");
props.put("com.sun.jndi.ldap.connect.pool.timeout", "1800000"); // 30
// min
}
if (!serverConfig.isVerifyServerCert() && serverConfig.useSsl) {
props.put("java.naming.ldap.factory.socket",
"org.nuxeo.ecm.directory.ldap.LDAPDirectory$TrustingSSLSocketFactory");
}
return props;
}
public Properties getContextProperties() {
return contextProperties;
}
/**
* Search controls that only fetch attributes defined by the schema
*
* @return common search controls to use for all LDAP search queries
* @throws DirectoryException
*/
protected SearchControls computeSearchControls() throws DirectoryException {
LDAPDirectoryDescriptor ldapDirectoryDesc = getDescriptor();
SearchControls scts = new SearchControls();
// respect the scope of the configuration
scts.setSearchScope(ldapDirectoryDesc.getSearchScope());
// only fetch attributes that are defined in the schema or needed to
// compute LDAPReferences
Set<String> attrs = new HashSet<>();
for (String fieldName : schemaFieldMap.keySet()) {
if (!references.containsKey(fieldName)) {
attrs.add(fieldMapper.getBackendField(fieldName));
}
}
attrs.add("objectClass");
for (Reference reference : getReferences()) {
if (reference instanceof LDAPReference) {
LDAPReference ldapReference = (LDAPReference) reference;
attrs.add(ldapReference.getStaticAttributeId(fieldMapper));
attrs.add(ldapReference.getDynamicAttributeId());
// Add Dynamic Reference attributes filtering
for (LDAPDynamicReferenceDescriptor dynAtt : ldapReference.getDynamicAttributes()) {
attrs.add(dynAtt.baseDN);
attrs.add(dynAtt.filter);
}
}
}
if (getPasswordField() != null) {
// never try to fetch the password
attrs.remove(getPasswordField());
}
scts.setReturningAttributes(attrs.toArray(new String[attrs.size()]));
scts.setCountLimit(ldapDirectoryDesc.getQuerySizeLimit());
scts.setTimeLimit(ldapDirectoryDesc.getQueryTimeLimit());
return scts;
}
public SearchControls getSearchControls() {
return getSearchControls(false);
}
public SearchControls getSearchControls(boolean fetchAllAttributes) {
if (fetchAllAttributes) {
// return the precomputed scts instance
return searchControls;
} else {
// build a new ftcs instance with no attribute filtering
LDAPDirectoryDescriptor ldapDirectoryDesc = getDescriptor();
SearchControls scts = new SearchControls();
scts.setSearchScope(ldapDirectoryDesc.getSearchScope());
scts.setReturningAttributes(new String[] { ldapDirectoryDesc.rdnAttribute,
ldapDirectoryDesc.fieldMapping.get(getIdField()) });
return scts;
}
}
protected DirContext createContext() throws DirectoryException {
try {
/*
* Dynamic server list requires re-computation on each access
*/
String serverName = getDescriptor().getServerName();
if (StringUtils.isEmpty(serverName)) {
throw new DirectoryException("server configuration is missing for directory " + getName());
}
LDAPServerDescriptor serverConfig = getServer();
if (serverConfig.isDynamicServerList()) {
String ldapUrls = serverConfig.getLdapUrls();
contextProperties.put(Context.PROVIDER_URL, ldapUrls);
}
return new InitialDirContext(contextProperties);
} catch (NamingException e) {
throw new DirectoryException("Cannot connect to LDAP directory '" + getName() + "': " + e.getMessage(), e);
}
}
/**
* @since 5.7
* @return ldap server descriptor bound to this directory
*/
public LDAPServerDescriptor getServer() {
return factory.getServer(getDescriptor().getServerName());
}
@Override
public Session getSession() throws DirectoryException {
if (schemaFieldMap == null) {
initLDAPConfig();
}
DirContext context;
if (testServer != null) {
context = testServer.getContext();
} else {
context = createContext();
}
Session session = new LDAPSession(this, context);
addSession(session);
return session;
}
public String getBaseFilter() {
// NXP-2461: always add control on id field in base filter
String idField = getIdField();
String idAttribute = getFieldMapper().getBackendField(idField);
String idFilter = String.format("(%s=*)", idAttribute);
if (baseFilter != null && !"".equals(baseFilter)) {
if (baseFilter.startsWith("(")) {
return String.format("(&%s%s)", baseFilter, idFilter);
} else {
return String.format("(&(%s)%s)", baseFilter, idFilter);
}
} else {
return idFilter;
}
}
public Map<String, Field> getSchemaFieldMap() {
return schemaFieldMap;
}
public void setTestServer(ContextProvider testServer) {
this.testServer = testServer;
}
/**
* SSLSocketFactory implementation that verifies all certificates.
*/
public static class TrustingSSLSocketFactory extends SSLSocketFactory {
private SSLSocketFactory factory;
/**
* Create a new SSLSocketFactory that creates a Socket regardless of the certificate used.
*/
public TrustingSSLSocketFactory() {
try {
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, new TrustManager[] { new TrustingX509TrustManager() }, new SecureRandom());
factory = sslContext.getSocketFactory();
} catch (NoSuchAlgorithmException nsae) {
throw new RuntimeException("Unable to initialize the SSL context: ", nsae);
} catch (KeyManagementException kme) {
throw new RuntimeException("Unable to register a trust manager: ", kme);
}
}
/**
* TrustingSSLSocketFactoryHolder is loaded on the first execution of TrustingSSLSocketFactory.getDefault() or
* the first access to TrustingSSLSocketFactoryHolder.INSTANCE, not before.
*/
private static class TrustingSSLSocketFactoryHolder {
public static final TrustingSSLSocketFactory INSTANCE = new TrustingSSLSocketFactory();
}
public static SocketFactory getDefault() {
return TrustingSSLSocketFactoryHolder.INSTANCE;
}
@Override
public String[] getDefaultCipherSuites() {
return factory.getDefaultCipherSuites();
}
@Override
public String[] getSupportedCipherSuites() {
return factory.getSupportedCipherSuites();
}
@Override
public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException {
return factory.createSocket(s, host, port, autoClose);
}
@Override
public Socket createSocket(String host, int port) throws IOException, UnknownHostException {
return factory.createSocket(host, port);
}
@Override
public Socket createSocket(InetAddress host, int port) throws IOException {
return factory.createSocket(host, port);
}
@Override
public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException,
UnknownHostException {
return factory.createSocket(host, port, localHost, localPort);
}
@Override
public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort)
throws IOException {
return factory.createSocket(address, port, localAddress, localPort);
}
/**
* Insecurely trusts everyone.
*/
private class TrustingX509TrustManager implements X509TrustManager {
@Override
public void checkClientTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {
return;
}
@Override
public void checkServerTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {
return;
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return new java.security.cert.X509Certificate[0];
}
}
}
}