View Javadoc
1   /**
2    * Copyright 2005-2014 The Kuali Foundation
3    *
4    * Licensed under the Educational Community 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.opensource.org/licenses/ecl2.php
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  package org.kuali.rice.kim.dao.impl;
17  
18  import org.apache.commons.lang.StringUtils;
19  import org.kuali.rice.coreservice.framework.parameter.ParameterService;
20  import org.kuali.rice.kim.api.identity.entity.Entity;
21  import org.kuali.rice.kim.api.identity.entity.EntityDefault;
22  import org.kuali.rice.kim.api.identity.principal.EntityNamePrincipalName;
23  import org.kuali.rice.kim.api.identity.principal.Principal;
24  import org.kuali.rice.kim.api.identity.privacy.EntityPrivacyPreferences;
25  import org.kuali.rice.kim.dao.LdapPrincipalDao;
26  import org.kuali.rice.kim.impl.identity.PersonImpl;
27  import org.kuali.rice.kim.ldap.InvalidLdapEntityException;
28  import org.kuali.rice.kim.util.Constants;
29  import org.springframework.ldap.SizeLimitExceededException;
30  import org.springframework.ldap.core.ContextMapper;
31  import org.springframework.ldap.core.ContextMapperCallbackHandler;
32  import org.springframework.ldap.core.DistinguishedName;
33  import org.springframework.ldap.core.LdapTemplate;
34  import org.springframework.ldap.filter.AndFilter;
35  import org.springframework.ldap.filter.LikeFilter;
36  import org.springframework.ldap.filter.NotFilter;
37  import org.springframework.ldap.filter.OrFilter;
38  
39  import javax.naming.NameClassPair;
40  import javax.naming.directory.SearchControls;
41  import java.util.ArrayList;
42  import java.util.Arrays;
43  import java.util.HashMap;
44  import java.util.List;
45  import java.util.Map;
46  import java.util.regex.Matcher;
47  import java.util.regex.Pattern;
48  
49  import static org.kuali.rice.core.util.BufferedLogger.*;
50  import static org.kuali.rice.kns.lookup.LookupUtils.getSearchResultsLimit;
51  
52  /**
53   * Integrated Data Access via LDAP to EDS. Provides implementation to interface method
54   * for using Spring-LDAP to communicate with EDS.
55   *
56   * @author Kuali Rice Team (rice.collab@kuali.org)
57   */
58  public class LdapPrincipalDaoImpl implements LdapPrincipalDao { 
59      private Constants kimConstants;
60      private LdapTemplate template;
61      private ParameterService parameterService;
62  
63      
64      private Map<String, ContextMapper> contextMappers;    
65      
66      public LdapPrincipalDaoImpl() {
67      }
68                              
69      /**
70       * In EDS, the principalId, principalName, and entityId will all be the same.
71       */
72      public Principal getPrincipal(String principalId) {
73          if (principalId == null) {
74              return null;
75          }
76          Map<String, Object> criteria = new HashMap();
77          criteria.put(getKimConstants().getKimLdapIdProperty(), principalId);
78          List<Principal> results = search(Principal.class, criteria);
79  
80          if (results.size() > 0) {
81              return results.get(0);
82          }
83  
84          return null;
85      }
86  
87      /**
88       * Assuming he principalId, principalName, and entityId will all be the same.
89       */
90      public Principal getPrincipalByName(String principalName) {
91          if (principalName == null) {
92              return null;
93          }
94          Map<String, Object> criteria = new HashMap();
95          criteria.put(getKimConstants().getKimLdapNameProperty(), principalName);
96          List<Principal> results = search(Principal.class, criteria);
97  
98          if (results.size() > 0) {
99              return results.get(0);
100         }
101         
102         return null;
103     }
104 
105     public <T> List<T> search(Class<T> type, Map<String, Object> criteria) {
106         AndFilter filter = new AndFilter();
107         
108         for (Map.Entry<String, Object> entry : criteria.entrySet()) {
109             //attempting to handle null values to prevent NPEs in this code.
110             if (entry.getValue() == null) {
111                 entry.setValue("null");
112             }
113             if (entry.getValue() instanceof Iterable) {
114                 OrFilter orFilter = new OrFilter();
115                 for (String value : (Iterable<String>) entry.getValue()) {
116                     if (value.startsWith("!")) {
117                         orFilter.or(new NotFilter(new LikeFilter(entry.getKey(), value.substring(1))));
118                     } else {
119                         orFilter.or(new LikeFilter(entry.getKey(), value));
120                     }
121                 }
122                 filter.and(orFilter);
123             }
124             else {
125                 if (((String)entry.getValue()).startsWith("!")) {
126                     filter.and(new NotFilter(new LikeFilter(entry.getKey(), ((String)entry.getValue()).substring(1))));
127                 } else {
128                     filter.and(new LikeFilter(entry.getKey(), (String) entry.getValue()));
129                 }
130             }
131         };
132         
133         info("Using filter ", filter);
134 
135         debug("Looking up mapper for ", type.getSimpleName());
136         final ContextMapper customMapper = contextMappers.get(type.getSimpleName());
137 
138         ContextMapperCallbackHandler callbackHandler = new CustomContextMapperCallbackHandler(customMapper);
139         
140         try {
141             getLdapTemplate().search(DistinguishedName.EMPTY_PATH, 
142                                      filter.encode(), 
143                                      getSearchControls(), callbackHandler);
144         }
145         catch (SizeLimitExceededException e) {
146             // Ignore this. We want to limit our results.
147         }
148 
149         return callbackHandler.getList();
150     }
151 
152     protected SearchControls getSearchControls() {
153         SearchControls retval = new SearchControls();
154         retval.setCountLimit(getSearchResultsLimit(PersonImpl.class).longValue());
155         retval.setSearchScope(SearchControls.SUBTREE_SCOPE);
156         return retval;
157     }
158 
159 	/**
160      * FIND entity objects based on the given criteria. 
161      * 
162      * @param entityId of user/person to grab entity information for
163      * @return {@link Entity}
164      */
165 	public Entity getEntity(String entityId) {
166 	    if (entityId == null) {
167 	        return null;
168 	    }
169         Map<String, Object> criteria = new HashMap();
170         criteria.put(getKimConstants().getKimLdapIdProperty(), entityId);
171 
172         List<Entity> results = search(Entity.class, criteria);
173 
174         debug("Got results from info lookup ", results, " with size ", results.size());
175 
176         if (results.size() > 0) {
177             return results.get(0);
178         }
179         
180         return null;
181     }
182 	
183 	/**
184 	 * Fetches full entity info, populated from EDS, based on the Entity's principal id
185 	 * @param principalId the principal id to look the entity up for
186 	 * @return the corresponding entity info
187 	 */
188 	public Entity getEntityByPrincipalId(String principalId) {
189 	    if (principalId == null) {
190 	        return null;
191 	    }
192 	   final Principal principal = getPrincipal(principalId);
193 	   if (principal != null && !StringUtils.isBlank(principal.getEntityId())) {
194 	       return getEntity(principal.getEntityId());
195 	   }
196 	   return null;
197 	}
198 
199 	public EntityDefault getEntityDefault(String entityId) {
200 	    if (entityId == null) {
201 	        return null;
202 	    }
203         Map<String, Object> criteria = new HashMap();
204         criteria.put(getKimConstants().getKimLdapIdProperty(), entityId);
205 
206         List<EntityDefault> results = search(EntityDefault.class, criteria);
207 
208         debug("Got results from info lookup ", results, " with size ", results.size());
209 
210         if (results.size() > 0) {
211             return results.get(0);
212         }
213         
214         return null;
215     }
216 
217     /**
218      * entityid and principalId are treated as the same.
219      * 
220      * @see #getEntityDefaultInfo(String)
221      */
222 	public EntityDefault getEntityDefaultByPrincipalId(String principalId) {
223         return getEntityDefault(principalId);
224     }
225 
226 	public EntityDefault getEntityDefaultByPrincipalName(String principalName) {
227         Map<String, Object> criteria = new HashMap();
228         criteria.put(getKimConstants().getKimLdapNameProperty(), principalName);
229 
230         List<EntityDefault> results = search(EntityDefault.class, criteria);
231         if (results.size() > 0) {
232             return results.get(0);
233         }
234         
235         return null;
236     }
237 
238 	public Entity getEntityByPrincipalName(String principalName) {
239         Map<String, Object> criteria = new HashMap();
240         criteria.put(getKimConstants().getKimLdapNameProperty(), principalName);
241 
242         List<Entity> results = search(Entity.class, criteria);
243         if (results.size() > 0) {
244             return results.get(0);
245         }
246         
247         return null;
248     }
249 
250 	public List<EntityDefault> lookupEntityDefault(Map<String,String> searchCriteria, boolean unbounded) {
251         List<EntityDefault> results = new ArrayList();
252         Map<String, Object> criteria = getLdapLookupCriteria(searchCriteria);
253         
254         results = search(EntityDefault.class, criteria);
255 
256         return results;
257     }
258 
259 	public List<String> lookupEntityIds(Map<String,String> searchCriteria) {
260         final List<String> results = new ArrayList<String>();
261         final Map<String, Object> criteria = getLdapLookupCriteria(searchCriteria);
262         
263         for (final Entity entity : search(Entity.class, criteria)) {
264             results.add(entity.getId());
265         }
266         
267         return results;
268     }
269     
270     /**
271      * Converts Kuali Lookup parameters into LDAP query parameters
272      * @param searchCriteria kuali lookup info
273      * @return {@link Map} of LDAP query info
274      */
275     protected Map<String, Object> getLdapLookupCriteria(Map<String, String> searchCriteria) {
276         Map<String, Object> criteria = new HashMap();
277         boolean hasTaxId = false;
278         
279         for (Map.Entry<String, String> criteriaEntry : searchCriteria.entrySet()) {
280             debug(String.format("Searching with criteria %s = %s", criteriaEntry.getKey(), criteriaEntry.getValue()));
281             String valueName = criteriaEntry.getKey();            
282             Object value = criteriaEntry.getValue();
283             if (!criteriaEntry.getValue().equals("*")) {
284                 valueName = String.format("%s.%s", criteriaEntry.getKey(), criteriaEntry.getValue());
285             }
286 
287             if (!value.equals("*") && isMapped(valueName)) {
288                 value = getLdapValue(valueName);
289                 debug(value, " mapped to valueName ", valueName);
290             }
291         
292             if (isMapped(criteriaEntry.getKey())) {
293                 debug(String.format("Setting attribute to (%s, %s)", 
294                                     getLdapAttribute(criteriaEntry.getKey()), 
295                                     value));
296                 final String key = getLdapAttribute(criteriaEntry.getKey());
297                 if (!criteria.containsKey(key)) {
298                     criteria.put(key, value);
299                 }
300             }
301             else if (criteriaEntry.getKey().equalsIgnoreCase(getKimConstants().getExternalIdProperty())) {
302                 criteria.put(getKimConstants().getKimLdapIdProperty(), value);
303             }
304             else if (criteriaEntry.getKey().equalsIgnoreCase(getKimConstants().getExternalIdTypeProperty()) 
305                      && value.toString().equals(getKimConstants().getTaxExternalIdTypeCode())) {
306                 hasTaxId = true;
307             }
308         }
309         return criteria;
310     }
311 
312 	public EntityPrivacyPreferences getEntityPrivacyPreferences(String entityId) {
313 	    if (entityId == null) {
314 	        return null;
315 	    }
316         Map<String, Object> criteria = new HashMap();
317         criteria.put(getKimConstants().getKimLdapIdProperty(), entityId);
318 
319         List<EntityPrivacyPreferences> results = search(EntityPrivacyPreferences.class, criteria);
320         if (results.size() > 0) {
321             return results.get(0);
322         }
323         
324         return null;
325     }
326 	
327     public Map<String, EntityNamePrincipalName> getDefaultNamesForPrincipalIds(List<String> principalIds) {
328         Map<String, Object> criteria = new HashMap();
329         Map<String, EntityNamePrincipalName> retval = new HashMap();
330         criteria.put(getKimConstants().getKimLdapIdProperty(), principalIds);
331 
332         List<EntityNamePrincipalName> results = search(EntityNamePrincipalName.class, criteria);
333 
334         for (EntityNamePrincipalName nameInfo : results) {
335             retval.put(nameInfo.getPrincipalName(), nameInfo);
336         }
337         return retval;
338     }
339 
340     public Map<String, EntityNamePrincipalName> getDefaultNamesForEntityIds(List<String> entityIds) {
341         return getDefaultNamesForPrincipalIds(entityIds);
342     }
343 
344     protected Matcher getKimAttributeMatcher(String kimAttribute) {
345         String mappedParamValue = getParameterService().getParameterValueAsString(getKimConstants().getParameterNamespaceCode(),
346                                                                                   getKimConstants().getParameterDetailTypeCode(),
347                                                                                   getKimConstants().getMappedParameterName());
348 
349         String regexStr = String.format("(%s|.*;%s)=([^=;]*).*", kimAttribute, kimAttribute);
350         debug("Matching KIM attribute with regex ", regexStr);
351         Matcher retval = Pattern.compile(regexStr).matcher(mappedParamValue);
352         
353         if (!retval.matches()) {
354             mappedParamValue = getParameterService().getParameterValueAsString(getKimConstants().getParameterNamespaceCode(),
355                                                                           getKimConstants().getParameterDetailTypeCode(),
356                                                                           getKimConstants().getMappedValuesName());
357             retval = Pattern.compile(regexStr).matcher(mappedParamValue);
358         }
359 
360         return retval;
361     }
362 
363     protected boolean isMapped(String kimAttribute) {
364         debug("Matching " + kimAttribute);
365         debug("Does ", kimAttribute, " match? ", getKimAttributeMatcher(kimAttribute).matches());
366         return getKimAttributeMatcher(kimAttribute).matches();
367     }
368 
369     protected String getLdapAttribute(String kimAttribute) {
370         Matcher matcher = getKimAttributeMatcher(kimAttribute);
371         debug("Does ", kimAttribute, " match? ", matcher.matches());
372         if (matcher.matches()) { 
373             return matcher.group(2);
374         } else {
375             return null;
376         }
377     }
378 
379     protected Object getLdapValue(String kimAttribute) {
380         Matcher matcher = getKimAttributeMatcher(kimAttribute);
381         debug("Does ", kimAttribute, " match? ", matcher.matches());
382         if (!matcher.matches()) {
383             return null;
384         }
385         String value = matcher.group(2);
386 
387         // If it's actually a list. It can only be a list if there are commas
388         if (value.contains(",")) {
389             return Arrays.asList(value.split(","));
390         }
391 
392         return value;
393     }
394 
395     public void setKimConstants(Constants constants) {
396         this.kimConstants = constants;
397     }
398 
399     public Constants getKimConstants() {
400         return kimConstants;
401     }
402 
403     public ParameterService getParameterService() {
404         return this.parameterService;
405     }
406 
407     public void setParameterService(ParameterService service) {
408         this.parameterService = service;
409     }
410 
411     public LdapTemplate getLdapTemplate() {
412         return template;
413     }
414 
415     public void setLdapTemplate(LdapTemplate template) {
416         this.template = template;
417     }
418     
419     public Map<String, ContextMapper> getContextMappers() {
420         return this.contextMappers;
421     }
422 
423     public void setContextMappers(final Map<String, ContextMapper> contextMappers) {
424         this.contextMappers = contextMappers;
425     }
426 
427     /**
428      * Overrides the existing {@link ContextMapperCallbackHandler} because we want to 
429      * intercede when there is invalid results from EDS.
430      * 
431      * @author Leo Przybylski (przybyls@arizona.edu)
432      */
433     private static final class CustomContextMapperCallbackHandler extends ContextMapperCallbackHandler {
434         public CustomContextMapperCallbackHandler(ContextMapper mapper) {
435             super(mapper);
436         }
437         
438         public void handleNameClassPair(NameClassPair nameClassPair) {
439             try {
440                 super.handleNameClassPair(nameClassPair);
441             }
442             catch (InvalidLdapEntityException ieee) {
443                 warn("LDAP Search Results yielded an invalid result from ", nameClassPair);
444             }
445         }
446     }
447 }