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}