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