xref: /OpenGrok/plugins/src/main/java/opengrok/auth/plugin/ldap/LdapFacade.java (revision 05aaab8c8c0f8b98f21471783e0303da92bd6090)
1b28a5538SAdam Hornacek /*
2b28a5538SAdam Hornacek  * CDDL HEADER START
3b28a5538SAdam Hornacek  *
4b28a5538SAdam Hornacek  * The contents of this file are subject to the terms of the
5b28a5538SAdam Hornacek  * Common Development and Distribution License (the "License").
6b28a5538SAdam Hornacek  * You may not use this file except in compliance with the License.
7b28a5538SAdam Hornacek  *
8b28a5538SAdam Hornacek  * See LICENSE.txt included in this distribution for the specific
9b28a5538SAdam Hornacek  * language governing permissions and limitations under the License.
10b28a5538SAdam Hornacek  *
11b28a5538SAdam Hornacek  * When distributing Covered Code, include this CDDL HEADER in each
12b28a5538SAdam Hornacek  * file and include the License file at LICENSE.txt.
13b28a5538SAdam Hornacek  * If applicable, add the following below this CDDL HEADER, with the
14b28a5538SAdam Hornacek  * fields enclosed by brackets "[]" replaced with your own identifying
15b28a5538SAdam Hornacek  * information: Portions Copyright [yyyy] [name of copyright owner]
16b28a5538SAdam Hornacek  *
17b28a5538SAdam Hornacek  * CDDL HEADER END
18b28a5538SAdam Hornacek  */
19b28a5538SAdam Hornacek 
20b28a5538SAdam Hornacek /*
21cbd2aafbSVladimir Kotal  * Copyright (c) 2016, 2020, Oracle and/or its affiliates. All rights reserved.
22b28a5538SAdam Hornacek  */
23b28a5538SAdam Hornacek package opengrok.auth.plugin.ldap;
24b28a5538SAdam Hornacek 
25b3a15424SVladimir Kotal import java.time.Duration;
26b3a15424SVladimir Kotal import java.time.Instant;
27b28a5538SAdam Hornacek import java.util.ArrayList;
28b28a5538SAdam Hornacek import java.util.HashMap;
29b28a5538SAdam Hornacek import java.util.List;
30b28a5538SAdam Hornacek import java.util.Map;
31b28a5538SAdam Hornacek import java.util.Set;
32b28a5538SAdam Hornacek import java.util.TreeSet;
33b28a5538SAdam Hornacek import java.util.logging.Level;
34b28a5538SAdam Hornacek import java.util.logging.Logger;
35b28a5538SAdam Hornacek import javax.naming.CommunicationException;
36b28a5538SAdam Hornacek import javax.naming.NameNotFoundException;
37b28a5538SAdam Hornacek import javax.naming.NamingEnumeration;
38b28a5538SAdam Hornacek import javax.naming.NamingException;
39b28a5538SAdam Hornacek import javax.naming.SizeLimitExceededException;
40b28a5538SAdam Hornacek import javax.naming.TimeLimitExceededException;
41b28a5538SAdam Hornacek import javax.naming.directory.Attribute;
42b28a5538SAdam Hornacek import javax.naming.directory.Attributes;
43b28a5538SAdam Hornacek import javax.naming.directory.SearchControls;
44b28a5538SAdam Hornacek import javax.naming.directory.SearchResult;
45b28a5538SAdam Hornacek 
46b3a15424SVladimir Kotal import io.micrometer.core.instrument.Timer;
47b28a5538SAdam Hornacek import opengrok.auth.plugin.configuration.Configuration;
48b28a5538SAdam Hornacek import opengrok.auth.plugin.util.WebHook;
49b28a5538SAdam Hornacek import opengrok.auth.plugin.util.WebHooks;
50b3a15424SVladimir Kotal import org.opengrok.indexer.Metrics;
51b28a5538SAdam Hornacek 
52b28a5538SAdam Hornacek public class LdapFacade extends AbstractLdapProvider {
53b28a5538SAdam Hornacek 
54b28a5538SAdam Hornacek     private static final Logger LOGGER = Logger.getLogger(LdapFacade.class.getName());
55b28a5538SAdam Hornacek 
56b28a5538SAdam Hornacek     /**
57ff44f24aSAdam Hornáček      * Default LDAP filter.
58b28a5538SAdam Hornacek      */
59b28a5538SAdam Hornacek     private static final String LDAP_FILTER = "objectclass=*";
60b28a5538SAdam Hornacek 
61b28a5538SAdam Hornacek     /**
62ff44f24aSAdam Hornáček      * Default timeout for retrieving the results.
63b28a5538SAdam Hornacek      */
64b28a5538SAdam Hornacek     private static final int LDAP_SEARCH_TIMEOUT = 5000; // ms
65b28a5538SAdam Hornacek 
66b28a5538SAdam Hornacek     /**
67ff44f24aSAdam Hornáček      * Default limit of result traversal.
68b28a5538SAdam Hornacek      *
69b28a5538SAdam Hornacek      * @see
70b28a5538SAdam Hornacek      * <a href="https://docs.oracle.com/javase/7/docs/api/javax/naming/directory/SearchControls.html#setCountLimit%28long%29">SearchControls</a>
71b28a5538SAdam Hornacek      *
72b28a5538SAdam Hornacek      * basically it does not mean that the server must send at most this number
73b28a5538SAdam Hornacek      * of results, but that the program should not iterate more than this number
74b28a5538SAdam Hornacek      * over the results.
75b28a5538SAdam Hornacek      */
76b28a5538SAdam Hornacek     private static final int LDAP_COUNT_LIMIT = 100;
77b28a5538SAdam Hornacek 
78b28a5538SAdam Hornacek     /**
79b28a5538SAdam Hornacek      * When there is no active server in the pool, the facade waits this time
80b28a5538SAdam Hornacek      * interval (since the last failure) until it tries the servers again.
81b28a5538SAdam Hornacek      *
82b28a5538SAdam Hornacek      * This should avoid heavy load to the LDAP servers when they are all
83b28a5538SAdam Hornacek      * broken/not responding/down - pool waiting.
84b28a5538SAdam Hornacek      *
85b28a5538SAdam Hornacek      * Also each server uses this same interval since its last failure - per
86b28a5538SAdam Hornacek      * server waiting.
87b28a5538SAdam Hornacek      */
88b28a5538SAdam Hornacek     private int interval = 10 * 1000; // ms
89b28a5538SAdam Hornacek 
90b28a5538SAdam Hornacek     /**
91ff44f24aSAdam Hornáček      * LDAP search base.
92b28a5538SAdam Hornacek      */
93b28a5538SAdam Hornacek     private String searchBase;
94b28a5538SAdam Hornacek 
95b28a5538SAdam Hornacek     /**
96b28a5538SAdam Hornacek      * Server pool.
97b28a5538SAdam Hornacek      */
98b28a5538SAdam Hornacek     private List<LdapServer> servers = new ArrayList<>();
99b28a5538SAdam Hornacek 
100b28a5538SAdam Hornacek     /**
101ff44f24aSAdam Hornáček      * server webHooks.
102b28a5538SAdam Hornacek      */
103b28a5538SAdam Hornacek     private WebHooks webHooks;
104b28a5538SAdam Hornacek 
105b28a5538SAdam Hornacek     private SearchControls controls;
10614c8a3ffSVladimir Kotal     private int actualServer = -1;
107b28a5538SAdam Hornacek     private long errorTimestamp = 0;
108b28a5538SAdam Hornacek     private boolean reported = false;
109b28a5538SAdam Hornacek 
110b3a15424SVladimir Kotal     private final Timer ldapLookupTimer = Timer.builder("ldap.latency").
111b3a15424SVladimir Kotal             description("LDAP lookup latency").
112b3a15424SVladimir Kotal             register(Metrics.getRegistry());
113b3a15424SVladimir Kotal 
114b28a5538SAdam Hornacek     /**
115b28a5538SAdam Hornacek      * Interface for converting LDAP results into user defined types.
116b28a5538SAdam Hornacek      *
117b28a5538SAdam Hornacek      * @param <T> the type of the result
118b28a5538SAdam Hornacek      */
119b28a5538SAdam Hornacek     private interface AttributeMapper<T> {
120b28a5538SAdam Hornacek 
mapFromAttributes(Attributes attr)12117deb9edSVladimir Kotal         T mapFromAttributes(Attributes attr) throws NamingException;
122b28a5538SAdam Hornacek     }
123b28a5538SAdam Hornacek 
124b28a5538SAdam Hornacek     /**
125b28a5538SAdam Hornacek      * Transforms the attributes to the set of strings used for authorization.
126b28a5538SAdam Hornacek      *
127b28a5538SAdam Hornacek      * Currently this behaves like it get all records stored in
128b28a5538SAdam Hornacek      */
129b28a5538SAdam Hornacek     private static class ContentAttributeMapper implements AttributeMapper<Map<String, Set<String>>> {
130b28a5538SAdam Hornacek 
131b28a5538SAdam Hornacek         private final String[] values;
132b28a5538SAdam Hornacek 
133b28a5538SAdam Hornacek         /**
134b28a5538SAdam Hornacek          * Create a new mapper which retrieves the given values in the resulting
135b28a5538SAdam Hornacek          * set.
136b28a5538SAdam Hornacek          *
137b28a5538SAdam Hornacek          * @param values include these values in the result
138b28a5538SAdam Hornacek          */
ContentAttributeMapper(String[] values)139b28a5538SAdam Hornacek         ContentAttributeMapper(String[] values) {
140b28a5538SAdam Hornacek             this.values = values;
141b28a5538SAdam Hornacek         }
142b28a5538SAdam Hornacek 
143b28a5538SAdam Hornacek         @Override
mapFromAttributes(Attributes attrs)14417deb9edSVladimir Kotal         public Map<String, Set<String>> mapFromAttributes(Attributes attrs) throws NamingException {
145b28a5538SAdam Hornacek             Map<String, Set<String>> map = new HashMap<>();
146b28a5538SAdam Hornacek 
147b28a5538SAdam Hornacek             if (values == null) {
14817deb9edSVladimir Kotal                 for (NamingEnumeration<? extends Attribute> attrEnum = attrs.getAll(); attrEnum.hasMore();) {
149b28a5538SAdam Hornacek                     Attribute attr = attrEnum.next();
150b28a5538SAdam Hornacek 
1510632fab4SVladimir Kotal                     addAttrToMap(map, attr);
152b28a5538SAdam Hornacek                 }
153b28a5538SAdam Hornacek             } else {
154ff44f24aSAdam Hornáček                 for (String value : values) {
155ff44f24aSAdam Hornáček                     Attribute attr = attrs.get(value);
156b28a5538SAdam Hornacek 
157b28a5538SAdam Hornacek                     if (attr == null) {
158b28a5538SAdam Hornacek                         continue;
159b28a5538SAdam Hornacek                     }
160b28a5538SAdam Hornacek 
1610632fab4SVladimir Kotal                     addAttrToMap(map, attr);
1620632fab4SVladimir Kotal                 }
1630632fab4SVladimir Kotal             }
1640632fab4SVladimir Kotal 
1650632fab4SVladimir Kotal             return map;
1660632fab4SVladimir Kotal         }
1670632fab4SVladimir Kotal 
addAttrToMap(Map<String, Set<String>> map, Attribute attr)1680632fab4SVladimir Kotal         private void addAttrToMap(Map<String, Set<String>> map, Attribute attr) throws NamingException {
169b28a5538SAdam Hornacek             if (!map.containsKey(attr.getID())) {
170b28a5538SAdam Hornacek                 map.put(attr.getID(), new TreeSet<>());
171b28a5538SAdam Hornacek             }
172b28a5538SAdam Hornacek 
173b28a5538SAdam Hornacek             final Set<String> valueSet = map.get(attr.getID());
174b28a5538SAdam Hornacek 
175b28a5538SAdam Hornacek             for (NamingEnumeration<?> values = attr.getAll(); values.hasMore(); ) {
176b28a5538SAdam Hornacek                 valueSet.add((String) values.next());
177b28a5538SAdam Hornacek             }
178b28a5538SAdam Hornacek         }
179b28a5538SAdam Hornacek     }
180b28a5538SAdam Hornacek 
LdapFacade(Configuration cfg)181b28a5538SAdam Hornacek     public LdapFacade(Configuration cfg) {
182d0624dbbSVladimir Kotal         setServers(cfg.getServers(), cfg.getConnectTimeout(), cfg.getReadTimeout());
183b28a5538SAdam Hornacek         setInterval(cfg.getInterval());
184b28a5538SAdam Hornacek         setSearchBase(cfg.getSearchBase());
185b28a5538SAdam Hornacek         setWebHooks(cfg.getWebHooks());
186e5567996SVladimir Kotal 
187e5567996SVladimir Kotal         // Anti-pattern: do some non trivial stuff in the constructor.
188b28a5538SAdam Hornacek         prepareSearchControls(cfg.getSearchTimeout(), cfg.getCountLimit());
189b28a5538SAdam Hornacek         prepareServers();
190b28a5538SAdam Hornacek     }
191b28a5538SAdam Hornacek 
setWebHooks(WebHooks webHooks)192b28a5538SAdam Hornacek     private void setWebHooks(WebHooks webHooks) {
193b28a5538SAdam Hornacek         this.webHooks = webHooks;
194b28a5538SAdam Hornacek     }
195b28a5538SAdam Hornacek 
196b28a5538SAdam Hornacek     /**
19714c8a3ffSVladimir Kotal      * Go through all servers in the pool and record the first working.
198b28a5538SAdam Hornacek      */
prepareServers()19914c8a3ffSVladimir Kotal     void prepareServers() {
20043e14d01SVladimir Kotal         LOGGER.log(Level.FINER, "checking servers for {0}", this);
201b28a5538SAdam Hornacek         for (int i = 0; i < servers.size(); i++) {
202b28a5538SAdam Hornacek             LdapServer server = servers.get(i);
20314c8a3ffSVladimir Kotal             if (server.isWorking() && actualServer == -1) {
204b28a5538SAdam Hornacek                 actualServer = i;
205b28a5538SAdam Hornacek             }
206b28a5538SAdam Hornacek         }
20743e14d01SVladimir Kotal 
20843e14d01SVladimir Kotal         // Close the connections to the inactive servers.
20943e14d01SVladimir Kotal         LOGGER.log(Level.FINER, "closing unused servers");
21043e14d01SVladimir Kotal         for (int i = 0; i < servers.size(); i++) {
21143e14d01SVladimir Kotal             if (i != actualServer) {
21243e14d01SVladimir Kotal                 servers.get(i).close();
21343e14d01SVladimir Kotal             }
21443e14d01SVladimir Kotal         }
21543e14d01SVladimir Kotal 
216*05aaab8cSVladimir Kotal         if (LOGGER.isLoggable(Level.FINER)) {
2178ef94609SVladimir Kotal             LOGGER.log(Level.FINER, String.format("server check done (current server: %s)",
2188ef94609SVladimir Kotal                     actualServer != -1 ? servers.get(actualServer) : "N/A"));
219b28a5538SAdam Hornacek         }
220*05aaab8cSVladimir Kotal     }
221b28a5538SAdam Hornacek 
222b28a5538SAdam Hornacek     /**
223b28a5538SAdam Hornacek      * Closes all available servers.
224b28a5538SAdam Hornacek      */
225b28a5538SAdam Hornacek     @Override
close()226b28a5538SAdam Hornacek     public void close() {
227ff44f24aSAdam Hornáček         for (LdapServer server : servers) {
228b28a5538SAdam Hornacek             server.close();
229b28a5538SAdam Hornacek         }
230b28a5538SAdam Hornacek     }
231b28a5538SAdam Hornacek 
getServers()232b28a5538SAdam Hornacek     public List<LdapServer> getServers() {
233b28a5538SAdam Hornacek         return servers;
234b28a5538SAdam Hornacek     }
235b28a5538SAdam Hornacek 
setServers(List<LdapServer> servers, int connectTimeout, int readTimeout)236d0624dbbSVladimir Kotal     public LdapFacade setServers(List<LdapServer> servers, int connectTimeout, int readTimeout) {
237b28a5538SAdam Hornacek         this.servers = servers;
238d0624dbbSVladimir Kotal         // Inherit timeout values from server pool configuration.
239b28a5538SAdam Hornacek         for (LdapServer server : servers) {
240d0624dbbSVladimir Kotal             if (server.getConnectTimeout() == 0 && connectTimeout != 0) {
241d0624dbbSVladimir Kotal                 server.setConnectTimeout(connectTimeout);
242d0624dbbSVladimir Kotal             }
243d0624dbbSVladimir Kotal             if (server.getReadTimeout() == 0 && readTimeout != 0) {
244d0624dbbSVladimir Kotal                 server.setReadTimeout(readTimeout);
245b28a5538SAdam Hornacek             }
246b28a5538SAdam Hornacek         }
247b28a5538SAdam Hornacek         return this;
248b28a5538SAdam Hornacek     }
249b28a5538SAdam Hornacek 
getInterval()250b28a5538SAdam Hornacek     public int getInterval() {
251b28a5538SAdam Hornacek         return interval;
252b28a5538SAdam Hornacek     }
253b28a5538SAdam Hornacek 
setInterval(int interval)254b28a5538SAdam Hornacek     public void setInterval(int interval) {
255b28a5538SAdam Hornacek         this.interval = interval;
256ff44f24aSAdam Hornáček         for (LdapServer server : servers) {
257b28a5538SAdam Hornacek             server.setInterval(interval);
258b28a5538SAdam Hornacek         }
259b28a5538SAdam Hornacek     }
260b28a5538SAdam Hornacek 
getSearchBase()261b28a5538SAdam Hornacek     public String getSearchBase() {
262b28a5538SAdam Hornacek         return searchBase;
263b28a5538SAdam Hornacek     }
264b28a5538SAdam Hornacek 
setSearchBase(String base)265b28a5538SAdam Hornacek     public void setSearchBase(String base) {
266b28a5538SAdam Hornacek         this.searchBase = base;
267b28a5538SAdam Hornacek     }
268b28a5538SAdam Hornacek 
269b28a5538SAdam Hornacek     @Override
isConfigured()270b28a5538SAdam Hornacek     public boolean isConfigured() {
2718ae5e262SAdam Hornacek         return servers != null && !servers.isEmpty() && searchBase != null && actualServer != -1;
272b28a5538SAdam Hornacek     }
273b28a5538SAdam Hornacek 
274b28a5538SAdam Hornacek     /**
2750e7ff20bSVladimir Kotal      * Get LDAP attributes.
276b28a5538SAdam Hornacek      *
27753c33ae5SVladimir Kotal      * @param dn LDAP DN attribute. If @{code null} then {@code searchBase} will be used.
278b28a5538SAdam Hornacek      * @param filter LDAP filter to use. If @{code null} then @{link LDAP_FILTER} will be used.
279b28a5538SAdam Hornacek      * @param values match these LDAP values
280b28a5538SAdam Hornacek      *
281b28a5538SAdam Hornacek      * @return set of strings describing the user's attributes
282b28a5538SAdam Hornacek      */
283b28a5538SAdam Hornacek     @Override
lookupLdapContent(String dn, String filter, String[] values)28417deb9edSVladimir Kotal     public LdapSearchResult<Map<String, Set<String>>> lookupLdapContent(String dn, String filter, String[] values) throws LdapException {
285b28a5538SAdam Hornacek 
286b28a5538SAdam Hornacek         return lookup(
28753c33ae5SVladimir Kotal                 dn != null ? dn : getSearchBase(),
288b28a5538SAdam Hornacek                 filter == null ? LDAP_FILTER : filter,
289b28a5538SAdam Hornacek                 values,
290b28a5538SAdam Hornacek                 new ContentAttributeMapper(values));
291b28a5538SAdam Hornacek     }
292b28a5538SAdam Hornacek 
prepareSearchControls(int ldapTimeout, int ldapCountLimit)293b28a5538SAdam Hornacek     private SearchControls prepareSearchControls(int ldapTimeout, int ldapCountLimit) {
294b28a5538SAdam Hornacek         controls = new SearchControls();
295b28a5538SAdam Hornacek         controls.setSearchScope(SearchControls.SUBTREE_SCOPE);
296b28a5538SAdam Hornacek         controls.setTimeLimit(ldapTimeout > 0 ? ldapTimeout : LDAP_SEARCH_TIMEOUT);
297b28a5538SAdam Hornacek         controls.setCountLimit(ldapCountLimit > 0 ? ldapCountLimit : LDAP_COUNT_LIMIT);
298b28a5538SAdam Hornacek 
299b28a5538SAdam Hornacek         return controls;
300b28a5538SAdam Hornacek     }
301b28a5538SAdam Hornacek 
getSearchControls()302b28a5538SAdam Hornacek     public SearchControls getSearchControls() {
303b28a5538SAdam Hornacek         return controls;
304b28a5538SAdam Hornacek     }
305b28a5538SAdam Hornacek 
306b28a5538SAdam Hornacek     /**
307ff44f24aSAdam Hornáček      * Lookups the LDAP server for content.
308b28a5538SAdam Hornacek      *
309b28a5538SAdam Hornacek      * @param <T> return type
310b28a5538SAdam Hornacek      * @param dn search base for the query
311b28a5538SAdam Hornacek      * @param filter LDAP filter for the query
312b28a5538SAdam Hornacek      * @param attributes returning LDAP attributes
313b28a5538SAdam Hornacek      * @param mapper mapper class implementing @code{AttributeMapper} closed
314b28a5538SAdam Hornacek      *
315b28a5538SAdam Hornacek      * @return results transformed with mapper
316b28a5538SAdam Hornacek      */
lookup(String dn, String filter, String[] attributes, AttributeMapper<T> mapper)31717deb9edSVladimir Kotal     private <T> LdapSearchResult<T> lookup(String dn, String filter, String[] attributes, AttributeMapper<T> mapper) throws LdapException {
318b3a15424SVladimir Kotal         Instant start = Instant.now();
319b3a15424SVladimir Kotal         LdapSearchResult<T> res = lookup(dn, filter, attributes, mapper, 0);
320b3a15424SVladimir Kotal         ldapLookupTimer.record(Duration.between(start, Instant.now()));
321b3a15424SVladimir Kotal         return res;
322b28a5538SAdam Hornacek     }
323b28a5538SAdam Hornacek 
3249f00baffSVladimir Kotal     // available for testing
getSearchDescription(String dn, String filter, String[] attributes)3259f00baffSVladimir Kotal     static String getSearchDescription(String dn, String filter, String[] attributes) {
3269f00baffSVladimir Kotal         StringBuilder builder = new StringBuilder();
3279f00baffSVladimir Kotal         builder.append("DN: ");
3289f00baffSVladimir Kotal         builder.append(dn);
3299f00baffSVladimir Kotal         builder.append(", filter: ");
3309f00baffSVladimir Kotal         builder.append(filter);
3319f00baffSVladimir Kotal         if (attributes != null) {
3329f00baffSVladimir Kotal             builder.append(", attributes: ");
3339f00baffSVladimir Kotal             builder.append(String.join(",", attributes));
3349f00baffSVladimir Kotal         }
3359f00baffSVladimir Kotal         return builder.toString();
33696f96d0bSVladimir Kotal     }
33796f96d0bSVladimir Kotal 
338b28a5538SAdam Hornacek     /**
339ff44f24aSAdam Hornáček      * Lookups the LDAP server for content.
340b28a5538SAdam Hornacek      *
341b28a5538SAdam Hornacek      * @param <T> return type
342b28a5538SAdam Hornacek      * @param dn search base for the query
343b28a5538SAdam Hornacek      * @param filter LDAP filter for the query
344b28a5538SAdam Hornacek      * @param attributes returning LDAP attributes
345b28a5538SAdam Hornacek      * @param mapper mapper class implementing @code{AttributeMapper} closed
346b28a5538SAdam Hornacek      * @param fail current count of failures
347b28a5538SAdam Hornacek      *
348b28a5538SAdam Hornacek      * @return results transformed with mapper or {@code null} on failure
349b28a5538SAdam Hornacek      * @throws LdapException LDAP exception
350b28a5538SAdam Hornacek      */
lookup(String dn, String filter, String[] attributes, AttributeMapper<T> mapper, int fail)35117deb9edSVladimir Kotal     private <T> LdapSearchResult<T> lookup(String dn, String filter, String[] attributes, AttributeMapper<T> mapper, int fail) throws LdapException {
352b28a5538SAdam Hornacek 
353b28a5538SAdam Hornacek         if (errorTimestamp > 0 && errorTimestamp + interval > System.currentTimeMillis()) {
354b28a5538SAdam Hornacek             if (!reported) {
355b28a5538SAdam Hornacek                 reported = true;
356b28a5538SAdam Hornacek                 LOGGER.log(Level.SEVERE, "LDAP server pool is still broken");
357b28a5538SAdam Hornacek             }
358b28a5538SAdam Hornacek             throw new LdapException("LDAP server pool is still broken");
359b28a5538SAdam Hornacek         }
360b28a5538SAdam Hornacek 
361b28a5538SAdam Hornacek         if (fail > servers.size() - 1) {
362b28a5538SAdam Hornacek             // did the whole rotation
363b28a5538SAdam Hornacek             LOGGER.log(Level.SEVERE, "Tried all LDAP servers in a pool but no server works");
364b28a5538SAdam Hornacek             errorTimestamp = System.currentTimeMillis();
365b28a5538SAdam Hornacek             reported = false;
366b28a5538SAdam Hornacek             WebHook hook;
367b28a5538SAdam Hornacek             if ((hook = webHooks.getFail()) != null) {
368b28a5538SAdam Hornacek                 hook.post();
369b28a5538SAdam Hornacek             }
370b28a5538SAdam Hornacek             throw new LdapException("Tried all LDAP servers in a pool but no server works");
371b28a5538SAdam Hornacek         }
372b28a5538SAdam Hornacek 
373b28a5538SAdam Hornacek         if (!isConfigured()) {
374b28a5538SAdam Hornacek             LOGGER.log(Level.SEVERE, "LDAP is not configured");
375b28a5538SAdam Hornacek             throw new LdapException("LDAP is not configured");
376b28a5538SAdam Hornacek         }
377b28a5538SAdam Hornacek 
378b28a5538SAdam Hornacek         NamingEnumeration<SearchResult> namingEnum = null;
37917deb9edSVladimir Kotal         LdapServer server = null;
380b28a5538SAdam Hornacek         try {
38117deb9edSVladimir Kotal             server = servers.get(actualServer);
382b28a5538SAdam Hornacek             controls.setReturningAttributes(attributes);
383b28a5538SAdam Hornacek             for (namingEnum = server.search(dn, filter, controls); namingEnum.hasMore();) {
384b28a5538SAdam Hornacek                 SearchResult sr = namingEnum.next();
385b28a5538SAdam Hornacek                 reported = false;
386b28a5538SAdam Hornacek                 if (errorTimestamp > 0) {
387b28a5538SAdam Hornacek                     errorTimestamp = 0;
388b28a5538SAdam Hornacek                     WebHook hook;
389b28a5538SAdam Hornacek                     if ((hook = webHooks.getRecover()) != null) {
390b28a5538SAdam Hornacek                         hook.post();
391b28a5538SAdam Hornacek                     }
392b28a5538SAdam Hornacek                 }
39317deb9edSVladimir Kotal 
39417deb9edSVladimir Kotal                 return new LdapSearchResult<>(sr.getNameInNamespace(), processResult(sr, mapper));
395b28a5538SAdam Hornacek             }
396b28a5538SAdam Hornacek         } catch (NameNotFoundException ex) {
39796f96d0bSVladimir Kotal             LOGGER.log(Level.WARNING, String.format("The LDAP name for search '%s' was not found on server %s",
39896f96d0bSVladimir Kotal                     getSearchDescription(dn, filter, attributes), server), ex);
399b28a5538SAdam Hornacek             throw new LdapException("The LDAP name was not found.", ex);
400b28a5538SAdam Hornacek         } catch (SizeLimitExceededException ex) {
401ee00a213SVladimir Kotal             LOGGER.log(Level.SEVERE, String.format("The maximum size of the LDAP result has exceeded "
402ee00a213SVladimir Kotal                     + "on server %s", server), ex);
403b28a5538SAdam Hornacek             closeActualServer();
404b28a5538SAdam Hornacek             actualServer = getNextServer();
405b28a5538SAdam Hornacek             return lookup(dn, filter, attributes, mapper, fail + 1);
406b28a5538SAdam Hornacek         } catch (TimeLimitExceededException ex) {
407ee00a213SVladimir Kotal             LOGGER.log(Level.SEVERE, String.format("Time limit for LDAP operation has exceeded on server %s",
408ee00a213SVladimir Kotal                     server), ex);
409b28a5538SAdam Hornacek             closeActualServer();
410b28a5538SAdam Hornacek             actualServer = getNextServer();
411b28a5538SAdam Hornacek             return lookup(dn, filter, attributes, mapper, fail + 1);
412b28a5538SAdam Hornacek         } catch (CommunicationException ex) {
413f3f79db5SVladimir Kotal             LOGGER.log(Level.WARNING, String.format("Communication error received on server %s, " +
414ee00a213SVladimir Kotal                     "reconnecting to next server.", server), ex);
415b28a5538SAdam Hornacek             closeActualServer();
416b28a5538SAdam Hornacek             actualServer = getNextServer();
417b28a5538SAdam Hornacek             return lookup(dn, filter, attributes, mapper, fail + 1);
418b28a5538SAdam Hornacek         } catch (NamingException ex) {
41996f96d0bSVladimir Kotal             LOGGER.log(Level.SEVERE, String.format("An arbitrary LDAP error occurred on server %s " +
420ce2ab33bSVladimir Kotal                     "when searching for '%s'", server, getSearchDescription(dn, filter, attributes)), ex);
421b28a5538SAdam Hornacek             closeActualServer();
422b28a5538SAdam Hornacek             actualServer = getNextServer();
423b28a5538SAdam Hornacek             return lookup(dn, filter, attributes, mapper, fail + 1);
424b28a5538SAdam Hornacek         } finally {
425b28a5538SAdam Hornacek             if (namingEnum != null) {
426b28a5538SAdam Hornacek                 try {
427b28a5538SAdam Hornacek                     namingEnum.close();
428b28a5538SAdam Hornacek                 } catch (NamingException e) {
429b28a5538SAdam Hornacek                     LOGGER.log(Level.WARNING,
430b28a5538SAdam Hornacek                             "failed to close search result enumeration");
431b28a5538SAdam Hornacek                 }
432b28a5538SAdam Hornacek             }
433b28a5538SAdam Hornacek         }
434b28a5538SAdam Hornacek 
435dd0cb931SVladimir Kotal         return null;
436b28a5538SAdam Hornacek     }
437b28a5538SAdam Hornacek 
closeActualServer()438b28a5538SAdam Hornacek     private void closeActualServer() {
439b28a5538SAdam Hornacek         servers.get(actualServer).close();
440b28a5538SAdam Hornacek     }
441b28a5538SAdam Hornacek 
442b28a5538SAdam Hornacek     /**
443b28a5538SAdam Hornacek      * Server take over algorithm behavior.
444b28a5538SAdam Hornacek      *
445b28a5538SAdam Hornacek      * @return the index of the next server to be used
446b28a5538SAdam Hornacek      */
getNextServer()447b28a5538SAdam Hornacek     private int getNextServer() {
448b28a5538SAdam Hornacek         return (actualServer + 1) % servers.size();
449b28a5538SAdam Hornacek     }
450b28a5538SAdam Hornacek 
451b28a5538SAdam Hornacek     /**
452b28a5538SAdam Hornacek      * Process the incoming LDAP result.
453b28a5538SAdam Hornacek      *
454b28a5538SAdam Hornacek      * @param <T> type of the result
455b28a5538SAdam Hornacek      * @param result LDAP result
456b28a5538SAdam Hornacek      * @param mapper mapper to transform the result into the result type
457b28a5538SAdam Hornacek      * @return transformed result
458b28a5538SAdam Hornacek      *
45917deb9edSVladimir Kotal      * @throws NamingException naming exception
460b28a5538SAdam Hornacek      */
processResult(SearchResult result, AttributeMapper<T> mapper)461b28a5538SAdam Hornacek     private <T> T processResult(SearchResult result, AttributeMapper<T> mapper) throws NamingException {
462b28a5538SAdam Hornacek         Attributes attrs = result.getAttributes();
463b28a5538SAdam Hornacek         if (attrs != null) {
46417deb9edSVladimir Kotal             return mapper.mapFromAttributes(attrs);
465b28a5538SAdam Hornacek         }
46617deb9edSVladimir Kotal 
467b28a5538SAdam Hornacek         return null;
468b28a5538SAdam Hornacek     }
46953c33ae5SVladimir Kotal 
4708ae5e262SAdam Hornacek     @Override
toString()47153c33ae5SVladimir Kotal     public String toString() {
472cbd2aafbSVladimir Kotal         return "{server=" + (actualServer != -1 ? servers.get(actualServer) : "no active server") +
473e5567996SVladimir Kotal                 ", searchBase=" + getSearchBase() + "}";
47453c33ae5SVladimir Kotal     }
475b28a5538SAdam Hornacek }
476