001/**
002 * Copyright 2005-2016 The Kuali Foundation
003 *
004 * Licensed under the Educational Community License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.opensource.org/licenses/ecl2.php
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.kuali.rice.kim.dao.impl;
017
018import org.apache.commons.lang.StringUtils;
019import org.kuali.rice.coreservice.framework.parameter.ParameterService;
020import org.kuali.rice.kim.api.identity.entity.Entity;
021import org.kuali.rice.kim.api.identity.entity.EntityDefault;
022import org.kuali.rice.kim.api.identity.principal.EntityNamePrincipalName;
023import org.kuali.rice.kim.api.identity.principal.Principal;
024import org.kuali.rice.kim.api.identity.privacy.EntityPrivacyPreferences;
025import org.kuali.rice.kim.dao.LdapPrincipalDao;
026import org.kuali.rice.kim.impl.identity.PersonImpl;
027import org.kuali.rice.kim.ldap.InvalidLdapEntityException;
028import org.kuali.rice.kim.util.Constants;
029import org.springframework.ldap.SizeLimitExceededException;
030import org.springframework.ldap.core.ContextMapper;
031import org.springframework.ldap.core.ContextMapperCallbackHandler;
032import org.springframework.ldap.core.DistinguishedName;
033import org.springframework.ldap.core.LdapTemplate;
034import org.springframework.ldap.filter.AndFilter;
035import org.springframework.ldap.filter.LikeFilter;
036import org.springframework.ldap.filter.NotFilter;
037import org.springframework.ldap.filter.OrFilter;
038
039import javax.naming.NameClassPair;
040import javax.naming.directory.SearchControls;
041import java.util.ArrayList;
042import java.util.Arrays;
043import java.util.HashMap;
044import java.util.List;
045import java.util.Map;
046import java.util.regex.Matcher;
047import java.util.regex.Pattern;
048
049import static org.kuali.rice.core.util.BufferedLogger.*;
050import static org.kuali.rice.kns.lookup.LookupUtils.getSearchResultsLimit;
051
052/**
053 * Integrated Data Access via LDAP to EDS. Provides implementation to interface method
054 * for using Spring-LDAP to communicate with EDS.
055 *
056 * @author Kuali Rice Team (rice.collab@kuali.org)
057 */
058public class LdapPrincipalDaoImpl implements LdapPrincipalDao { 
059    private Constants kimConstants;
060    private LdapTemplate template;
061    private ParameterService parameterService;
062
063    
064    private Map<String, ContextMapper> contextMappers;    
065    
066    public LdapPrincipalDaoImpl() {
067    }
068                            
069    /**
070     * In EDS, the principalId, principalName, and entityId will all be the same.
071     */
072    public Principal getPrincipal(String principalId) {
073        if (principalId == null) {
074            return null;
075        }
076        Map<String, Object> criteria = new HashMap();
077        criteria.put(getKimConstants().getKimLdapIdProperty(), principalId);
078        List<Principal> results = search(Principal.class, criteria);
079
080        if (results.size() > 0) {
081            return results.get(0);
082        }
083
084        return null;
085    }
086
087    /**
088     * Assuming he principalId, principalName, and entityId will all be the same.
089     */
090    public Principal getPrincipalByName(String principalName) {
091        if (principalName == null) {
092            return null;
093        }
094        Map<String, Object> criteria = new HashMap();
095        criteria.put(getKimConstants().getKimLdapNameProperty(), principalName);
096        List<Principal> results = search(Principal.class, criteria);
097
098        if (results.size() > 0) {
099            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}