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