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 ForgeRock AS. 015 */ 016package org.forgerock.openam.ldap; 017 018import com.sun.identity.shared.debug.Debug; 019import java.security.GeneralSecurityException; 020import java.util.ArrayList; 021import java.util.Collections; 022import java.util.HashMap; 023import java.util.HashSet; 024import java.util.LinkedHashSet; 025import java.util.List; 026import java.util.Map; 027import java.util.Set; 028import java.util.StringTokenizer; 029import java.util.concurrent.TimeUnit; 030import org.forgerock.i18n.LocalizedIllegalArgumentException; 031import org.forgerock.opendj.ldap.Attribute; 032import org.forgerock.opendj.ldap.ByteString; 033import org.forgerock.opendj.ldap.ConnectionFactory; 034import org.forgerock.opendj.ldap.Connections; 035import org.forgerock.opendj.ldap.DN; 036import org.forgerock.opendj.ldap.ErrorResultException; 037import org.forgerock.opendj.ldap.FailoverLoadBalancingAlgorithm; 038import org.forgerock.opendj.ldap.Filter; 039import org.forgerock.opendj.ldap.LDAPConnectionFactory; 040import org.forgerock.opendj.ldap.LDAPOptions; 041import org.forgerock.opendj.ldap.LoadBalancerEventListener; 042import org.forgerock.opendj.ldap.SSLContextBuilder; 043import org.forgerock.opendj.ldap.SearchScope; 044import org.forgerock.opendj.ldap.requests.Requests; 045 046/** 047 * Utility methods to help interaction with the OpenDJ LDAP SDK. 048 * There are two main ways currently to create connection pools/factories: 049 * <ul> 050 * <li>Providing a set of servers in the format specified in {@link 051 * #prioritizeServers(java.util.Set, java.lang.String, java.lang.String)}, which will be prioritized based on the 052 * current server's server ID/site ID.</li> 053 * <li>Providing a set of LDAPURLs, which are already considered as "prioritized".</li> 054 * </ul> 055 * In case the configuration provides the possibility to assign LDAP servers to OpenAM servers/sites, then either you 056 * can prioritize manually (if the logic differs from this implementation) and create the corresponding {@link LDAPURL} 057 * objects, or you can pass in the list to the newPrioritized* methods. 058 * 059 * @author Peter Major 060 * @supported.all.api 061 */ 062public class LDAPUtils { 063 064 private static final String LDAP_SCOPE_BASE = "SCOPE_BASE"; 065 private static final String LDAP_SCOPE_ONE = "SCOPE_ONE"; 066 private static final String LDAP_SCOPE_SUB = "SCOPE_SUB"; 067 private static final Map<String, SearchScope> scopes; 068 private static final Debug DEBUG = Debug.getInstance("LDAPUtils"); 069 private static final int DEFAULT_HEARTBEAT_TIMEOUT_MS = 500; 070 071 static { 072 Map<String, SearchScope> mappings = new HashMap<String, SearchScope>(3); 073 mappings.put(LDAP_SCOPE_BASE, SearchScope.BASE_OBJECT); 074 mappings.put(LDAP_SCOPE_ONE, SearchScope.SINGLE_LEVEL); 075 mappings.put(LDAP_SCOPE_SUB, SearchScope.WHOLE_SUBTREE); 076 scopes = Collections.unmodifiableMap(mappings); 077 } 078 079 private LDAPUtils() { 080 } 081 082 /** 083 * Based on the incoming parameters prioritizes the LDAP server list, then creates a connection pool that is 084 * capable to failover to the servers defined in case there is an error. 085 * 086 * @param servers The set of servers in the format defined in {@link 087 * #prioritizeServers(java.util.Set, java.lang.String, java.lang.String)}. 088 * @param hostServerId The server ID for this OpenAM server. 089 * @param hostSiteId The site ID for this OpenAM server. 090 * @param username The directory user's DN. May be null if this is an anonymous connection. 091 * @param password The directory user's password. 092 * @param maxSize The max size of the created pool. 093 * @param heartBeatInterval The interval for sending out heartbeat requests. 094 * @param heartBeatTimeUnit The timeunit for the heartbeat interval. 095 * @param ldapOptions Additional LDAP settings used to create the pool. 096 * @return A failover loadbalanced authenticated/anonymous connection pool, which may also send heartbeat requests. 097 */ 098 public static ConnectionFactory newPrioritizedFailoverConnectionPool(Set<String> servers, 099 String hostServerId, 100 String hostSiteId, 101 String username, 102 char[] password, 103 int maxSize, 104 int heartBeatInterval, 105 String heartBeatTimeUnit, 106 LDAPOptions ldapOptions) { 107 return newFailoverConnectionPool(prioritizeServers(servers, hostServerId, hostSiteId), 108 username, password, maxSize, heartBeatInterval, heartBeatTimeUnit, ldapOptions); 109 } 110 111 /** 112 * Creates a new connection pool that is capable to failover to the servers defined in case there is an error. 113 * 114 * @param servers The set of LDAP URLs that will be used to set up the connection factory. 115 * @param username The directory user's DN. May be null if this is an anonymous connection. 116 * @param password The directory user's password. 117 * @param maxSize The max size of the created pool. 118 * @param heartBeatInterval The interval for sending out heartbeat requests. 119 * @param heartBeatTimeUnit The timeunit for the heartbeat interval. 120 * @param ldapOptions Additional LDAP settings used to create the pool 121 * @return A failover loadbalanced authenticated/anonymous connection pool, which may also send heartbeat requests. 122 */ 123 public static ConnectionFactory newFailoverConnectionPool(Set<LDAPURL> servers, 124 String username, 125 char[] password, 126 int maxSize, 127 int heartBeatInterval, 128 String heartBeatTimeUnit, 129 LDAPOptions ldapOptions) { 130 List<ConnectionFactory> factories = new ArrayList<ConnectionFactory>(servers.size()); 131 for (LDAPURL ldapurl : servers) { 132 ConnectionFactory cf = Connections.newFixedConnectionPool( 133 newConnectionFactory(ldapurl, username, password, heartBeatInterval, heartBeatTimeUnit, 134 ldapOptions), maxSize); 135 factories.add(cf); 136 } 137 138 return loadBalanceFactories(factories); 139 } 140 141 /** 142 * Based on the incoming parameters prioritizes the LDAP server list, then creates a connection factory that is 143 * capable to failover to the servers defined in case there is an error. 144 * 145 * @param servers The set of servers in the format defined in {@link 146 * #prioritizeServers(java.util.Set, java.lang.String, java.lang.String)}. 147 * @param hostServerId The server ID for this OpenAM server. 148 * @param hostSiteId The site ID for this OpenAM server. 149 * @param username The directory user's DN. May be null if this is an anonymous connection. 150 * @param password The directory user's password. 151 * @param heartBeatInterval The interval for sending out heartbeat requests. 152 * @param heartBeatTimeUnit The timeunit for the heartbeat interval. 153 * @param options Additional LDAP settings used to create the connection factory. 154 * @return A failover loadbalanced authenticated/anonymous connection factory, which may also send heartbeat 155 * requests. 156 */ 157 public static ConnectionFactory newPrioritizedFailoverConnectionFactory(Set<String> servers, 158 String hostServerId, 159 String hostSiteId, 160 String username, 161 char[] password, 162 int heartBeatInterval, 163 String heartBeatTimeUnit, 164 LDAPOptions options) { 165 return newFailoverConnectionFactory(prioritizeServers(servers, hostServerId, hostSiteId), 166 username, password, heartBeatInterval, heartBeatTimeUnit, options); 167 } 168 169 /** 170 * Creates a new connection factory that is capable to failover to the servers defined in case there is an error. 171 * 172 * @param servers The set of LDAP URLs that will be used to set up the connection factory. 173 * @param username The directory user's DN. May be null if this is an anonymous connection. 174 * @param password The directory user's password. 175 * @param heartBeatInterval The interval for sending out heartbeat requests. 176 * @param heartBeatTimeUnit The timeunit for the heartbeat interval. 177 * @param ldapOptions Additional LDAP settings used to create the connection factory. 178 * @return A failover loadbalanced authenticated/anonymous connection factory, which may also send heartbeat 179 * requests. 180 */ 181 public static ConnectionFactory newFailoverConnectionFactory(Set<LDAPURL> servers, 182 String username, 183 char[] password, 184 int heartBeatInterval, 185 String heartBeatTimeUnit, 186 LDAPOptions ldapOptions) { 187 List<ConnectionFactory> factories = new ArrayList<ConnectionFactory>(servers.size()); 188 for (LDAPURL ldapurl : servers) { 189 factories.add(newConnectionFactory(ldapurl, username, password, heartBeatInterval, heartBeatTimeUnit, 190 ldapOptions)); 191 } 192 return loadBalanceFactories(factories); 193 } 194 195 /** 196 * Creates a new connection factory based on the provided parameters. 197 * 198 * @param ldapurl The address of the LDAP server. 199 * @param username The directory user's DN. May be null if this is an anonymous connection. 200 * @param password The directory user's password. 201 * @param heartBeatInterval The interval for sending out heartbeat requests. 202 * @param heartBeatTimeUnit The timeunit for the heartbeat interval. 203 * @param ldapOptions Additional LDAP settings used to create the connection factory. 204 * @return An authenticated/anonymous connection factory, which may also send heartbeat requests. 205 */ 206 private static ConnectionFactory newConnectionFactory(LDAPURL ldapurl, 207 String username, 208 char[] password, 209 int heartBeatInterval, 210 String heartBeatTimeUnit, 211 LDAPOptions ldapOptions) { 212 Boolean ssl = ldapurl.isSSL(); 213 if (ssl != null && ssl.booleanValue()) { 214 try { 215 //Creating a defensive copy of ldapOptions to handle the case when a mixture of SSL/non-SSL connections 216 //needs to be established. 217 ldapOptions = new LDAPOptions(ldapOptions).setSSLContext(new SSLContextBuilder().getSSLContext()); 218 } catch (GeneralSecurityException gse) { 219 DEBUG.error("An error occurred while creating SSLContext", gse); 220 } 221 } 222 ConnectionFactory cf = new LDAPConnectionFactory(ldapurl.getHost(), ldapurl.getPort(), ldapOptions); 223 if (heartBeatInterval > 0) { 224 TimeUnit unit = TimeUnit.valueOf(heartBeatTimeUnit.toUpperCase()); 225 cf = Connections.newHeartBeatConnectionFactory(cf, unit.toMillis(heartBeatInterval), 226 DEFAULT_HEARTBEAT_TIMEOUT_MS, TimeUnit.MILLISECONDS); 227 } 228 if (username != null) { 229 cf = Connections.newAuthenticatedConnectionFactory(cf, Requests.newSimpleBindRequest(username, password)); 230 } 231 return cf; 232 } 233 234 private static ConnectionFactory loadBalanceFactories(List<ConnectionFactory> factories) { 235 return Connections.newLoadBalancer(new FailoverLoadBalancingAlgorithm(factories, 236 new LoggingLBEventListener())); 237 } 238 239 /** 240 * Prioritizes the incoming LDAP servers based on their assigned servers/sites. 241 * The format of the server list can be either one of the followings: 242 * <ul> 243 * <li><code>host:port</code> - The LDAP server has no preferred 244 * server/site</li> 245 * <li><code>host:port|serverid</code> - The LDAP server should be mainly 246 * used by an OpenAM instance with the same serverid</li> 247 * <li><code>host:port|serverid|siteid</code> - The LDAP server should be 248 * mainly used by an OpenAM instance with the same serverid or with the same 249 * siteid</li> 250 * </ul> 251 * The resulting priority list will have the following order: 252 * <ul> 253 * <li>servers that are linked with this server</li> 254 * <li>servers that are linked with the current site</li> 255 * <li>any other server that did not match in the same order as they were defined</li> 256 * </ul> 257 * 258 * @param servers The Set of servers that needs to be prioritized in the previously described format. 259 * @param hostServerId This server's ID. 260 * @param hostSiteId This server's site ID. 261 * @return The prioritized Set of LDAP URLs that can be used to create connection factories. 262 */ 263 public static Set<LDAPURL> prioritizeServers(Set<String> servers, String hostServerId, String hostSiteId) { 264 Set<LDAPURL> ldapServers = new LinkedHashSet<LDAPURL>(servers.size()); 265 Set<LDAPURL> serverDefined = new LinkedHashSet<LDAPURL>(servers.size()); 266 Set<LDAPURL> siteDefined = new LinkedHashSet<LDAPURL>(servers.size()); 267 Set<LDAPURL> nonMatchingServers = new LinkedHashSet<LDAPURL>(servers.size()); 268 for (String server : servers) { 269 StringTokenizer tokenizer = new StringTokenizer(server, "|"); 270 String ldapUrl = tokenizer.nextToken(); 271 String assignedServerId = ""; 272 String assignedSiteId = ""; 273 274 if (tokenizer.hasMoreTokens()) { 275 assignedServerId = tokenizer.nextToken(); 276 } 277 if (tokenizer.hasMoreTokens()) { 278 assignedSiteId = tokenizer.nextToken(); 279 } 280 if (!assignedServerId.isEmpty() && assignedServerId.equals(hostServerId)) { 281 serverDefined.add(LDAPURL.valueOf(ldapUrl)); 282 } else if (!assignedSiteId.isEmpty() && assignedSiteId.equals(hostSiteId)) { 283 siteDefined.add(LDAPURL.valueOf(ldapUrl)); 284 } else { 285 nonMatchingServers.add(LDAPURL.valueOf(ldapUrl)); 286 } 287 } 288 //Let's add them in the order of priority to the ldapServers set, this way the most appropriate servers should 289 //be at the beginning of the list and towards the end of the list are the possibly most remote servers. 290 ldapServers.addAll(serverDefined); 291 ldapServers.addAll(siteDefined); 292 ldapServers.addAll(nonMatchingServers); 293 return ldapServers; 294 } 295 296 /** 297 * Converts string representation of scope (as defined in the configuration) to the corresponding 298 * {@link SearchScope} object. 299 * 300 * @param scope the string representation of the scope. 301 * @param defaultScope in case the coversion fail this default scope should be returned. 302 * @return the corresponding {@link SearchScope} object. 303 */ 304 public static SearchScope getSearchScope(String scope, SearchScope defaultScope) { 305 SearchScope searchScope = scopes.get(scope); 306 return searchScope == null ? defaultScope : searchScope; 307 } 308 309 /** 310 * Parses the incoming filter, and in case of failure falls back to the default filter. 311 * 312 * @param filter The filter that needs to be parsed. 313 * @param defaultFilter If the parsing fails, this will be returned. 314 * @return The parsed Filter object, or the default Filter, if the parse failed. 315 */ 316 public static Filter parseFilter(String filter, Filter defaultFilter) { 317 try { 318 return filter == null ? defaultFilter : Filter.valueOf(filter); 319 } catch (LocalizedIllegalArgumentException liae) { 320 DEBUG.error("Unable to construct Filter from " + filter + " -> " + liae.getMessage() 321 + "\nFalling back to " + defaultFilter.toString()); 322 } 323 return defaultFilter; 324 } 325 326 /** 327 * Returns the RDN without the attribute name from the passed in {@link DN} object, for example: 328 * <code>uid=demo,ou=people,dc=example,dc=com</code> will return <code>demo</code>. 329 * 330 * @param dn The DN that we need the name of. 331 * @return The RDN of the DN without the attribute name. 332 */ 333 public static String getName(DN dn) { 334 return dn.rdn().getFirstAVA().getAttributeValue().toString(); 335 } 336 337 /** 338 * Converts the Attribute to an attribute name, 2-dimensional byte array map and adds it to the map passed in. 339 * The first dimension of the byte array separates the different values, the second dimension holds the actual 340 * value. 341 * 342 * @param attribute The attribute that needs to be converted. 343 * @param map The map where the converted attribute is added to. 344 */ 345 public static void addAttributeToMapAsByteArray(Attribute attribute, Map<String, byte[][]> map) { 346 byte[][] values = new byte[attribute.size()][]; 347 int counter = 0; 348 for (ByteString byteString : attribute) { 349 byte[] bytes = byteString.toByteArray(); 350 values[counter++] = bytes; 351 } 352 map.put(attribute.getAttributeDescriptionAsString(), values); 353 } 354 355 /** 356 * Converts the Attribute to an attribute name, set of String values map and adds it to the map passed in. 357 * 358 * @param attribute The attribute that needs to be converted. 359 * @param map The map where the converted attribute is added to. 360 */ 361 public static void addAttributeToMapAsString(Attribute attribute, Map<String, Set<String>> map) { 362 map.put(attribute.getAttributeDescriptionAsString(), getAttributeValuesAsStringSet(attribute)); 363 } 364 365 /** 366 * Converts all the attribute values to a String Set. 367 * 368 * @param attribute the attribute to be converted. 369 * @return A Set of String representations of the Attribute values. 370 */ 371 public static Set<String> getAttributeValuesAsStringSet(Attribute attribute) { 372 Set<String> values = new HashSet<String>(attribute.size()); 373 for (ByteString byteString : attribute) { 374 values.add(byteString.toString()); 375 } 376 return values; 377 } 378 379 /** 380 * Converts the incoming set of URLs to {@link LDAPURL} instances and returns them as a set. The iteration order 381 * of the originally passed in Set is retained. 382 * 383 * @param servers The LDAP server URLs that needs to be converted to {@link LDAPURL} instances. 384 * @return A set of LDAPURLs corresponding to the passed in URLs. 385 */ 386 public static Set<LDAPURL> convertToLDAPURLs(Set<String> servers) { 387 if (servers == null) { 388 return new LinkedHashSet<LDAPURL>(0); 389 } else { 390 Set<LDAPURL> ret = new LinkedHashSet<LDAPURL>(servers.size()); 391 for (String server : servers) { 392 ret.add(LDAPURL.valueOf(server)); 393 } 394 return ret; 395 } 396 } 397 398 private static class LoggingLBEventListener implements LoadBalancerEventListener { 399 400 public void handleConnectionFactoryOffline(ConnectionFactory factory, ErrorResultException error) { 401 DEBUG.error("Connection factory became offline: " + factory, error); 402 } 403 404 public void handleConnectionFactoryOnline(ConnectionFactory factory) { 405 DEBUG.error("Connection factory became online: " + factory); 406 } 407 } 408}