View Javadoc

1   /*
2    * Copyright [2006] [University Corporation for Advanced Internet Development, Inc.]
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package edu.internet2.middleware.shibboleth.common.attribute.resolver.provider.dataConnector;
18  
19  import java.io.IOException;
20  import java.security.GeneralSecurityException;
21  import java.security.KeyStore;
22  import java.security.cert.X509Certificate;
23  import java.util.HashMap;
24  import java.util.Iterator;
25  import java.util.List;
26  import java.util.Map;
27  import java.util.StringTokenizer;
28  
29  import javax.naming.NamingException;
30  import javax.naming.directory.SearchResult;
31  import javax.net.ssl.HostnameVerifier;
32  import javax.net.ssl.KeyManager;
33  import javax.net.ssl.KeyManagerFactory;
34  import javax.net.ssl.SSLContext;
35  import javax.net.ssl.SSLSocketFactory;
36  import javax.net.ssl.TrustManager;
37  import javax.net.ssl.TrustManagerFactory;
38  
39  import org.opensaml.xml.security.x509.X509Credential;
40  import org.opensaml.xml.util.DatatypeHelper;
41  import org.slf4j.Logger;
42  import org.slf4j.LoggerFactory;
43  import org.springframework.context.ApplicationEvent;
44  import org.springframework.context.ApplicationListener;
45  
46  import edu.internet2.middleware.shibboleth.common.attribute.BaseAttribute;
47  import edu.internet2.middleware.shibboleth.common.attribute.provider.BasicAttribute;
48  import edu.internet2.middleware.shibboleth.common.attribute.resolver.AttributeResolutionException;
49  import edu.internet2.middleware.shibboleth.common.attribute.resolver.provider.ShibbolethResolutionContext;
50  import edu.internet2.middleware.shibboleth.common.attribute.resolver.provider.dataConnector.TemplateEngine.CharacterEscapingStrategy;
51  import edu.internet2.middleware.shibboleth.common.session.LogoutEvent;
52  import edu.vt.middleware.ldap.Ldap;
53  import edu.vt.middleware.ldap.LdapConfig;
54  import edu.vt.middleware.ldap.LdapPool;
55  import edu.vt.middleware.ldap.LdapUtil;
56  
57  /**
58   * <code>LdapDataConnector</code> provides a plugin to retrieve attributes from an LDAP.
59   */
60  public class LdapDataConnector extends BaseDataConnector implements ApplicationListener {
61  
62      /** Search scope values. */
63      public static enum SEARCH_SCOPE {
64          /** Object level search scope. */
65          OBJECT,
66          /** One level search scope. */
67          ONELEVEL,
68          /** Subtree search scope. */
69          SUBTREE
70      };
71  
72      /** Authentication type values. */
73      public static enum AUTHENTICATION_TYPE {
74          /** Anonymous authentication type. */
75          ANONYMOUS,
76          /** Simple authentication type. */
77          SIMPLE,
78          /** Strong authentication type. */
79          STRONG,
80          /** External authentication type. */
81          EXTERNAL,
82          /** Digest MD5 authentication type. */
83          DIGEST_MD5,
84          /** Cram MD5 authentication type. */
85          CRAM_MD5,
86          /** Kerberos authentication type. */
87          GSSAPI
88      };
89  
90      /** Class logger. */
91      private static Logger log = LoggerFactory.getLogger(LdapDataConnector.class);
92  
93      /** SSL trust managers. */
94      private TrustManager[] sslTrustManagers;
95  
96      /** SSL key managers. */
97      private KeyManager[] sslKeyManagers;
98  
99      /** Whether multiple result sets should be merged. */
100     private boolean mergeMultipleResults;
101 
102     /** Whether an empty result set is an error. */
103     private boolean noResultsIsError;
104 
105     /** Whether to cache search results for the duration of the session. */
106     private boolean cacheResults;
107 
108     /** Template engine used to change filter template into actual filter. */
109     private TemplateEngine filterCreator;
110 
111     /** Name the filter template is registered under within the template engine. */
112     private String filterTemplateName;
113 
114     /** Template that produces the query to use. */
115     private String filterTemplate;
116 
117     /** Attributes to return from ldap searches. */
118     private String[] returnAttributes;
119 
120     /** Ldap configuration. */
121     private LdapConfig ldapConfig;
122 
123     /** LdapPool object. */
124     private LdapPool ldapPool;
125 
126     /** Maximum number of idle objects in the ldap pool. */
127     private int poolMaxIdle;
128 
129     /** Initial capacity of the the ldap pool. */
130     private int poolInitIdleCapacity;
131 
132     /** Data cache. */
133     private Map<String, Map<String, Map<String, BaseAttribute>>> cache;
134 
135     /** Whether this data connector has been initialized. */
136     private boolean initialized;
137 
138     /** Filter value escaping strategy. */
139     private final LDAPValueEscapingStrategy escapingStrategy;
140 
141     /**
142      * This creates a new ldap data connector with the supplied properties.
143      * 
144      * @param ldapUrl <code>String</code> to connect to
145      * @param ldapBaseDn <code>String</code> to begin searching at
146      * @param startTls <code>boolean</code> whether connection should startTls
147      * @param maxIdle <code>int</code> maximum number of idle pool objects
148      * @param initIdleCapacity <code>int</code> initial capacity of the pool
149      */
150     public LdapDataConnector(String ldapUrl, String ldapBaseDn, boolean startTls, int maxIdle, int initIdleCapacity) {
151         super();
152         ldapConfig = new LdapConfig(ldapUrl, ldapBaseDn);
153         ldapConfig.useTls(startTls);
154         poolMaxIdle = maxIdle;
155         poolInitIdleCapacity = initIdleCapacity;
156         escapingStrategy = new LDAPValueEscapingStrategy();
157     }
158 
159     /**
160      * Initializes the connector and prepares it for use.
161      */
162     public void initialize() {
163         initialized = true;
164         registerTemplate();
165         initializeLdapPool();
166         initializeCache();
167     }
168 
169     /**
170      * Initializes the ldap pool and prepares it for use. {@link #initialize()} must be called first or this method does
171      * nothing.
172      */
173     protected void initializeLdapPool() {
174         if (initialized) {
175             ldapPool = new LdapPool(ldapConfig, poolMaxIdle, poolInitIdleCapacity);
176         }
177     }
178 
179     /**
180      * Initializes the cache and prepares it for use. {@link #initialize()} must be called first or this method does
181      * nothing.
182      */
183     protected void initializeCache() {
184         if (cacheResults && initialized) {
185             cache = new HashMap<String, Map<String, Map<String, BaseAttribute>>>();
186         }
187     }
188 
189     /**
190      * This removes all entries from the cache. {@link #initialize()} must be called first or this method does nothing.
191      */
192     protected void clearCache() {
193         if (cacheResults && initialized) {
194             cache.clear();
195         }
196     }
197 
198     /**
199      * Registers the query template with template engine. {@link #initialize()} must be called first or this method does
200      * nothing.
201      */
202     protected void registerTemplate() {
203         if (initialized) {
204             filterTemplateName = "shibboleth.resolver.dc." + getId();
205             filterCreator.registerTemplate(filterTemplateName, filterTemplate);
206         }
207     }
208 
209     /**
210      * This returns whether this connector will merge multiple search results into one result. The default is false.
211      * 
212      * @return <code>boolean</code>
213      */
214     public boolean isMergeResults() {
215         return mergeMultipleResults;
216     }
217 
218     /**
219      * This sets whether this connector will merge multiple search results into one result. This method will remove any
220      * cached results.
221      * 
222      * @see #clearCache()
223      * 
224      * @param b <code>boolean</code>
225      */
226     public void setMergeResults(boolean b) {
227         mergeMultipleResults = b;
228         clearCache();
229     }
230 
231     /**
232      * This returns whether this connector will cache search results. The default is false.
233      * 
234      * @return <code>boolean</code>
235      */
236     public boolean isCacheResults() {
237         return cacheResults;
238     }
239 
240     /**
241      * This sets whether this connector will cache search results.
242      * 
243      * @see #initializeCache()
244      * 
245      * @param b <code>boolean</code>
246      */
247     public void setCacheResults(boolean b) {
248         cacheResults = b;
249         if (!cacheResults) {
250             cache = null;
251         } else {
252             initializeCache();
253         }
254     }
255 
256     /**
257      * This returns whether this connector will throw an exception if no search results are found. The default is false.
258      * 
259      * @return <code>boolean</code>
260      */
261     public boolean isNoResultsIsError() {
262         return noResultsIsError;
263     }
264 
265     /**
266      * This sets whether this connector will throw an exception if no search results are found.
267      * 
268      * @param b <code>boolean</code>
269      */
270     public void setNoResultsIsError(boolean b) {
271         noResultsIsError = b;
272     }
273 
274     /**
275      * Gets the engine used to evaluate the query template.
276      * 
277      * @return engine used to evaluate the query template
278      */
279     public TemplateEngine getTemplateEngine() {
280         return filterCreator;
281     }
282 
283     /**
284      * Sets the engine used to evaluate the query template.
285      * 
286      * @param engine engine used to evaluate the query template
287      */
288     public void setTemplateEngine(TemplateEngine engine) {
289         filterCreator = engine;
290         registerTemplate();
291         clearCache();
292     }
293 
294     /**
295      * Gets the template used to create queries.
296      * 
297      * @return template used to create queries
298      */
299     public String getFilterTemplate() {
300         return filterTemplate;
301     }
302 
303     /**
304      * Sets the template used to create queries.
305      * 
306      * @param template template used to create queries
307      */
308     public void setFilterTemplate(String template) {
309         filterTemplate = template;
310         clearCache();
311     }
312 
313     /**
314      * This returns the URL this connector is using.
315      * 
316      * @return <code>String</code>
317      */
318     public String getLdapUrl() {
319         return ldapConfig.getHost();
320     }
321 
322     /**
323      * This returns the base DN this connector is using.
324      * 
325      * @return <code>String</code>
326      */
327     public String getBaseDn() {
328         return ldapConfig.getBase();
329     }
330 
331     /**
332      * This returns whether this connector will start TLS for all connections to the ldap.
333      * 
334      * @return <code>boolean</code>
335      */
336     public boolean isUseStartTls() {
337         return ldapConfig.isTlsEnabled();
338     }
339 
340     /**
341      * This returns the SSL Socket Factory that will be used for all TLS and SSL connections to the ldap.
342      * 
343      * @return <code>SSLSocketFactory</code>
344      */
345     public SSLSocketFactory getSslSocketFactory() {
346         return ldapConfig.getSslSocketFactory();
347     }
348 
349     /**
350      * This sets the SSL Socket Factory that will be used for all TLS and SSL connections to the ldap. This method will
351      * remove any cached results and initialize the ldap pool.
352      * 
353      * @see #clearCache()
354      * @see #initializeLdapPool()
355      * 
356      * @param sf <code>SSLSocketFactory</code>
357      */
358     public void setSslSocketFactory(SSLSocketFactory sf) {
359         ldapConfig.setSslSocketFactory(sf);
360         clearCache();
361         initializeLdapPool();
362     }
363 
364     /**
365      * This returns the trust managers that will be used for all TLS and SSL connections to the ldap.
366      * 
367      * @return <code>TrustManager[]</code>
368      */
369     public TrustManager[] getSslTrustManagers() {
370         return sslTrustManagers;
371     }
372 
373     /**
374      * This sets the trust managers that will be used for all TLS and SSL connections to the ldap. This method will
375      * remove any cached results and initialize the ldap pool.
376      * 
377      * @see #clearCache()
378      * @see #initializeLdapPool()
379      * @see #setSslSocketFactory(SSLSocketFactory)
380      * 
381      * @param tc <code>X509Credential</code> to create TrustManagers with
382      */
383     public void setSslTrustManagers(X509Credential tc) {
384         if (tc != null) {
385             try {
386                 TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
387                 KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType());
388                 keystore.load(null, null);
389                 for (X509Certificate c : tc.getEntityCertificateChain()) {
390                     keystore.setCertificateEntry("ldap_tls_trust_" + c.getSerialNumber(), c);
391                 }
392                 tmf.init(keystore);
393                 sslTrustManagers = tmf.getTrustManagers();
394                 SSLContext ctx = SSLContext.getInstance("TLS");
395                 ctx.init(sslKeyManagers, sslTrustManagers, null);
396                 ldapConfig.setSslSocketFactory(ctx.getSocketFactory());
397                 clearCache();
398                 initializeLdapPool();
399             } catch (GeneralSecurityException e) {
400                 log.error("Error initializing trust managers", e);
401             } catch (IOException e) {
402                 log.error("Error initializing trust managers", e);
403             }
404         }
405     }
406 
407     /**
408      * This returns the key managers that will be used for all TLS and SSL connections to the ldap.
409      * 
410      * @return <code>KeyManager[]</code>
411      */
412     public KeyManager[] getSslKeyManagers() {
413         return sslKeyManagers;
414     }
415 
416     /**
417      * This sets the key managers that will be used for all TLS and SSL connections to the ldap. This method will remove
418      * any cached results and initialize the ldap pool.
419      * 
420      * @see #clearCache()
421      * @see #initializeLdapPool()
422      * @see #setSslSocketFactory(SSLSocketFactory)
423      * 
424      * @param kc <code>X509Credential</code> to create KeyManagers with
425      */
426     public void setSslKeyManagers(X509Credential kc) {
427         if (kc != null) {
428             try {
429                 KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
430                 KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType());
431                 keystore.load(null, null);
432                 keystore.setKeyEntry("ldap_tls_client_auth", kc.getPrivateKey(), "changeit".toCharArray(), kc
433                         .getEntityCertificateChain().toArray(new X509Certificate[0]));
434                 kmf.init(keystore, "changeit".toCharArray());
435                 sslKeyManagers = kmf.getKeyManagers();
436                 SSLContext ctx = SSLContext.getInstance("TLS");
437                 ctx.init(sslKeyManagers, sslTrustManagers, null);
438                 ldapConfig.setSslSocketFactory(ctx.getSocketFactory());
439                 clearCache();
440                 initializeLdapPool();
441             } catch (GeneralSecurityException e) {
442                 log.error("Error initializing key managers", e);
443             } catch (IOException e) {
444                 log.error("Error initializing key managers", e);
445             }
446         }
447     }
448 
449     /**
450      * This returns the hostname verifier that will be used for all TLS and SSL connections to the ldap.
451      * 
452      * @return <code>HostnameVerifier</code>
453      */
454     public HostnameVerifier getHostnameVerifier() {
455         return ldapConfig.getHostnameVerifier();
456     }
457 
458     /**
459      * This sets the hostname verifier that will be used for all TLS and SSL connections to the ldap. This method will
460      * remove any cached results and initialize the ldap pool.
461      * 
462      * @see #clearCache()
463      * @see #initializeLdapPool()
464      * 
465      * @param hv <code>HostnameVerifier</code>
466      */
467     public void setHostnameVerifier(HostnameVerifier hv) {
468         ldapConfig.setHostnameVerifier(hv);
469         clearCache();
470         initializeLdapPool();
471     }
472 
473     /**
474      * This returns the authentication type used when binding to the ldap.
475      * 
476      * @return <code>AUTHENTICATION_TYPE</code>
477      */
478     public AUTHENTICATION_TYPE getAuthenticationType() {
479         AUTHENTICATION_TYPE type = null;
480         if (ldapConfig.isAnonymousAuth()) {
481             type = AUTHENTICATION_TYPE.ANONYMOUS;
482         } else if (ldapConfig.isSimpleAuth()) {
483             type = AUTHENTICATION_TYPE.SIMPLE;
484         } else if (ldapConfig.isStrongAuth()) {
485             type = AUTHENTICATION_TYPE.STRONG;
486         } else if (ldapConfig.isExternalAuth()) {
487             type = AUTHENTICATION_TYPE.EXTERNAL;
488         } else if (ldapConfig.isDigestMD5Auth()) {
489             type = AUTHENTICATION_TYPE.DIGEST_MD5;
490         } else if (ldapConfig.isCramMD5Auth()) {
491             type = AUTHENTICATION_TYPE.CRAM_MD5;
492         } else if (ldapConfig.isGSSAPIAuth()) {
493             type = AUTHENTICATION_TYPE.GSSAPI;
494         }
495         return type;
496     }
497 
498     /**
499      * This sets the authentication type used when binding to the ldap. This method will remove any cached results and
500      * initialize the ldap pool.
501      * 
502      * @see #clearCache()
503      * @see #initializeLdapPool()
504      * 
505      * @param type <code>AUTHENTICATION_TYPE</code>
506      */
507     public void setAuthenticationType(AUTHENTICATION_TYPE type) {
508         if (type == AUTHENTICATION_TYPE.ANONYMOUS) {
509             ldapConfig.useAnonymousAuth();
510         } else if (type == AUTHENTICATION_TYPE.SIMPLE) {
511             ldapConfig.useSimpleAuth();
512         } else if (type == AUTHENTICATION_TYPE.STRONG) {
513             ldapConfig.useStrongAuth();
514         } else if (type == AUTHENTICATION_TYPE.EXTERNAL) {
515             ldapConfig.useExternalAuth();
516         } else if (type == AUTHENTICATION_TYPE.DIGEST_MD5) {
517             ldapConfig.useDigestMD5Auth();
518         } else if (type == AUTHENTICATION_TYPE.CRAM_MD5) {
519             ldapConfig.useCramMD5Auth();
520         } else if (type == AUTHENTICATION_TYPE.GSSAPI) {
521             ldapConfig.useGSSAPIAuth();
522         }
523         clearCache();
524         initializeLdapPool();
525     }
526 
527     /**
528      * This returns the search scope used when searching the ldap.
529      * 
530      * @return <code>int</code>
531      */
532     public SEARCH_SCOPE getSearchScope() {
533         SEARCH_SCOPE scope = null;
534         if (ldapConfig.isObjectSearchScope()) {
535             scope = SEARCH_SCOPE.OBJECT;
536         } else if (ldapConfig.isOneLevelSearchScope()) {
537             scope = SEARCH_SCOPE.ONELEVEL;
538         } else if (ldapConfig.isSubTreeSearchScope()) {
539             scope = SEARCH_SCOPE.SUBTREE;
540         }
541         return scope;
542     }
543 
544     /**
545      * This sets the search scope used when searching the ldap. This method will remove any cached results.
546      * 
547      * @see #clearCache()
548      * 
549      * @param scope directory search scope
550      */
551     public void setSearchScope(SEARCH_SCOPE scope) {
552         if (scope == SEARCH_SCOPE.OBJECT) {
553             ldapConfig.useObjectSearchScope();
554         } else if (scope == SEARCH_SCOPE.SUBTREE) {
555             ldapConfig.useSubTreeSearchScope();
556         } else if (scope == SEARCH_SCOPE.ONELEVEL) {
557             ldapConfig.useOneLevelSearchScope();
558         }
559         clearCache();
560     }
561 
562     /**
563      * This returns the attributes that all searches will request from the ldap.
564      * 
565      * @return <code>String[]</code>
566      */
567     public String[] getReturnAttributes() {
568         return returnAttributes;
569     }
570 
571     /**
572      * This sets the attributes that all searches will request from the ldap. This method will remove any cached
573      * results.
574      * 
575      * @see #clearCache()
576      * 
577      * @param s <code>String[]</code>
578      */
579     public void setReturnAttributes(String[] s) {
580         returnAttributes = s;
581         clearCache();
582     }
583 
584     /**
585      * This sets the attributes that all searches will request from the ldap. s should be a comma delimited string.
586      * 
587      * @param s <code>String[]</code> comma delimited returnAttributes
588      */
589     public void setReturnAttributes(String s) {
590         StringTokenizer st = new StringTokenizer(s, ",");
591         String[] ra = new String[st.countTokens()];
592         for (int count = 0; count < st.countTokens(); count++) {
593             ra[count] = st.nextToken();
594         }
595         setReturnAttributes(ra);
596     }
597 
598     /**
599      * This returns the time in milliseconds that the ldap will wait for search results. A value of 0 means to wait
600      * indefinitely.
601      * 
602      * @return <code>int</code> milliseconds
603      */
604     public int getSearchTimeLimit() {
605         return ldapConfig.getTimeLimit();
606     }
607 
608     /**
609      * This sets the time in milliseconds that the ldap will wait for search results. A value of 0 means to wait
610      * indefinitely. This method will remove any cached results.
611      * 
612      * @see #clearCache()
613      * 
614      * @param i <code>int</code> milliseconds
615      */
616     public void setSearchTimeLimit(int i) {
617         ldapConfig.setTimeLimit(i);
618         clearCache();
619     }
620 
621     /**
622      * This returns the maximum number of search results the ldap will return. A value of 0 all entries will be
623      * returned.
624      * 
625      * @return <code>long</code> maximum number of search results
626      */
627     public long getMaxResultSize() {
628         return ldapConfig.getCountLimit();
629     }
630 
631     /**
632      * This sets the maximum number of search results the ldap will return. A value of 0 all entries will be returned.
633      * This method will remove any cached results.
634      * 
635      * @see #clearCache()
636      * 
637      * @param l <code>long</code> maximum number of search results
638      */
639     public void setMaxResultSize(long l) {
640         ldapConfig.setCountLimit(l);
641         clearCache();
642     }
643 
644     /**
645      * This returns whether objects will be returned in the search results. The default is false.
646      * 
647      * @return <code>boolean</code>
648      */
649     public boolean isReturningObjects() {
650         return ldapConfig.getReturningObjFlag();
651     }
652 
653     /**
654      * This sets whether objects will be returned in the search results. This method will remove any cached results.
655      * 
656      * @see #clearCache()
657      * 
658      * @param b <code>boolean</code>
659      */
660     public void setReturningObjects(boolean b) {
661         ldapConfig.setReturningObjFlag(b);
662         clearCache();
663     }
664 
665     /**
666      * This returns whether link dereferencing will be used during the search. The default is false.
667      * 
668      * @return <code>boolean</code>
669      */
670     public boolean isLinkDereferencing() {
671         return ldapConfig.getDerefLinkFlag();
672     }
673 
674     /**
675      * This sets whether link dereferencing will be used during the search. This method will remove any cached results.
676      * 
677      * @see #clearCache()
678      * 
679      * @param b <code>boolean</code>
680      */
681     public void setLinkDereferencing(boolean b) {
682         ldapConfig.setDerefLinkFlag(b);
683         clearCache();
684     }
685 
686     /**
687      * This returns the principal dn used to bind to the ldap for all searches.
688      * 
689      * @return <code>String</code> principal dn
690      */
691     public String getPrincipal() {
692         return ldapConfig.getServiceUser();
693     }
694 
695     /**
696      * This sets the principal dn used to bind to the ldap for all searches. This method will remove any cached results
697      * and initialize the ldap pool.
698      * 
699      * @see #clearCache()
700      * @see #initializeLdapPool()
701      * 
702      * @param s <code>String</code> principal dn
703      */
704     public void setPrincipal(String s) {
705         ldapConfig.setServiceUser(s);
706         clearCache();
707         initializeLdapPool();
708     }
709 
710     /**
711      * This returns the principal credential used to bind to the ldap for all searches.
712      * 
713      * @return <code>String</code> principal credential
714      */
715     public String getPrincipalCredential() {
716         return (String) ldapConfig.getServiceCredential();
717     }
718 
719     /**
720      * This sets the principal credential used to bind to the ldap for all searches. This method will remove any cached
721      * results and initialize the ldap pool.
722      * 
723      * @see #clearCache()
724      * @see #initializeLdapPool()
725      * 
726      * @param s <code>String</code> principal credential
727      */
728     public void setPrincipalCredential(String s) {
729         ldapConfig.setServiceCredential(s);
730         clearCache();
731         initializeLdapPool();
732     }
733 
734     /**
735      * This sets additional ldap context environment properties. This method will remove any cached results and
736      * initialize the ldap pool.
737      * 
738      * @see #clearCache()
739      * @see #initializeLdapPool()
740      * 
741      * @param ldapProperties <code>Map</code> of name/value pairs
742      */
743     public void setLdapProperties(Map<String, String> ldapProperties) {
744         for (Map.Entry<String, String> entry : ldapProperties.entrySet()) {
745             ldapConfig.setEnvironmentProperties(entry.getKey(), entry.getValue());
746         }
747         clearCache();
748         initializeLdapPool();
749     }
750 
751     /** {@inheritDoc} */
752     public void onApplicationEvent(ApplicationEvent evt) {
753         if (evt instanceof LogoutEvent) {
754             LogoutEvent logoutEvent = (LogoutEvent) evt;
755             cache.remove(logoutEvent.getUserSession().getPrincipalName());
756         }
757     }
758 
759     /** {@inheritDoc} */
760     public Map<String, BaseAttribute> resolve(ShibbolethResolutionContext resolutionContext)
761             throws AttributeResolutionException {
762         String searchFilter = filterCreator.createStatement(filterTemplateName, resolutionContext, getDependencyIds(),
763                 escapingStrategy);
764         searchFilter = searchFilter.trim();
765         log.debug("Search filter: {}", searchFilter);
766 
767         // create Attribute objects to return
768         Map<String, BaseAttribute> attributes = null;
769 
770         // check for cached data
771         if (cacheResults) {
772             log.debug("Checking cache for search results");
773             attributes = getCachedAttributes(resolutionContext, searchFilter);
774             if (attributes != null && log.isDebugEnabled()) {
775                 log.debug("Returning attributes from cache");
776             }
777         }
778 
779         // results not found in the cache
780         if (attributes == null) {
781             log.debug("Retrieving attributes from LDAP");
782             Iterator<SearchResult> results = searchLdap(searchFilter);
783             // check for empty result set
784             if (noResultsIsError && !results.hasNext()) {
785                 throw new AttributeResolutionException("No LDAP entry found for "
786                         + resolutionContext.getAttributeRequestContext().getPrincipalName());
787             }
788             // build resolved attributes from LDAP attributes
789             attributes = buildBaseAttributes(results);
790             if (cacheResults && attributes != null) {
791                 setCachedAttributes(resolutionContext, searchFilter, attributes);
792                 log.debug("Stored results in the cache");
793             }
794         }
795 
796         return attributes;
797     }
798 
799     /** {@inheritDoc} */
800     public void validate() throws AttributeResolutionException {
801         Ldap ldap = null;
802         try {
803             ldap = (Ldap) ldapPool.borrowObject();
804             if (!ldap.connect()) {
805                 throw new NamingException();
806             }
807         } catch (NamingException e) {
808             log.error("An error occured when attempting to search the LDAP: " + ldapConfig.getEnvironment(), e);
809             throw new AttributeResolutionException("An error occurred when attempting to search the LDAP");
810         } catch (Exception e) {
811             log.error("Could not retrieve Ldap object from pool", e);
812             throw new AttributeResolutionException(
813                     "An error occurred when attempting to retrieve a LDAP connection from the pool");
814         } finally {
815             if (ldap != null) {
816                 try {
817                     ldapPool.returnObject(ldap);
818                 } catch (Exception e) {
819                     log.error("Could not return Ldap object back to pool", e);
820                 }
821             }
822         }
823     }
824 
825     /**
826      * This searches the LDAP with the supplied filter.
827      * 
828      * @param searchFilter <code>String</code> the searchFilter that produced the attributes
829      * @return <code>Iterator</code> of search results
830      * @throws AttributeResolutionException if an error occurs performing the search
831      */
832     protected Iterator<SearchResult> searchLdap(String searchFilter) throws AttributeResolutionException {
833         Ldap ldap = null;
834         try {
835             ldap = (Ldap) ldapPool.borrowObject();
836             return ldap.search(searchFilter, returnAttributes);
837         } catch (NamingException e) {
838             log.error("An error occured when attempting to search the LDAP: " + ldapConfig.getEnvironment(), e);
839             throw new AttributeResolutionException("An error occurred when attempting to search the LDAP");
840         } catch (Exception e) {
841             log.error("Could not retrieve Ldap object from pool", e);
842             throw new AttributeResolutionException(
843                     "An error occurred when attempting to retrieve a LDAP connection from the pool");
844         } finally {
845             if (ldap != null) {
846                 try {
847                     ldapPool.returnObject(ldap);
848                 } catch (Exception e) {
849                     log.error("Could not return Ldap object back to pool", e);
850                 }
851             }
852         }
853     }
854 
855     /**
856      * This returns a map of attribute ids to attributes from the supplied search results.
857      * 
858      * @param results <code>Iterator</code> of LDAP search results
859      * @return <code>Map</code> of attribute ids to attributes
860      * @throws AttributeResolutionException if an error occurs parsing attribute results
861      */
862     protected Map<String, BaseAttribute> buildBaseAttributes(Iterator<SearchResult> results)
863             throws AttributeResolutionException {
864 
865         Map<String, BaseAttribute> attributes = new HashMap<String, BaseAttribute>();
866 
867         if (!results.hasNext()) {
868             return attributes;
869         }
870 
871         do{
872             SearchResult sr = results.next();
873             Map<String, List<String>> newAttrsMap = null;
874             try{
875                 newAttrsMap = LdapUtil.parseAttributes(sr.getAttributes(), true);
876             } catch (NamingException e) {
877                 log.error("Error parsing LDAP attributes", e);
878                 throw new AttributeResolutionException("Error parsing LDAP attributes");
879             }
880             
881             for (Map.Entry<String, List<String>> entry : newAttrsMap.entrySet()) {
882                 log.debug("Found the following attribute: {}", entry);
883                 BaseAttribute<String> attribute = attributes.get(entry.getKey());
884                 if(attribute == null){
885                     attribute = new BasicAttribute<String>();
886                     ((BasicAttribute)attribute).setId(entry.getKey());
887                     attributes.put(entry.getKey(), attribute);
888                 }
889                 
890                 List<String> values = entry.getValue();
891                 if(values != null && !values.isEmpty()){
892                     for(String value : values){
893                         if(!DatatypeHelper.isEmpty(value)){
894                             attribute.getValues().add(DatatypeHelper.safeTrimOrNullString(value));
895                         }
896                     }
897                 }
898             }
899         }while (mergeMultipleResults && results.hasNext());
900         
901         return attributes;
902     }
903 
904     /**
905      * This stores the supplied attributes in the cache.
906      * 
907      * @param resolutionContext <code>ResolutionContext</code>
908      * @param searchFiler the searchFilter that produced the attributes
909      * @param attributes <code>Map</code> of attribute ids to attributes
910      */
911     protected void setCachedAttributes(ShibbolethResolutionContext resolutionContext, String searchFiler,
912             Map<String, BaseAttribute> attributes) {
913         Map<String, Map<String, BaseAttribute>> results = null;
914         String principal = resolutionContext.getAttributeRequestContext().getPrincipalName();
915         if (cache.containsKey(principal)) {
916             results = cache.get(principal);
917         } else {
918             results = new HashMap<String, Map<String, BaseAttribute>>();
919             cache.put(principal, results);
920         }
921         results.put(searchFiler, attributes);
922     }
923 
924     /**
925      * This retrieves any cached attributes for the supplied resolution context. Returns null if nothing is cached.
926      * 
927      * @param resolutionContext <code>ResolutionContext</code>
928      * @param searchFilter the search filter the produced the attributes
929      * 
930      * @return <code>Map</code> of attributes ids to attributes
931      */
932     protected Map<String, BaseAttribute> getCachedAttributes(ShibbolethResolutionContext resolutionContext,
933             String searchFilter) {
934         Map<String, BaseAttribute> attributes = null;
935         if (cacheResults) {
936             String principal = resolutionContext.getAttributeRequestContext().getPrincipalName();
937             if (cache.containsKey(principal)) {
938                 Map<String, Map<String, BaseAttribute>> results = cache.get(principal);
939                 attributes = results.get(searchFilter);
940             }
941         }
942         return attributes;
943     }
944 
945     /**
946      * Escapes values that will be included within an LDAP filter.
947      */
948     protected class LDAPValueEscapingStrategy implements CharacterEscapingStrategy {
949 
950         /** {@inheritDoc} */
951         public String escape(String value) {
952             return value.replace("*", "\\*").replace("(", "\\(").replace(")", "\\)").replace("\\", "\\");
953         }
954     }
955 }