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}