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