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