001/* 002 * The contents of this file are subject to the terms of the Common Development and 003 * Distribution License (the License). You may not use this file except in compliance with the 004 * License. 005 * 006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the 007 * specific language governing permission and limitations under the License. 008 * 009 * When distributing Covered Software, include this CDDL Header Notice in each file and include 010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL 011 * Header, with the fields enclosed by brackets [] replaced by your own identifying 012 * information: "Portions copyright [year] [name of copyright owner]". 013 * 014 * Copyright 2013-2017 ForgeRock AS. 015 */ 016package org.forgerock.openam.ldap; 017 018import static org.forgerock.openam.ldap.LDAPConstants.TLS_PROTOCOL_VERSION_LIST; 019import static org.forgerock.opendj.ldap.LDAPConnectionFactory.*; 020 021import com.sun.identity.shared.Constants; 022import com.sun.identity.shared.configuration.SystemPropertiesManager; 023import com.sun.identity.shared.debug.Debug; 024 025import java.security.GeneralSecurityException; 026import java.util.ArrayList; 027import java.util.Collections; 028import java.util.HashMap; 029import java.util.HashSet; 030import java.util.LinkedHashSet; 031import java.util.List; 032import java.util.Map; 033import java.util.Set; 034import java.util.StringTokenizer; 035import java.util.concurrent.TimeUnit; 036 037import javax.naming.InvalidNameException; 038 039import org.forgerock.openam.utils.CollectionUtils; 040import org.forgerock.i18n.LocalizedIllegalArgumentException; 041import org.forgerock.opendj.ldap.Attribute; 042import org.forgerock.opendj.ldap.ByteString; 043import org.forgerock.opendj.ldap.Connection; 044import org.forgerock.opendj.ldap.ConnectionFactory; 045import org.forgerock.opendj.ldap.Connections; 046import org.forgerock.opendj.ldap.DN; 047import org.forgerock.opendj.ldap.Filter; 048import org.forgerock.opendj.ldap.LDAPConnectionFactory; 049import org.forgerock.opendj.ldap.LdapException; 050import org.forgerock.opendj.ldap.LoadBalancerEventListener; 051import org.forgerock.opendj.ldap.RDN; 052import org.forgerock.opendj.ldap.SSLContextBuilder; 053import org.forgerock.opendj.ldap.SearchResultReferenceIOException; 054import org.forgerock.opendj.ldap.SearchScope; 055import org.forgerock.opendj.ldap.TrustManagers; 056import org.forgerock.opendj.ldif.ConnectionEntryReader; 057import org.forgerock.util.Option; 058import org.forgerock.util.Options; 059import org.forgerock.util.Reject; 060import org.forgerock.util.time.Duration; 061 062/** 063 * Utility methods to help interaction with the OpenDJ LDAP SDK. 064 * There are two main ways currently to create connection pools/factories: 065 * <ul> 066 * <li>Providing a set of servers in the format specified in {@link 067 * #prioritizeServers(java.util.Set, java.lang.String, java.lang.String)}, which will be prioritized based on the 068 * current server's server ID/site ID.</li> 069 * <li>Providing a set of LDAPURLs, which are already considered as "prioritized".</li> 070 * </ul> 071 * In case the configuration provides the possibility to assign LDAP servers to OpenAM servers/sites, then either you 072 * can prioritize manually (if the logic differs from this implementation) and create the corresponding {@link LDAPURL} 073 * objects, or you can pass in the list to the newPrioritized* methods. 074 * 075 * @supported.all.api 076 */ 077public final class LDAPUtils { 078 079 /** 080 * An {@link Option} that tells whether affinity based load balancing is enabled for the connections. 081 */ 082 public static final Option<Boolean> AFFINITY_ENABLED = Option.withDefault(false); 083 private static final String LDAP_SCOPE_BASE = "SCOPE_BASE"; 084 private static final String LDAP_SCOPE_ONE = "SCOPE_ONE"; 085 private static final String LDAP_SCOPE_SUB = "SCOPE_SUB"; 086 private static final Map<String, SearchScope> SCOPES; 087 private static final Debug DEBUG = Debug.getInstance("LDAPUtils"); 088 private static final int DEFAULT_HEARTBEAT_TIMEOUT = 3; 089 private static final List<String> LDAP_SECURE_PROTOCOLS = getEnabledProtocols(); 090 091 static { 092 Map<String, SearchScope> mappings = new HashMap<String, SearchScope>(3); 093 mappings.put(LDAP_SCOPE_BASE, SearchScope.BASE_OBJECT); 094 mappings.put(LDAP_SCOPE_ONE, SearchScope.SINGLE_LEVEL); 095 mappings.put(LDAP_SCOPE_SUB, SearchScope.WHOLE_SUBTREE); 096 SCOPES = Collections.unmodifiableMap(mappings); 097 } 098 099 private LDAPUtils() { 100 } 101 102 /** 103 * Based on the incoming parameters prioritizes the LDAP server list, then creates a connection pool that is 104 * capable to failover to the servers defined in case there is an error. 105 * 106 * @param servers The set of servers in the format defined in {@link 107 * #prioritizeServers(java.util.Set, java.lang.String, java.lang.String)}. 108 * @param hostServerId The server ID for this OpenAM server. 109 * @param hostSiteId The site ID for this OpenAM server. 110 * @param username The directory user's DN. May be null if this is an anonymous connection. 111 * @param password The directory user's password. 112 * @param maxSize The max size of the created pool. 113 * @param heartBeatInterval The interval for sending out heartbeat requests. 114 * @param heartBeatTimeUnit The timeunit for the heartbeat interval. 115 * @param useStartTLS Use StartTLS to connect to the LDAP Server(s). 116 * @param sslTrustAll trust all certs to connect to the LDAPS Server(s). 117 * @param ldapOptions Additional LDAP settings used to create the pool. 118 * @return A failover loadbalanced authenticated/anonymous connection pool, which may also send heartbeat requests. 119 */ 120 public static ConnectionFactory newPrioritizedFailoverConnectionPool(Set<String> servers, 121 String hostServerId, 122 String hostSiteId, 123 String username, 124 char[] password, 125 int maxSize, 126 int heartBeatInterval, 127 String heartBeatTimeUnit, 128 boolean useStartTLS, 129 boolean sslTrustAll, 130 Options ldapOptions) { 131 return newFailoverConnectionPool(prioritizeServers(servers, hostServerId, hostSiteId), 132 username, password, maxSize, heartBeatInterval, heartBeatTimeUnit, useStartTLS, sslTrustAll, 133 ldapOptions); 134 } 135 136 /** 137 * Creates a new connection pool that is capable to failover to the servers defined in case there is an error. 138 * 139 * @param servers The set of LDAP URLs that will be used to set up the connection factory. 140 * @param username The directory user's DN. May be null if this is an anonymous connection. 141 * @param password The directory user's password. 142 * @param maxSize The max size of the created pool. 143 * @param heartBeatInterval The interval for sending out heartbeat requests. 144 * @param heartBeatTimeUnit The timeunit for the heartbeat interval. 145 * @param useStartTLS Use StartTLS to connect to the LDAP Server(s). 146 * @param sslTrustAll trust all certs to connect to the LDAPS Server(s). 147 * @param ldapOptions Additional LDAP settings used to create the pool 148 * @return A failover loadbalanced authenticated/anonymous connection pool, which may also send heartbeat requests. 149 */ 150 public static ConnectionFactory newFailoverConnectionPool(Set<LDAPURL> servers, 151 String username, 152 char[] password, 153 int maxSize, 154 int heartBeatInterval, 155 String heartBeatTimeUnit, 156 boolean useStartTLS, 157 boolean sslTrustAll, 158 Options ldapOptions) { 159 List<ConnectionFactory> factories = new ArrayList<ConnectionFactory>(servers.size()); 160 for (LDAPURL ldapurl : servers) { 161 ConnectionFactory cf = Connections.newFixedConnectionPool( 162 newConnectionFactory(ldapurl, username, password, heartBeatInterval, heartBeatTimeUnit, 163 useStartTLS, sslTrustAll, ldapOptions), maxSize); 164 factories.add(cf); 165 } 166 167 return loadBalanceFactories(factories, ldapOptions); 168 } 169 170 /** 171 * Based on the incoming parameters prioritizes the LDAP server list, then creates a connection factory that is 172 * capable to failover to the servers defined in case there is an error. 173 * 174 * @param servers The set of servers in the format defined in {@link 175 * #prioritizeServers(java.util.Set, java.lang.String, java.lang.String)}. 176 * @param hostServerId The server ID for this OpenAM server. 177 * @param hostSiteId The site ID for this OpenAM server. 178 * @param username The directory user's DN. May be null if this is an anonymous connection. 179 * @param password The directory user's password. 180 * @param heartBeatInterval The interval for sending out heartbeat requests. 181 * @param heartBeatTimeUnit The timeunit for the heartbeat interval. 182 * @param useStartTLS Use StartTLS to connect to the LDAP Server(s). 183 * @param sslTrustAll trust all certs to connect to the LDAPS Server(s). 184 * @param options Additional LDAP settings used to create the connection factory. 185 * @return A failover loadbalanced authenticated/anonymous connection factory, which may also send heartbeat 186 * requests. 187 */ 188 public static ConnectionFactory newPrioritizedFailoverConnectionFactory(Set<String> servers, 189 String hostServerId, 190 String hostSiteId, 191 String username, 192 char[] password, 193 int heartBeatInterval, 194 String heartBeatTimeUnit, 195 boolean useStartTLS, 196 boolean sslTrustAll, 197 Options options) { 198 return newFailoverConnectionFactory(prioritizeServers(servers, hostServerId, hostSiteId), 199 username, password, heartBeatInterval, heartBeatTimeUnit, useStartTLS, sslTrustAll, options); 200 } 201 202 /** 203 * Creates a new connection factory that is capable to failover to the servers defined in case there is an error. 204 * 205 * @param servers The set of LDAP URLs that will be used to set up the connection factory. 206 * @param username The directory user's DN. May be null if this is an anonymous connection. 207 * @param password The directory user's password. 208 * @param heartBeatInterval The interval for sending out heartbeat requests. 209 * @param heartBeatTimeUnit The timeunit for the heartbeat interval. 210 * @param useStartTLS Use StartTLS to connect to the LDAP Server(s). 211 * @param sslTrustAll trust all certs to connect to the LDAPS Server(s). 212 * @param ldapOptions Additional LDAP settings used to create the connection factory. 213 * @return A failover loadbalanced authenticated/anonymous connection factory, which may also send heartbeat 214 * requests. 215 */ 216 public static ConnectionFactory newFailoverConnectionFactory(Set<LDAPURL> servers, 217 String username, 218 char[] password, 219 int heartBeatInterval, 220 String heartBeatTimeUnit, 221 boolean useStartTLS, 222 boolean sslTrustAll, 223 Options ldapOptions) { 224 List<ConnectionFactory> factories = new ArrayList<ConnectionFactory>(servers.size()); 225 for (LDAPURL ldapurl : servers) { 226 factories.add(newConnectionFactory(ldapurl, username, password, heartBeatInterval, heartBeatTimeUnit, 227 useStartTLS, sslTrustAll, ldapOptions)); 228 } 229 return loadBalanceFactories(factories, ldapOptions); 230 } 231 232 /** 233 * Creates a new connection factory based on the provided parameters. 234 * 235 * @param ldapurl The address of the LDAP server. 236 * @param username The directory user's DN. May be null if this is an anonymous connection. 237 * @param password The directory user's password. 238 * @param heartBeatInterval The interval for sending out heartbeat requests. 239 * @param heartBeatTimeUnit The timeunit for the heartbeat interval. 240 * @param useStartTLS Use StartTLS to connect to the LDAP Server(s). 241 * @param sslTrustAll trust all certs to connect to the LDAPS Server(s). 242 * @param ldapOptions Additional LDAP settings used to create the connection factory. 243 * @return An authenticated/anonymous connection factory, which may also send heartbeat requests. 244 */ 245 private static ConnectionFactory newConnectionFactory(LDAPURL ldapurl, 246 String username, 247 char[] password, 248 int heartBeatInterval, 249 String heartBeatTimeUnit, 250 boolean useStartTLS, 251 boolean sslTrustAll, 252 Options ldapOptions) { 253 Boolean ssl = ldapurl.isSSL(); 254 int heartBeatTimeout = 255 SystemPropertiesManager.getAsInt(Constants.LDAP_HEARTBEAT_TIMEOUT, DEFAULT_HEARTBEAT_TIMEOUT); 256 if (ssl != null && ssl.booleanValue() || useStartTLS) { 257 try { 258 //Creating a defensive copy of ldapOptions to handle the case when a mixture of SSL/non-SSL connections 259 //needs to be established. 260 ldapOptions = Options.copyOf(ldapOptions); 261 SSLContextBuilder builder = new SSLContextBuilder(); 262 if (sslTrustAll) { 263 builder.setTrustManager(TrustManagers.trustAll()); 264 } 265 ldapOptions = ldapOptions.set(SSL_CONTEXT, builder.getSSLContext()); 266 if (useStartTLS) { 267 ldapOptions = ldapOptions.set(SSL_USE_STARTTLS, true); 268 } 269 ldapOptions = ldapOptions.set(SSL_ENABLED_PROTOCOLS, LDAP_SECURE_PROTOCOLS); 270 } catch (GeneralSecurityException gse) { 271 DEBUG.error("An error occurred while creating SSLContext", gse); 272 } 273 } 274 275 // Enable heartbeat 276 if (heartBeatInterval > 0 && heartBeatTimeUnit != null) { 277 TimeUnit unit = TimeUnit.valueOf(heartBeatTimeUnit.toUpperCase()); 278 ldapOptions = ldapOptions 279 .set(HEARTBEAT_ENABLED, true) 280 .set(HEARTBEAT_INTERVAL, new Duration(unit.toSeconds(heartBeatInterval), TimeUnit.SECONDS)) 281 .set(HEARTBEAT_TIMEOUT, new Duration(unit.toSeconds(heartBeatTimeout), TimeUnit.SECONDS)); 282 } 283 284 // Enable Authenticated connection 285 if (username != null) { 286 ldapOptions = ldapOptions.set(AUTHN_BIND_REQUEST, LDAPRequests.newSimpleBindRequest(username, password)); 287 } 288 289 return new LDAPConnectionFactory(ldapurl.getHost(), ldapurl.getPort(), ldapOptions); 290 } 291 292 private static ConnectionFactory loadBalanceFactories(List<ConnectionFactory> factories, Options options) { 293 if (options.get(AFFINITY_ENABLED)) { 294 return Connections.newShardedRequestLoadBalancer(factories, options); 295 } else { 296 return Connections.newFailoverLoadBalancer(factories, options); 297 } 298 } 299 300 /** 301 * Prioritizes the incoming LDAP servers based on their assigned servers/sites. 302 * The format of the server list can be either one of the followings: 303 * <ul> 304 * <li><code>host:port</code> - The LDAP server has no preferred 305 * server/site</li> 306 * <li><code>host:port|serverid</code> - The LDAP server should be mainly 307 * used by an OpenAM instance with the same serverid</li> 308 * <li><code>host:port|serverid|siteid</code> - The LDAP server should be 309 * mainly used by an OpenAM instance with the same serverid or with the same 310 * siteid</li> 311 * </ul> 312 * The resulting priority list will have the following order: 313 * <ul> 314 * <li>servers that are linked with this server</li> 315 * <li>servers that are linked with the current site</li> 316 * <li>any other server that did not match in the same order as they were defined</li> 317 * </ul> 318 * 319 * @param servers The Set of servers that needs to be prioritized in the previously described format. 320 * @param hostServerId This server's ID. 321 * @param hostSiteId This server's site ID. 322 * @return The prioritized Set of LDAP URLs that can be used to create connection factories. 323 */ 324 public static Set<LDAPURL> prioritizeServers(Set<String> servers, String hostServerId, String hostSiteId) { 325 Set<LDAPURL> ldapServers = new LinkedHashSet<LDAPURL>(servers.size()); 326 Set<LDAPURL> serverDefined = new LinkedHashSet<LDAPURL>(servers.size()); 327 Set<LDAPURL> siteDefined = new LinkedHashSet<LDAPURL>(servers.size()); 328 Set<LDAPURL> nonMatchingServers = new LinkedHashSet<LDAPURL>(servers.size()); 329 for (String server : servers) { 330 StringTokenizer tokenizer = new StringTokenizer(server, "|"); 331 String ldapUrl = tokenizer.nextToken(); 332 String assignedServerId = ""; 333 String assignedSiteId = ""; 334 335 if (tokenizer.hasMoreTokens()) { 336 assignedServerId = tokenizer.nextToken(); 337 } 338 if (tokenizer.hasMoreTokens()) { 339 assignedSiteId = tokenizer.nextToken(); 340 } 341 if (!assignedServerId.isEmpty() && assignedServerId.equals(hostServerId)) { 342 serverDefined.add(LDAPURL.valueOf(ldapUrl)); 343 } else if (!assignedSiteId.isEmpty() && assignedSiteId.equals(hostSiteId)) { 344 siteDefined.add(LDAPURL.valueOf(ldapUrl)); 345 } else { 346 nonMatchingServers.add(LDAPURL.valueOf(ldapUrl)); 347 } 348 } 349 //Let's add them in the order of priority to the ldapServers set, this way the most appropriate servers should 350 //be at the beginning of the list and towards the end of the list are the possibly most remote servers. 351 ldapServers.addAll(serverDefined); 352 ldapServers.addAll(siteDefined); 353 ldapServers.addAll(nonMatchingServers); 354 return ldapServers; 355 } 356 357 /** 358 * Converts string representation of scope (as defined in the configuration) to the corresponding 359 * {@link SearchScope} object. 360 * 361 * @param scope the string representation of the scope. 362 * @param defaultScope in case the coversion fail this default scope should be returned. 363 * @return the corresponding {@link SearchScope} object. 364 */ 365 public static SearchScope getSearchScope(String scope, SearchScope defaultScope) { 366 SearchScope searchScope = SCOPES.get(scope); 367 return searchScope == null ? defaultScope : searchScope; 368 } 369 370 /** 371 * Parses the incoming filter, and in case of failure falls back to the default filter. 372 * 373 * @param filter The filter that needs to be parsed. 374 * @param defaultFilter If the parsing fails, this will be returned. 375 * @return The parsed Filter object, or the default Filter, if the parse failed. 376 */ 377 public static Filter parseFilter(String filter, Filter defaultFilter) { 378 try { 379 return filter == null ? defaultFilter : Filter.valueOf(filter); 380 } catch (LocalizedIllegalArgumentException liae) { 381 DEBUG.error("Unable to construct Filter from " + filter + " -> " + liae.getMessage() 382 + "\nFalling back to " + defaultFilter.toString()); 383 } 384 return defaultFilter; 385 } 386 387 /** 388 * Returns the RDN without the attribute name from the passed in {@link DN} object, for example: 389 * <code>uid=demo,ou=people,dc=example,dc=com</code> will return <code>demo</code>. 390 * 391 * @param dn The DN that we need the name of. 392 * @return The RDN of the DN without the attribute name. 393 */ 394 public static String getName(DN dn) { 395 return dn.rdn().getFirstAVA().getAttributeValue().toString(); 396 } 397 398 /** 399 * Converts the Attribute to an attribute name, 2-dimensional byte array map and adds it to the map passed in. 400 * The first dimension of the byte array separates the different values, the second dimension holds the actual 401 * value. 402 * 403 * @param attribute The attribute that needs to be converted. 404 * @param map The map where the converted attribute is added to. 405 */ 406 public static void addAttributeToMapAsByteArray(Attribute attribute, Map<String, byte[][]> map) { 407 byte[][] values = new byte[attribute.size()][]; 408 int counter = 0; 409 for (ByteString byteString : attribute) { 410 byte[] bytes = byteString.toByteArray(); 411 values[counter++] = bytes; 412 } 413 map.put(attribute.getAttributeDescriptionAsString(), values); 414 } 415 416 /** 417 * Converts the Attribute to an attribute name, set of String values map and adds it to the map passed in. 418 * 419 * @param attribute The attribute that needs to be converted. 420 * @param map The map where the converted attribute is added to. 421 */ 422 public static void addAttributeToMapAsString(Attribute attribute, Map<String, Set<String>> map) { 423 map.put(attribute.getAttributeDescriptionAsString(), getAttributeValuesAsStringSet(attribute)); 424 } 425 426 /** 427 * Converts all the attribute values to a String Set. 428 * 429 * @param attribute the attribute to be converted. 430 * @return A Set of String representations of the Attribute values. 431 */ 432 public static Set<String> getAttributeValuesAsStringSet(Attribute attribute) { 433 Set<String> values = new HashSet<String>(attribute.size()); 434 for (ByteString byteString : attribute) { 435 values.add(byteString.toString()); 436 } 437 return values; 438 } 439 440 /** 441 * Converts the incoming set of URLs to {@link LDAPURL} instances and returns them as a set. The iteration order 442 * of the originally passed in Set is retained. 443 * 444 * @param servers The LDAP server URLs that needs to be converted to {@link LDAPURL} instances. 445 * @return A set of LDAPURLs corresponding to the passed in URLs. 446 */ 447 public static Set<LDAPURL> convertToLDAPURLs(Set<String> servers) { 448 if (servers == null) { 449 return new LinkedHashSet<LDAPURL>(0); 450 } else { 451 Set<LDAPURL> ret = new LinkedHashSet<LDAPURL>(servers.size()); 452 for (String server : servers) { 453 ret.add(LDAPURL.valueOf(server)); 454 } 455 return ret; 456 } 457 } 458 459 /** 460 * When provided a DN, returns the value part of the first RDN. 461 * @param dn A DN. 462 * @return The value part of the first RDN. 463 * @throws IllegalArgumentException When the DN's RDN is multivalued, or when the DN is not a valid name. 464 */ 465 public static String rdnValueFromDn(String dn) { 466 return rdnValueFromDn(DN.valueOf(dn)); 467 } 468 469 /** 470 * When provided a DN, returns the value part of the first RDN. 471 * @param dn A DN. 472 * @return The value part of the first RDN. 473 * @throws IllegalArgumentException When the DN's RDN is multivalued. 474 */ 475 public static String rdnValueFromDn(DN dn) { 476 if (dn.size() > 0) { 477 return rdnValue(dn.rdn()); 478 } 479 return ""; 480 } 481 482 /** 483 * When provided an RDN, returns the value part. 484 * @param rdn An RDN. 485 * @return The value part. 486 * @throws IllegalArgumentException When the RDN is multivalued. 487 */ 488 public static String rdnValue(RDN rdn) { 489 Reject.ifTrue(rdn.isMultiValued(), "Multivalued RDNs not supported"); 490 return rdn.getFirstAVA().getAttributeValue().toString(); 491 } 492 493 /** 494 * When provided a DN, returns the attribute type name of the first RDN. 495 * @param dn A DN. 496 * @return The attribute type name of the first RDN. 497 * @throws IllegalArgumentException When the DN's RDN is multivalued. 498 */ 499 public static String rdnTypeFromDn(String dn) { 500 return rdnTypeFromDn(DN.valueOf(dn)); 501 } 502 503 /** 504 * When provided a DN, returns the attribute type name of the first RDN. 505 * @param dn A DN. 506 * @return The attribute type name of the first RDN. 507 * @throws IllegalArgumentException When the DN's RDN is multivalued. 508 */ 509 public static String rdnTypeFromDn(DN dn) { 510 if (dn.size() > 0) { 511 return rdnType(dn.rdn()); 512 } 513 return ""; 514 } 515 516 /** 517 * When provided an RDN, returns the attribute type name. 518 * @param rdn An RDN. 519 * @return The attribute type name. 520 * @throws IllegalArgumentException When the RDN is multivalued. 521 */ 522 public static String rdnType(RDN rdn) { 523 Reject.ifTrue(rdn.size() != 1, "Multivalued RDNs not supported"); 524 return rdn.getFirstAVA().getAttributeType().getNameOrOID(); 525 } 526 527 /** 528 * Returns a set of all the non-root DNs from the collection that are not equal to the {@code compare} parameter. 529 * @param compare The DN to compare against. 530 * @param dns THe DNs to compare. 531 * @return A {@code Set} of non identical DNs. 532 * @throws InvalidNameException If an error occurs. 533 */ 534 public static Set<String> collectNonIdenticalValues(DN compare, Set<String> dns) throws InvalidNameException { 535 Set<String> results = new HashSet<>(); 536 for (String dnString : dns) { 537 DN dn = DN.valueOf(dnString); 538 if (dn.size() > 0 && compare.compareTo(dn) != 0) { 539 results.add(rdnValueFromDn(dn)); 540 } 541 } 542 return results; 543 } 544 545 /** 546 * Gets the DB name. 547 * 548 * @param suffix The suffix. 549 * @param ld The connection. 550 * @return The name of the DB. 551 */ 552 public static String getDBName(String suffix, Connection ld) { 553 String filter = "cn=" + suffix; 554 555 try { 556 ConnectionEntryReader results = ld.search(LDAPRequests.newSearchRequest("cn=mapping tree,cn=config", 557 SearchScope.WHOLE_SUBTREE, filter)); 558 while (results.hasNext()) { 559 Attribute dbName = results.readEntry().getAttribute("nsslapd-backend"); 560 if (dbName != null) { 561 return dbName.firstValueAsString(); 562 } 563 } 564 } catch (LdapException e) { 565 // If not S1DS, then cn=mapping tree wouldn't exist. 566 // Hence return userRoot as DBNAME. 567 } catch (SearchResultReferenceIOException e) { 568 DEBUG.error("LDAPUtils.getDBName: Did not expect to get a reference", e); 569 } 570 return "userRoot"; 571 } 572 573 /** 574 * Tests whether the supplied string is a DN, and is not the root DN. 575 * @param candidateDN The possible DN. 576 * @return {@code true} if the string is a DN. 577 */ 578 public static boolean isDN(String candidateDN) { 579 try { 580 return newDN(candidateDN).size() > 0; 581 } catch (LocalizedIllegalArgumentException e) { 582 DEBUG.error("LDAPUtils.isDN: Invalid DN", e); 583 } 584 return false; 585 } 586 587 /** 588 * Escapes characters that should be escaped. 589 * 590 * @param str The string to escape. 591 * @return The escaped string. 592 */ 593 public static String escapeValue(String str) { 594 return DN.escapeAttributeValue(str); 595 } 596 597 /** 598 * Escapes the provided assertion value according to the LDAP standard. As a special case this method does not 599 * escape the '*' character, in order to be able to use wildcards in filters. 600 * 601 * @param assertionValue The filter assertionValue that needs to be escaped. 602 * @return The escaped assertionValue. 603 */ 604 public static String partiallyEscapeAssertionValue(String assertionValue) { 605 StringBuilder sb = new StringBuilder(assertionValue.length()); 606 for (int j = 0; j < assertionValue.length(); j++) { 607 char c = assertionValue.charAt(j); 608 if (c == '*') { 609 sb.append(c); 610 } else { 611 sb.append(Filter.escapeAssertionValue(String.valueOf(c))); 612 } 613 } 614 return sb.toString(); 615 } 616 617 /** 618 * Normalizes the DN. 619 * 620 * @param dn The DN to normalize. 621 * @return The normalized DN. 622 */ 623 public static String normalizeDN(String dn) { 624 return newDN(dn).toString().toLowerCase(); 625 } 626 627 /** 628 * Creates a DN from the specified DN string. 629 * 630 * @param orgName The DN string. 631 * @return A DN. 632 */ 633 public static DN newDN(String orgName) { 634 if (orgName == null || orgName.startsWith("/") || !orgName.contains("=")) { 635 return DN.rootDN(); 636 } else { 637 return DN.valueOf(orgName); 638 } 639 } 640 641 /** 642 * Converts a DN String to a RFC format and lowers case. 643 * 644 * @param dn 645 * the DN String to be formated 646 * @return a lowercase RFC fromat DN String 647 */ 648 public static String formatToRFC(String dn) { 649 return DN.valueOf(dn).toString().toLowerCase(); 650 } 651 652 /** 653 * Determines if the DN's are equal. 654 * 655 * @param dn1 The first DN. 656 * @param dn2 The second DN. 657 * @return {@code true} if the DN's are equal. 658 */ 659 public static boolean dnEquals(String dn1, String dn2) { 660 DN dnObj1 = DN.valueOf(dn1); 661 DN dnObj2 = DN.valueOf(dn2); 662 return dnObj1.equals(dnObj2); 663 } 664 665 private static class LoggingLBEventListener implements LoadBalancerEventListener { 666 667 public void handleConnectionFactoryOffline(ConnectionFactory factory, LdapException error) { 668 DEBUG.error("Connection factory became offline: " + factory, error); 669 } 670 671 public void handleConnectionFactoryOnline(ConnectionFactory factory) { 672 DEBUG.error("Connection factory became online: " + factory); 673 } 674 } 675 676 /** 677 * Creates a ConnectionFactory from the host string and associated details. The host string can be any of the 678 * following: 679 * <ul> 680 * <li>A plain hostname/IP address</li> 681 * <li>A hostname and port, in the format <code>[host]:[port]</code></li> 682 * <li>A space-separated list of hostnames in priority order, e.g. <code>host1 host2 host3</code></li> 683 * <li> 684 * A space-separated list of hostnames with port numbers in priority order, e.g. 685 * <code>host1:389 host2:50389</code> 686 * </li> 687 * </ul> 688 * If a list of hosts is given, a load balanced {@code ConnectionFactory} is returned. All factories are 689 * pre-authenticated using the supplied credentials. 690 * @param host The host/host-port string. 691 * @param defaultPort The port number to use for hosts that do not specify a port in the string. 692 * @param ssl SSL enabled or not. 693 * @param authDN The DN to bind with. 694 * @param authPasswd The password to bind with. 695 * @param options Any additional options. 696 * @return A connection factory. 697 */ 698 public static ConnectionFactory createFailoverConnectionFactory(String host, int defaultPort, boolean ssl, 699 String authDN, String authPasswd, Options options) { 700 StringTokenizer st = new StringTokenizer(host); 701 String[] hostList = new String[st.countTokens()]; 702 int[] portList = new int[st.countTokens()]; 703 int hostCount = 0; 704 while (st.hasMoreTokens()) { 705 String s = st.nextToken(); 706 int colon = s.indexOf(':'); 707 if (colon > 0) { 708 hostList[hostCount] = s.substring(0, colon); 709 portList[hostCount] = Integer.parseInt(s.substring(colon + 1)); 710 } else { 711 hostList[hostCount] = s; 712 portList[hostCount] = defaultPort; 713 } 714 hostCount++; 715 } 716 717 if (hostCount > 1) { 718 List<ConnectionFactory> factories = new ArrayList<>(); 719 for (int i = 0; i < hostCount; i++) { 720 factories.add(createSingleHostConnectionFactory(hostList[i], portList[i], ssl, authDN, authPasswd, 721 options)); 722 } 723 return loadBalanceFactories(factories, options); 724 } else { 725 return createSingleHostConnectionFactory(hostList[0], portList[0], ssl, authDN, authPasswd, options); 726 } 727 } 728 729 /** 730 * Converts the serverName, port and ssl into LDAPURL and add it into a Set. 731 * 732 * @param serverName The LDAP server name. 733 * @param port The LDAP server port number. 734 * @param isSSL boolean value of true/false for ssl. 735 * @return A set of LDAPURLs based on the passed serverName, port and ssl. 736 */ 737 public static Set<LDAPURL> getLdapUrls(String serverName, int port, boolean isSSL) { 738 return CollectionUtils.asSet(LDAPURL.valueOf(serverName, port, isSSL)); 739 } 740 741 /** 742 * Converts the ldapServers and ssl into LDAPURL and add it into a Set. 743 * 744 * @param ldapServers The LDAP servers in the format of serverName:port 745 * @param isSSL boolean value of true/false for ssl. 746 * @return A set of LDAPURLs based on the passed serverName, port and ssl. 747 */ 748 public static Set<LDAPURL> getLdapUrls(Set<LDAPURL> ldapServers, boolean isSSL) { 749 Set<LDAPURL> ldapUrls = new LinkedHashSet<>(ldapServers.size()); 750 for (LDAPURL url : ldapServers) { 751 ldapUrls.add(LDAPURL.valueOf(url.getHost(), url.getPort(), isSSL)); 752 } 753 return ldapUrls; 754 } 755 756 private static ConnectionFactory createSingleHostConnectionFactory(String host, int port, boolean ssl, 757 String authDN, String authPasswd, Options options) { 758 options = options.set(AUTHN_BIND_REQUEST, LDAPRequests.newSimpleBindRequest(authDN, authPasswd.toCharArray())); 759 if (ssl) { 760 try { 761 options = options.set(LDAPConnectionFactory.SSL_CONTEXT, new SSLContextBuilder().getSSLContext()); 762 options = options.set(SSL_ENABLED_PROTOCOLS, LDAP_SECURE_PROTOCOLS); 763 } catch (GeneralSecurityException gse) { 764 DEBUG.error("An error occurred while creating SSLContext", gse); 765 } 766 767 } 768 return new LDAPConnectionFactory(host, port, options); 769 } 770 771 /** 772 * Return a list of the available TLS protocols to use. 773 * 774 * <p> 775 * All protocols starting with "SSL" are removed. In particular the SSLv2Hello pseudo protocol is removed which will 776 * prevent connections to a server that doesn't have this configured. 777 * </p> 778 * <p> 779 * To override the defaults, set the <em>org.forgerock.openam.ldap.secure.protocol.version</em> system property 780 * to a comma-separated list of desired protocols. 781 * </p> 782 * 783 * @return A list of valid protocol strings. 784 */ 785 private static List<String> getEnabledProtocols() { 786 787 if (TLS_PROTOCOL_VERSION_LIST != null && TLS_PROTOCOL_VERSION_LIST.size() != 0) { 788 if (DEBUG.messageEnabled()) { 789 DEBUG.message("LDAPUtils: LDAPS Protocols specified " + TLS_PROTOCOL_VERSION_LIST.toString()); 790 } 791 return TLS_PROTOCOL_VERSION_LIST; 792 } 793 794 List<String> enabled = new ArrayList<String>(); 795 List<String> protocols = new ArrayList<String>(); 796 try { 797 enabled = CollectionUtils.asList(new SSLContextBuilder().getSSLContext() 798 .createSSLEngine().getEnabledProtocols()); 799 } catch (GeneralSecurityException gse) { 800 DEBUG.error("An error occurred while setting the SSLContext", gse); 801 } 802 803 // exclude SSLv2Hello and SSLv3 804 for (String currentProtocol : enabled) { 805 if (!currentProtocol.startsWith("SSL")) { 806 protocols.add(currentProtocol); 807 } 808 } 809 if (DEBUG.messageEnabled()) { 810 DEBUG.message("LDAPUtils: LDAPS Protocols used " + protocols.toString()); 811 } 812 return protocols; 813 } 814}