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