/*
* (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
*
*/
package org.nuxeo.ecm.directory.ldap;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import javax.naming.InvalidNameException;
import javax.naming.NamingException;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.directory.shared.ldap.name.LdapDN;
import org.nuxeo.common.xmap.annotation.XNode;
import org.nuxeo.common.xmap.annotation.XNodeList;
import org.nuxeo.common.xmap.annotation.XObject;
import org.nuxeo.ecm.directory.DirectoryException;
import org.nuxeo.ecm.directory.ldap.dns.DNSServiceEntry;
import org.nuxeo.ecm.directory.ldap.dns.DNSServiceResolver;
import org.nuxeo.ecm.directory.ldap.dns.DNSServiceResolverImpl;
import com.sun.jndi.ldap.LdapURL;
@XObject(value = "server")
public class LDAPServerDescriptor {
public static final Log log = LogFactory.getLog(LDAPServerDescriptor.class);
protected static final String LDAPS_SCHEME = "ldaps";
protected static final String LDAP_SCHEME = "ldap";
@XNode("@name")
public String name;
public String ldapUrls;
public String bindDn;
@XNode("connectionTimeout")
public int connectionTimeout = 10000; // timeout after 10 seconds
@XNode("poolingEnabled")
public boolean poolingEnabled = true;
@XNode("verifyServerCert")
public boolean verifyServerCert = true;
/**
* @since 5.7
*/
@XNode("retries")
public int retries = 5;
protected LinkedHashSet<LdapEntry> ldapEntries;
protected boolean isDynamicServerList = false;
protected boolean useSsl = false;
protected final DNSServiceResolver srvResolver = DNSServiceResolverImpl.getInstance();
public boolean isDynamicServerList() {
return isDynamicServerList;
}
public String getName() {
return name;
}
public String bindPassword = "";
@XNode("bindDn")
public void setBindDn(String bindDn) {
if (null != bindDn && bindDn.trim().equals("")) {
// empty bindDn means anonymous authentication
this.bindDn = null;
} else {
this.bindDn = bindDn;
}
}
public String getBindDn() {
return bindDn;
}
@XNode("bindPassword")
public void setBindPassword(String bindPassword) {
if (bindPassword == null) {
// no password means empty pasword
this.bindPassword = "";
} else {
this.bindPassword = bindPassword;
}
}
public String getBindPassword() {
return bindPassword;
}
public String getLdapUrls() {
if (ldapUrls != null) {
return ldapUrls;
}
// Leverage JNDI support for clustered servers by concatenating
// all the provided URLs for fail-over
StringBuilder calculatedLdapUrls = new StringBuilder();
for (LdapEntry entry : ldapEntries) {
calculatedLdapUrls.append(entry);
calculatedLdapUrls.append(' ');
}
/*
* If the configuration does not contain any domain entries then cache the urls, domain entries should always be
* re-queried however as the LDAP server list should change dynamically
*/
if (!isDynamicServerList) {
return ldapUrls = calculatedLdapUrls.toString().trim();
}
return calculatedLdapUrls.toString().trim();
}
@XNodeList(value = "ldapUrl", componentType = LDAPUrlDescriptor.class, type = LDAPUrlDescriptor[].class)
public void setLdapUrls(LDAPUrlDescriptor[] ldapUrls) throws DirectoryException {
if (ldapUrls == null) {
throw new DirectoryException("At least one <ldapUrl/> server declaration is required");
}
ldapEntries = new LinkedHashSet<LdapEntry>();
Set<LDAPUrlDescriptor> processed = new HashSet<LDAPUrlDescriptor>();
List<String> urls = new ArrayList<String>(ldapUrls.length);
for (LDAPUrlDescriptor url : ldapUrls) {
LdapURL ldapUrl;
try {
/*
* Empty string translates to ldap://localhost:389 through JNDI
*/
if (StringUtils.isEmpty(url.getValue())) {
urls.add(url.getValue());
ldapEntries.add(new LdapEntryDescriptor(url));
continue;
}
/*
* Parse the URI to make sure it is valid
*/
ldapUrl = new LdapURL(url.getValue());
if (!processed.add(url)) {
continue;
}
} catch (NamingException e) {
throw new DirectoryException(e);
}
useSsl = useSsl || ldapUrl.useSsl();
/*
* RFC-2255 - The "ldap" prefix indicates an entry or entries residing in the LDAP server running on the
* given hostname at the given port number. The default LDAP port is TCP port 389. If no hostport is given,
* the client must have some apriori knowledge of an appropriate LDAP server to contact.
*/
if (ldapUrl.getHost() == null) {
/*
* RFC-2782 - Check to see if an LDAP SRV record is defined in the DNS server
*/
String domain = convertDNtoFQDN(ldapUrl.getDN());
if (domain != null) {
/*
* Dynamic URL - retrieve from SRV record
*/
List<String> discoveredUrls;
try {
discoveredUrls = discoverLdapServers(domain, ldapUrl.useSsl(), url.getSrvPrefix());
} catch (NamingException e) {
throw new DirectoryException(String.format("SRV record DNS lookup failed for %s.%s: %s",
url.getSrvPrefix(), domain, e.getMessage()), e);
}
/*
* Discovered URLs could be empty, lets check at the end though
*/
urls.addAll(discoveredUrls);
/*
* Store entries in an ordered set and remember that we were dynamic
*/
ldapEntries.add(new LdapEntryDomain(url, domain, ldapUrl.useSsl()));
isDynamicServerList = true;
} else {
throw new DirectoryException("Invalid LDAP SRV reference, this should be of the form"
+ " ldap:///dc=example,dc=org");
}
} else {
/*
* Static URL - store the value
*/
urls.add(url.getValue());
/*
* Store entries in an ordered set
*/
ldapEntries.add(new LdapEntryDescriptor(url));
}
}
/*
* Oops no valid URLs to connect to :(
*/
if (urls.isEmpty()) {
throw new DirectoryException("No valid server urls returned from DNS query");
}
}
/**
* Whether this server descriptor defines a secure ldap connection
*/
public boolean useSsl() {
return useSsl;
}
/**
* Retrieve server URLs from DNS SRV record
*
* @param domain The domain to query
* @param useSsl Whether the connection to this domain should be secure
* @return List of servers or empty list
* @throws NamingException if DNS lookup fails
*/
protected List<String> discoverLdapServers(String domain, boolean useSsl, String srvPrefix) throws NamingException {
List<String> result = new ArrayList<String>();
List<DNSServiceEntry> servers = getSRVResolver().resolveLDAPDomainServers(domain, srvPrefix);
for (DNSServiceEntry serviceEntry : servers) {
/*
* Rebuild the URL
*/
StringBuilder realUrl = (useSsl) ? new StringBuilder(LDAPS_SCHEME + "://") : new StringBuilder(LDAP_SCHEME
+ "://");
realUrl.append(serviceEntry);
result.add(realUrl.toString());
}
return result;
}
/**
* Convert domain from the ldap form dc=nuxeo,dc=org to the DNS domain name form nuxeo.org
*
* @param dn base DN of the domain
* @return the FQDN or null is DN is not matching the expected structure
* @throws DirectoryException is the DN is invalid
*/
protected String convertDNtoFQDN(String dn) throws DirectoryException {
try {
LdapDN ldapDN = new LdapDN(dn);
Enumeration<String> components = ldapDN.getAll();
List<String> domainComponents = new ArrayList<String>();
while (components.hasMoreElements()) {
String component = components.nextElement();
if (component.startsWith("dc=")) {
domainComponents.add(component.substring(3));
} else {
break;
}
}
Collections.reverse(domainComponents);
return StringUtils.join(domainComponents, ".");
} catch (InvalidNameException e) {
throw new DirectoryException(e);
}
}
public boolean isPoolingEnabled() {
return poolingEnabled;
}
public boolean isVerifyServerCert() {
return verifyServerCert;
}
public int getConnectionTimeout() {
return connectionTimeout;
}
public void setConnectionTimeout(int connectionTimeout) {
this.connectionTimeout = connectionTimeout;
}
protected DNSServiceResolver getSRVResolver() {
return srvResolver;
}
/**
* Common internal interface for Ldap entries
*
* @author Bob Browning
*/
protected interface LdapEntry {
String getUrl() throws NamingException;
}
/**
* Server URL implementation of {@link LdapEntry}
*
* @author Bob Browning
*/
protected class LdapEntryDescriptor implements LdapEntry {
protected LDAPUrlDescriptor url;
public LdapEntryDescriptor(LDAPUrlDescriptor descriptor) {
url = descriptor;
}
@Override
public String toString() {
try {
return getUrl();
} catch (NamingException e) {
log.error(e, e);
return "[DNS lookup failed]";
}
}
@Override
public boolean equals(Object obj) {
if (obj instanceof LdapEntryDescriptor) {
return url.equals(obj);
}
return false;
}
@Override
public int hashCode() {
return url.hashCode();
}
@Override
public String getUrl() throws NamingException {
return url.getValue();
}
}
/**
* Domain implementation of {@link LdapEntry} using DNS SRV record
*
* @author Bob Browning
*/
protected final class LdapEntryDomain extends LdapEntryDescriptor {
protected final String domain;
protected final boolean useSsl;
public LdapEntryDomain(LDAPUrlDescriptor descriptor, final String domain, boolean useSsl) {
super(descriptor);
this.domain = domain;
this.useSsl = useSsl;
}
@Override
public String getUrl() throws NamingException {
List<DNSServiceEntry> servers = getSRVResolver().resolveLDAPDomainServers(domain, url.getSrvPrefix());
StringBuilder result = new StringBuilder();
for (DNSServiceEntry serviceEntry : servers) {
/*
* Rebuild the URL
*/
result.append(useSsl ? LDAPS_SCHEME + "://" : LDAP_SCHEME + "://");
result.append(serviceEntry);
result.append(' ');
}
return result.toString().trim();
}
private LDAPServerDescriptor getOuterType() {
return LDAPServerDescriptor.this;
}
@Override
public int hashCode() {
final int prime = 31;
int result = super.hashCode();
result = prime * result + getOuterType().hashCode();
result = prime * result + ((domain == null) ? 0 : domain.hashCode());
result = prime * result + (useSsl ? 1231 : 1237);
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!super.equals(obj)) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
LdapEntryDomain other = (LdapEntryDomain) obj;
if (!getOuterType().equals(other.getOuterType())) {
return false;
}
if (domain == null) {
if (other.domain != null) {
return false;
}
} else if (!domain.equals(other.domain)) {
return false;
}
if (useSsl != other.useSsl) {
return false;
}
return true;
}
}
/**
* @since 5.7
*/
public int getRetries() {
return retries;
}
}