001/*
002 * (C) Copyright 2006-2007 Nuxeo SAS (http://nuxeo.com/) and contributors.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the GNU Lesser General Public License
006 * (LGPL) version 2.1 which accompanies this distribution, and is available at
007 * http://www.gnu.org/licenses/lgpl.html
008 *
009 * This library is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
012 * Lesser General Public License for more details.
013 *
014 * Contributors:
015 *     Nuxeo - initial API and implementation
016 *
017 */
018package org.nuxeo.ecm.directory.ldap;
019
020import java.util.ArrayList;
021import java.util.Collections;
022import java.util.Enumeration;
023import java.util.HashSet;
024import java.util.LinkedHashSet;
025import java.util.List;
026import java.util.Set;
027
028import javax.naming.InvalidNameException;
029import javax.naming.NamingException;
030
031import org.apache.commons.lang.StringUtils;
032import org.apache.commons.logging.Log;
033import org.apache.commons.logging.LogFactory;
034import org.apache.directory.shared.ldap.name.LdapDN;
035import org.nuxeo.common.xmap.annotation.XNode;
036import org.nuxeo.common.xmap.annotation.XNodeList;
037import org.nuxeo.common.xmap.annotation.XObject;
038import org.nuxeo.ecm.directory.DirectoryException;
039import org.nuxeo.ecm.directory.ldap.dns.DNSServiceEntry;
040import org.nuxeo.ecm.directory.ldap.dns.DNSServiceResolver;
041import org.nuxeo.ecm.directory.ldap.dns.DNSServiceResolverImpl;
042
043import com.sun.jndi.ldap.LdapURL;
044
045@XObject(value = "server")
046public class LDAPServerDescriptor {
047
048    public static final Log log = LogFactory.getLog(LDAPServerDescriptor.class);
049
050    protected static final String LDAPS_SCHEME = "ldaps";
051
052    protected static final String LDAP_SCHEME = "ldap";
053
054    @XNode("@name")
055    public String name;
056
057    public String ldapUrls;
058
059    public String bindDn;
060
061    @XNode("connectionTimeout")
062    public int connectionTimeout = 10000; // timeout after 10 seconds
063
064    @XNode("poolingEnabled")
065    public boolean poolingEnabled = true;
066
067    @XNode("verifyServerCert")
068    public boolean verifyServerCert = true;
069
070    /**
071     * @since 5.7
072     */
073    @XNode("retries")
074    public int retries = 5;
075
076    protected LinkedHashSet<LdapEntry> ldapEntries;
077
078    protected boolean isDynamicServerList = false;
079
080    protected boolean useSsl = false;
081
082    protected final DNSServiceResolver srvResolver = DNSServiceResolverImpl.getInstance();
083
084    public boolean isDynamicServerList() {
085        return isDynamicServerList;
086    }
087
088    public String getName() {
089        return name;
090    }
091
092    public String bindPassword = "";
093
094    @XNode("bindDn")
095    public void setBindDn(String bindDn) {
096        if (null != bindDn && bindDn.trim().equals("")) {
097            // empty bindDn means anonymous authentication
098            this.bindDn = null;
099        } else {
100            this.bindDn = bindDn;
101        }
102    }
103
104    public String getBindDn() {
105        return bindDn;
106    }
107
108    @XNode("bindPassword")
109    public void setBindPassword(String bindPassword) {
110        if (bindPassword == null) {
111            // no password means empty pasword
112            this.bindPassword = "";
113        } else {
114            this.bindPassword = bindPassword;
115        }
116    }
117
118    public String getBindPassword() {
119        return bindPassword;
120    }
121
122    public String getLdapUrls() {
123        if (ldapUrls != null) {
124            return ldapUrls;
125        }
126
127        // Leverage JNDI support for clustered servers by concatenating
128        // all the provided URLs for fail-over
129        StringBuilder calculatedLdapUrls = new StringBuilder();
130        for (LdapEntry entry : ldapEntries) {
131            calculatedLdapUrls.append(entry);
132            calculatedLdapUrls.append(' ');
133        }
134
135        /*
136         * If the configuration does not contain any domain entries then cache the urls, domain entries should always be
137         * re-queried however as the LDAP server list should change dynamically
138         */
139        if (!isDynamicServerList) {
140            return ldapUrls = calculatedLdapUrls.toString().trim();
141        }
142        return calculatedLdapUrls.toString().trim();
143    }
144
145    @XNodeList(value = "ldapUrl", componentType = LDAPUrlDescriptor.class, type = LDAPUrlDescriptor[].class)
146    public void setLdapUrls(LDAPUrlDescriptor[] ldapUrls) throws DirectoryException {
147        if (ldapUrls == null) {
148            throw new DirectoryException("At least one <ldapUrl/> server declaration is required");
149        }
150        ldapEntries = new LinkedHashSet<LdapEntry>();
151
152        Set<LDAPUrlDescriptor> processed = new HashSet<LDAPUrlDescriptor>();
153
154        List<String> urls = new ArrayList<String>(ldapUrls.length);
155        for (LDAPUrlDescriptor url : ldapUrls) {
156            LdapURL ldapUrl;
157            try {
158                /*
159                 * Empty string translates to ldap://localhost:389 through JNDI
160                 */
161                if (StringUtils.isEmpty(url.getValue())) {
162                    urls.add(url.getValue());
163                    ldapEntries.add(new LdapEntryDescriptor(url));
164                    continue;
165                }
166
167                /*
168                 * Parse the URI to make sure it is valid
169                 */
170                ldapUrl = new LdapURL(url.getValue());
171                if (!processed.add(url)) {
172                    continue;
173                }
174            } catch (NamingException e) {
175                throw new DirectoryException(e);
176            }
177
178            useSsl = useSsl || ldapUrl.useSsl();
179
180            /*
181             * RFC-2255 - The "ldap" prefix indicates an entry or entries residing in the LDAP server running on the
182             * given hostname at the given port number. The default LDAP port is TCP port 389. If no hostport is given,
183             * the client must have some apriori knowledge of an appropriate LDAP server to contact.
184             */
185            if (ldapUrl.getHost() == null) {
186                /*
187                 * RFC-2782 - Check to see if an LDAP SRV record is defined in the DNS server
188                 */
189                String domain = convertDNtoFQDN(ldapUrl.getDN());
190                if (domain != null) {
191                    /*
192                     * Dynamic URL - retrieve from SRV record
193                     */
194                    List<String> discoveredUrls;
195                    try {
196                        discoveredUrls = discoverLdapServers(domain, ldapUrl.useSsl(), url.getSrvPrefix());
197                    } catch (NamingException e) {
198                        throw new DirectoryException(String.format("SRV record DNS lookup failed for %s.%s: %s",
199                                url.getSrvPrefix(), domain, e.getMessage()), e);
200                    }
201
202                    /*
203                     * Discovered URLs could be empty, lets check at the end though
204                     */
205                    urls.addAll(discoveredUrls);
206
207                    /*
208                     * Store entries in an ordered set and remember that we were dynamic
209                     */
210                    ldapEntries.add(new LdapEntryDomain(url, domain, ldapUrl.useSsl()));
211                    isDynamicServerList = true;
212                } else {
213                    throw new DirectoryException("Invalid LDAP SRV reference, this should be of the form"
214                            + " ldap:///dc=example,dc=org");
215                }
216            } else {
217                /*
218                 * Static URL - store the value
219                 */
220                urls.add(url.getValue());
221
222                /*
223                 * Store entries in an ordered set
224                 */
225                ldapEntries.add(new LdapEntryDescriptor(url));
226            }
227        }
228
229        /*
230         * Oops no valid URLs to connect to :(
231         */
232        if (urls.isEmpty()) {
233            throw new DirectoryException("No valid server urls returned from DNS query");
234        }
235    }
236
237    /**
238     * Whether this server descriptor defines a secure ldap connection
239     */
240    public boolean useSsl() {
241        return useSsl;
242    }
243
244    /**
245     * Retrieve server URLs from DNS SRV record
246     *
247     * @param domain The domain to query
248     * @param useSsl Whether the connection to this domain should be secure
249     * @return List of servers or empty list
250     * @throws NamingException if DNS lookup fails
251     */
252    protected List<String> discoverLdapServers(String domain, boolean useSsl, String srvPrefix) throws NamingException {
253        List<String> result = new ArrayList<String>();
254        List<DNSServiceEntry> servers = getSRVResolver().resolveLDAPDomainServers(domain, srvPrefix);
255
256        for (DNSServiceEntry serviceEntry : servers) {
257            /*
258             * Rebuild the URL
259             */
260            StringBuilder realUrl = (useSsl) ? new StringBuilder(LDAPS_SCHEME + "://") : new StringBuilder(LDAP_SCHEME
261                    + "://");
262            realUrl.append(serviceEntry);
263            result.add(realUrl.toString());
264        }
265        return result;
266    }
267
268    /**
269     * Convert domain from the ldap form dc=nuxeo,dc=org to the DNS domain name form nuxeo.org
270     *
271     * @param dn base DN of the domain
272     * @return the FQDN or null is DN is not matching the expected structure
273     * @throws DirectoryException is the DN is invalid
274     */
275    protected String convertDNtoFQDN(String dn) throws DirectoryException {
276        try {
277            LdapDN ldapDN = new LdapDN(dn);
278            Enumeration<String> components = ldapDN.getAll();
279            List<String> domainComponents = new ArrayList<String>();
280            while (components.hasMoreElements()) {
281                String component = components.nextElement();
282                if (component.startsWith("dc=")) {
283                    domainComponents.add(component.substring(3));
284                } else {
285                    break;
286                }
287            }
288            Collections.reverse(domainComponents);
289            return StringUtils.join(domainComponents, ".");
290        } catch (InvalidNameException e) {
291            throw new DirectoryException(e);
292        }
293    }
294
295    public boolean isPoolingEnabled() {
296        return poolingEnabled;
297    }
298
299    public boolean isVerifyServerCert() {
300        return verifyServerCert;
301    }
302
303    public int getConnectionTimeout() {
304        return connectionTimeout;
305    }
306
307    public void setConnectionTimeout(int connectionTimeout) {
308        this.connectionTimeout = connectionTimeout;
309    }
310
311    protected DNSServiceResolver getSRVResolver() {
312        return srvResolver;
313    }
314
315    /**
316     * Common internal interface for Ldap entries
317     *
318     * @author Bob Browning
319     */
320    protected interface LdapEntry {
321        String getUrl() throws NamingException;
322    }
323
324    /**
325     * Server URL implementation of {@link LdapEntry}
326     *
327     * @author Bob Browning
328     */
329    protected class LdapEntryDescriptor implements LdapEntry {
330
331        protected LDAPUrlDescriptor url;
332
333        public LdapEntryDescriptor(LDAPUrlDescriptor descriptor) {
334            url = descriptor;
335        }
336
337        @Override
338        public String toString() {
339            try {
340                return getUrl();
341            } catch (NamingException e) {
342                log.error(e, e);
343                return "[DNS lookup failed]";
344            }
345        }
346
347        @Override
348        public boolean equals(Object obj) {
349            if (obj instanceof LdapEntryDescriptor) {
350                return url.equals(obj);
351            }
352            return false;
353        }
354
355        @Override
356        public int hashCode() {
357            return url.hashCode();
358        }
359
360        public String getUrl() throws NamingException {
361            return url.getValue();
362        }
363
364    }
365
366    /**
367     * Domain implementation of {@link LdapEntry} using DNS SRV record
368     *
369     * @author Bob Browning
370     */
371    protected final class LdapEntryDomain extends LdapEntryDescriptor {
372
373        protected final String domain;
374
375        protected final boolean useSsl;
376
377        public LdapEntryDomain(LDAPUrlDescriptor descriptor, final String domain, boolean useSsl) {
378            super(descriptor);
379            this.domain = domain;
380            this.useSsl = useSsl;
381        }
382
383        @Override
384        public String getUrl() throws NamingException {
385            List<DNSServiceEntry> servers = getSRVResolver().resolveLDAPDomainServers(domain, url.getSrvPrefix());
386
387            StringBuilder result = new StringBuilder();
388            for (DNSServiceEntry serviceEntry : servers) {
389                /*
390                 * Rebuild the URL
391                 */
392                result.append(useSsl ? LDAPS_SCHEME + "://" : LDAP_SCHEME + "://");
393                result.append(serviceEntry);
394                result.append(' ');
395            }
396            return result.toString().trim();
397        }
398
399        private LDAPServerDescriptor getOuterType() {
400            return LDAPServerDescriptor.this;
401        }
402
403        @Override
404        public int hashCode() {
405            final int prime = 31;
406            int result = super.hashCode();
407            result = prime * result + getOuterType().hashCode();
408            result = prime * result + ((domain == null) ? 0 : domain.hashCode());
409            result = prime * result + (useSsl ? 1231 : 1237);
410            return result;
411        }
412
413        @Override
414        public boolean equals(Object obj) {
415            if (this == obj) {
416                return true;
417            }
418            if (!super.equals(obj)) {
419                return false;
420            }
421            if (getClass() != obj.getClass()) {
422                return false;
423            }
424            LdapEntryDomain other = (LdapEntryDomain) obj;
425            if (!getOuterType().equals(other.getOuterType())) {
426                return false;
427            }
428            if (domain == null) {
429                if (other.domain != null) {
430                    return false;
431                }
432            } else if (!domain.equals(other.domain)) {
433                return false;
434            }
435            if (useSsl != other.useSsl) {
436                return false;
437            }
438            return true;
439        }
440    }
441
442    /**
443     * @since 5.7
444     */
445    public int getRetries() {
446        return retries;
447    }
448
449}