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.