View Javadoc

1   /*
2    * Licensed to the University Corporation for Advanced Internet Development, 
3    * Inc. (UCAID) under one or more contributor license agreements.  See the 
4    * NOTICE file distributed with this work for additional information regarding
5    * copyright ownership. The UCAID licenses this file to You under the Apache 
6    * License, Version 2.0 (the "License"); you may not use this file except in 
7    * compliance with the License.  You may obtain a copy of the License at
8    *
9    *    http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  package edu.internet2.middleware.shibboleth.common.attribute.resolver.provider.dataConnector;
19  
20  import java.util.HashMap;
21  import java.util.Iterator;
22  import java.util.Map;
23  import java.util.Set;
24  import java.util.StringTokenizer;
25  
26  import javax.naming.NamingException;
27  import javax.naming.directory.SearchResult;
28  
29  import net.sf.ehcache.Cache;
30  import net.sf.ehcache.Element;
31  
32  import org.opensaml.xml.util.DatatypeHelper;
33  import org.slf4j.Logger;
34  import org.slf4j.LoggerFactory;
35  
36  import edu.internet2.middleware.shibboleth.common.attribute.BaseAttribute;
37  import edu.internet2.middleware.shibboleth.common.attribute.provider.BasicAttribute;
38  import edu.internet2.middleware.shibboleth.common.attribute.resolver.AttributeResolutionException;
39  import edu.internet2.middleware.shibboleth.common.attribute.resolver.provider.ShibbolethResolutionContext;
40  import edu.internet2.middleware.shibboleth.common.attribute.resolver.provider.dataConnector.TemplateEngine.CharacterEscapingStrategy;
41  import edu.vt.middleware.ldap.Ldap;
42  import edu.vt.middleware.ldap.SearchFilter;
43  import edu.vt.middleware.ldap.bean.LdapAttribute;
44  import edu.vt.middleware.ldap.bean.LdapAttributes;
45  import edu.vt.middleware.ldap.bean.LdapBeanProvider;
46  
47  /**
48   * <code>LdapDataConnector</code> provides a plugin to retrieve attributes from an LDAP.
49   */
50  public class LdapDataConnector extends BaseDataConnector {
51  
52      /** Authentication type values. */
53      public static enum AUTHENTICATION_TYPE {
54          /** Anonymous authentication type. */
55          ANONYMOUS("none"),
56          /** Simple authentication type. */
57          SIMPLE("simple"),
58          /** Strong authentication type. */
59          STRONG("strong"),
60          /** External authentication type. */
61          EXTERNAL("EXTERNAL"),
62          /** Digest MD5 authentication type. */
63          DIGEST_MD5("DIGEST-MD5"),
64          /** Cram MD5 authentication type. */
65          CRAM_MD5("CRAM-MD5"),
66          /** Kerberos authentication type. */
67          GSSAPI("GSSAPI");
68  
69          /** auth type name passed to LdapConfig. */
70          private String authTypeName;
71  
72          /**
73           * Default constructor.
74           * 
75           * @param s auth type name
76           */
77          private AUTHENTICATION_TYPE(String s) {
78              authTypeName = s;
79          }
80  
81          /**
82           * This returns the auth type name needed by the LdapConfig.
83           * 
84           * @return auth type name
85           */
86          public String getAuthTypeName() {
87              return authTypeName;
88          }
89  
90          /**
91           * Returns the corresponding AUTHENTICATION_TYPE for the supplied auth type name.
92           * 
93           * @param s auth type name to lookup
94           * @return AUTHENTICATION_TYPE
95           */
96          public static AUTHENTICATION_TYPE getAuthenticationTypeByName(String s) {
97              AUTHENTICATION_TYPE type = null;
98              if (AUTHENTICATION_TYPE.ANONYMOUS.getAuthTypeName().equals(s)) {
99                  type = AUTHENTICATION_TYPE.ANONYMOUS;
100             } else if (AUTHENTICATION_TYPE.SIMPLE.getAuthTypeName().equals(s)) {
101                 type = AUTHENTICATION_TYPE.SIMPLE;
102             } else if (AUTHENTICATION_TYPE.STRONG.getAuthTypeName().equals(s)) {
103                 type = AUTHENTICATION_TYPE.STRONG;
104             } else if (AUTHENTICATION_TYPE.EXTERNAL.getAuthTypeName().equals(s)) {
105                 type = AUTHENTICATION_TYPE.EXTERNAL;
106             } else if (AUTHENTICATION_TYPE.DIGEST_MD5.getAuthTypeName().equals(s)) {
107                 type = AUTHENTICATION_TYPE.DIGEST_MD5;
108             } else if (AUTHENTICATION_TYPE.CRAM_MD5.getAuthTypeName().equals(s)) {
109                 type = AUTHENTICATION_TYPE.CRAM_MD5;
110             } else if (AUTHENTICATION_TYPE.GSSAPI.getAuthTypeName().equals(s)) {
111                 type = AUTHENTICATION_TYPE.GSSAPI;
112             }
113             return type;
114         }
115     };
116 
117     /** Class logger. */
118     private static Logger log = LoggerFactory.getLogger(LdapDataConnector.class);
119 
120     /** Ldap pool strategy. */
121     private LdapPoolStrategy ldapPool;
122 
123     /** Template engine used to change filter template into actual filter. */
124     private TemplateEngine filterCreator;
125 
126     /** Name the filter template is registered under within the template engine. */
127     private String filterTemplateName;
128 
129     /** Template that produces the query to use. */
130     private String filterTemplate;
131 
132     /** Attributes to return from ldap searches. */
133     private String[] returnAttributes;
134 
135     /** Whether an empty result set is an error. */
136     private boolean noResultsIsError;
137     
138     /** Cache of past search results. */
139     private Cache resultsCache;
140 
141     /** Filter value escaping strategy. */
142     private final LDAPValueEscapingStrategy escapingStrategy;
143 
144     /**
145      * This creates a new LDAP data connector with the supplied properties.
146      * 
147      * @param pool LDAP connection pooling strategy
148      * @param cache cached used to cache search results, or null if results should not be cached
149      */
150     public LdapDataConnector(LdapPoolStrategy pool, Cache cache) {
151         super();
152         ldapPool = pool;
153 
154         resultsCache = cache;
155 
156         escapingStrategy = new LDAPValueEscapingStrategy();
157     }
158 
159     /**
160      * This sets the underlying template engine and registers the supplied template.
161      * 
162      * @param engine engine used to fill in search filter templates
163      * @param template search filter template
164      */
165     public void registerTemplate(TemplateEngine engine, String template) {
166         if (getId() == null) {
167             throw new IllegalStateException("Template cannot be registered until plugin id has been set");
168         }
169         filterCreator = engine;
170         filterTemplate = template;
171         filterTemplateName = "shibboleth.resolver.dc." + getId();
172         filterCreator.registerTemplate(filterTemplateName, filterTemplate);        
173     }
174 
175     /** Removes all entries from the cache if results are being cached. */
176     protected void clearCache() {
177         if (isCacheResults()) {
178             resultsCache.removeAll();
179         }
180     }
181 
182     /**
183      * This returns whether this connector will cache search results.
184      * 
185      * @return true if results are being cached
186      */
187     public boolean isCacheResults() {
188         return resultsCache != null;
189     }
190 
191     /**
192      * This returns whether this connector will throw an exception if no search results are found.
193      * 
194      * @return true if searches which return no results are considered an error
195      */
196     public boolean isNoResultsIsError() {
197         return noResultsIsError;
198     }
199 
200     /**
201      * This sets whether this connector will throw an exception if no search results are found.
202      * 
203      * @param isError true if searches which return no results are considered an error, false otherwise
204      */
205     public void setNoResultsIsError(boolean isError) {
206         noResultsIsError = isError;
207     }
208     
209     /**
210      * Gets the engine used to evaluate the query template.
211      * 
212      * @return engine used to evaluate the query template
213      */
214     public TemplateEngine getTemplateEngine() {
215         return filterCreator;
216     }
217 
218     /**
219      * Gets the template used to create queries.
220      * 
221      * @return template used to create queries
222      */
223     public String getFilterTemplate() {
224         return filterTemplate;
225     }
226 
227     /**
228      * This returns the ldap pool strategy this connector is using.
229      * 
230      * @return ldap pool strategy
231      */
232     public LdapPoolStrategy getLdapPool() {
233         return ldapPool;
234     }
235 
236     /**
237      * This returns the attributes that all searches will request from the ldap.
238      * 
239      * @return <code>String[]</code>
240      */
241     public String[] getReturnAttributes() {
242         return returnAttributes;
243     }
244 
245     /**
246      * This sets the attributes that all searches will request from the ldap. This method will remove any cached
247      * results.
248      * 
249      * @see #clearCache()
250      * 
251      * @param attributes <code>String[]</code>
252      */
253     public void setReturnAttributes(String[] attributes) {
254         returnAttributes = attributes;
255     }
256 
257     /**
258      * This sets the attributes that all searches will request from the ldap. s should be a comma delimited string.
259      * 
260      * @param s <code>String[]</code> comma delimited returnAttributes
261      */
262     public void setReturnAttributes(String s) {
263         StringTokenizer st = new StringTokenizer(s, ",");
264         String[] ra = new String[st.countTokens()];
265         for (int count = 0; count < st.countTokens(); count++) {
266             ra[count] = st.nextToken();
267         }
268         setReturnAttributes(ra);
269     }
270 
271     /** {@inheritDoc} */
272     public void validate() throws AttributeResolutionException {
273         Ldap ldap = null;
274         try {
275             ldap = ldapPool.checkOut();
276             if(ldap == null){
277                 log.error("Unable to retrieve an LDAP connection");
278                 throw new AttributeResolutionException("Unable to retrieve LDAP connection");
279             }
280             if (!ldap.connect()) {
281                 throw new NamingException();
282             }
283         } catch (NamingException e) {
284             log.error("An error occured when attempting to search the LDAP: " + ldap.getLdapConfig().getEnvironment(),
285                     e);
286             throw new AttributeResolutionException("An error occurred when attempting to search the LDAP", e);
287         } catch (Exception e) {
288             log.error("Could not retrieve Ldap object from pool", e);
289             throw new AttributeResolutionException(
290                     "An error occurred when attempting to retrieve a LDAP connection from the pool", e);
291         } finally {
292             if (ldap != null) {
293                 try {
294                     ldapPool.checkIn(ldap);
295                 } catch (Exception e) {
296                     log.error("Could not return Ldap object back to pool", e);
297                 }
298             }
299         }
300     }
301 
302     /** {@inheritDoc} */
303     public Map<String, BaseAttribute> resolve(ShibbolethResolutionContext resolutionContext)
304             throws AttributeResolutionException {
305         String searchFilter = filterCreator.createStatement(filterTemplateName, resolutionContext, getDependencyIds(),
306                 escapingStrategy);
307         searchFilter = searchFilter.trim();
308         log.debug("Search filter: {}", searchFilter);
309 
310         // attempt to get attributes from the cache
311         Map<String, BaseAttribute> attributes = retrieveAttributesFromCache(searchFilter);
312 
313         // results not found in the cache
314         if (attributes == null) {
315             Iterator<SearchResult> results = searchLdap(searchFilter);
316 
317             if (noResultsIsError && !results.hasNext()) {
318                 log.debug("LDAP data connector " + getId()
319                         + " - No result returned and connector configured to treat this as an error.");
320                 throw new AttributeResolutionException("No LDAP entry found for "
321                         + resolutionContext.getAttributeRequestContext().getPrincipalName());
322             }
323 
324             // build resolved attributes from LDAP attributes and cache the result
325             attributes = buildBaseAttributes(results);
326             cacheResult(searchFilter, attributes);
327         }
328 
329         return attributes;
330     }
331 
332     /**
333      * This retrieves any cached attributes for the supplied resolution context. Returns null if nothing is cached.
334      * 
335      * @param searchFilter the search filter the produced the attributes
336      * 
337      * @return <code>Map</code> of attributes IDs to attributes
338      */
339     protected Map<String, BaseAttribute> retrieveAttributesFromCache(String searchFilter) {
340         if (!isCacheResults()) {
341             return null;
342         }
343 
344         log.debug("LDAP data connector {} - Checking cache for search results", getId());
345         Element cachedResult = resultsCache.get(searchFilter);
346         if (cachedResult != null && !cachedResult.isExpired()) {
347             log.debug("LDAP data connector {} - Returning attributes from cache", getId());
348             return (Map<String, BaseAttribute>) cachedResult.getObjectValue();
349         }
350 
351         log.debug("LDAP data connector {} - No results cached for search filter '{}'", getId(), searchFilter);
352         return null;
353     }
354 
355     /**
356      * This searches the LDAP with the supplied filter.
357      * 
358      * @param searchFilter <code>String</code> the searchFilter that produced the attributes
359      * @return <code>Iterator</code> of search results
360      * @throws AttributeResolutionException if an error occurs performing the search
361      */
362     protected Iterator<SearchResult> searchLdap(String searchFilter) throws AttributeResolutionException {
363         log.debug("LDAP data connector {} - Retrieving attributes from LDAP", getId());
364 
365         Ldap ldap = null;
366         try {
367             ldap = ldapPool.checkOut();
368             return ldap.search(new SearchFilter(searchFilter), returnAttributes);
369         } catch (NamingException e) {
370             log.debug("LDAP data connector " + getId() + " - An error occured when attempting to search the LDAP: "
371                     + ldap.getLdapConfig().getEnvironment(), e);
372             throw new AttributeResolutionException("An error occurred when attempting to search the LDAP");
373         } catch (Exception e) {
374             log.debug("LDAP data connector " + getId() + " - Could not perform ldap search", e);
375             throw new AttributeResolutionException("An error occurred when attempting to perform a LDAP search");
376         } finally {
377             if (ldap != null) {
378                 try {
379                     ldapPool.checkIn(ldap);
380                 } catch (Exception e) {
381                     log.error("LDAP data connector " + getId() + " - Could not return Ldap object back to pool", e);
382                 }
383             }
384         }
385     }
386 
387     /**
388      * This returns a map of attribute ids to attributes from the supplied search results.
389      * 
390      * @param results <code>Iterator</code> of LDAP search results
391      * @return <code>Map</code> of attribute ids to attributes
392      * @throws AttributeResolutionException if an error occurs parsing attribute results
393      */
394     protected Map<String, BaseAttribute> buildBaseAttributes(Iterator<SearchResult> results)
395             throws AttributeResolutionException {
396 
397         Map<String, BaseAttribute> attributes = new HashMap<String, BaseAttribute>();
398 
399         if (!results.hasNext()) {
400             return attributes;
401         }
402 
403         SearchResult sr = results.next();
404         LdapAttributes ldapAttrs = null;
405         try {
406             ldapAttrs = LdapBeanProvider.getLdapBeanFactory().newLdapAttributes();
407             ldapAttrs.addAttributes(sr.getAttributes());
408         } catch (NamingException e) {
409             log.debug("LDAP data connector " + getId() + " - Error parsing LDAP attributes", e);
410             throw new AttributeResolutionException("Error parsing LDAP attributes", e);
411         }
412 
413         for (LdapAttribute ldapAttr : ldapAttrs.getAttributes()) {
414             log.debug("LDAP data connector {} - Found the following attribute: {}", getId(), ldapAttr);
415             BaseAttribute attribute = attributes.get(ldapAttr.getName());
416             if (attribute == null) {
417                 attribute = new BasicAttribute<String>(ldapAttr.getName());
418                 attributes.put(ldapAttr.getName(), attribute);
419             }
420 
421             Set<Object> values = ldapAttr.getValues();
422             if (values != null && !values.isEmpty()) {
423                 for (Object value : values) {
424                     if (value instanceof String) {
425                         String s = (String) value;
426                         if (!DatatypeHelper.isEmpty(s)) {
427                             attribute.getValues().add(DatatypeHelper.safeTrimOrNullString(s));
428                         }
429                     } else {
430                         log.debug("LDAP data connector {} - Attribute {} contained a value that is not of type String",
431                                 getId(), ldapAttr.getName());
432                         attribute.getValues().add(value);
433                     }
434                 }
435             }
436         }
437 
438         return attributes;
439     }
440 
441     /**
442      * This stores the supplied attributes in the cache.
443      * 
444      * @param searchFilter the searchFilter that produced the attributes
445      * @param attributes <code>Map</code> of attribute IDs to attributes
446      */
447     protected void cacheResult(String searchFilter, Map<String, BaseAttribute> attributes) {
448         if (!isCacheResults()) {
449             return;
450         }
451 
452         log.debug("LDAP data connector {} - Caching attributes from search '{}'", getId(), searchFilter);
453         resultsCache.put(new Element(searchFilter, attributes));
454     }
455 
456     /**
457      * Escapes values that will be included within an LDAP filter.
458      */
459     protected class LDAPValueEscapingStrategy implements CharacterEscapingStrategy {
460 
461         /** {@inheritDoc} */
462         public String escape(String value) {
463             return value.replace("*", "\\*").replace("(", "\\(").replace(")", "\\)").replace("\\", "\\");
464         }
465     }
466 }