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.util.Constants;
028import org.springframework.ldap.SizeLimitExceededException;
029import org.springframework.ldap.core.ContextMapper;
030import org.springframework.ldap.core.ContextMapperCallbackHandler;
031import org.springframework.ldap.core.DistinguishedName;
032import org.springframework.ldap.core.LdapTemplate;
033import org.springframework.ldap.filter.AndFilter;
034import org.springframework.ldap.filter.LikeFilter;
035import org.springframework.ldap.filter.NotFilter;
036import org.springframework.ldap.filter.OrFilter;
037
038import javax.naming.directory.SearchControls;
039import java.util.ArrayList;
040import java.util.Arrays;
041import java.util.HashMap;
042import java.util.List;
043import java.util.Map;
044import java.util.regex.Matcher;
045import java.util.regex.Pattern;
046
047import static org.kuali.rice.core.util.BufferedLogger.*;
048import static org.kuali.rice.kns.lookup.LookupUtils.getSearchResultsLimit;
049
050/**
051 * Integrated Data Access via LDAP to EDS. Provides implementation to interface method
052 * for using Spring-LDAP to communicate with EDS.
053 *
054 * @author Kuali Rice Team (rice.collab@kuali.org)
055 */
056public class LdapPrincipalDaoImpl implements LdapPrincipalDao { 
057    private Constants kimConstants;
058    private LdapTemplate template;
059    private ParameterService parameterService;
060
061    
062    private Map<String, ContextMapper> contextMappers;    
063    
064    public LdapPrincipalDaoImpl() {
065    }
066                            
067    /**
068     * In EDS, the principalId, principalName, and entityId will all be the same.
069     */
070    public Principal getPrincipal(String principalId) {
071        if (principalId == null) {
072            return null;
073        }
074        Map<String, Object> criteria = new HashMap();
075        criteria.put(getKimConstants().getKimLdapIdProperty(), principalId);
076        List<Principal> results = search(Principal.class, criteria);
077
078        if (results.size() > 0) {
079            return results.get(0);
080        }
081
082        return null;
083    }
084
085    /**
086     * Assuming he principalId, principalName, and entityId will all be the same.
087     */
088    public Principal getPrincipalByName(String principalName) {
089        if (principalName == null) {
090            return null;
091        }
092        Map<String, Object> criteria = new HashMap();
093        criteria.put(getKimConstants().getKimLdapNameProperty(), principalName);
094        List<Principal> results = search(Principal.class, criteria);
095
096        if (results.size() > 0) {
097            return results.get(0);
098        }
099        
100        return null;
101    }
102
103    public <T> List<T> search(Class<T> type, Map<String, Object> criteria) {
104        AndFilter filter = new AndFilter();
105        
106        for (Map.Entry<String, Object> entry : criteria.entrySet()) {
107            //attempting to handle null values to prevent NPEs in this code.
108            if (entry.getValue() == null) {
109                entry.setValue("null");
110            }
111            if (entry.getValue() instanceof Iterable) {
112                OrFilter orFilter = new OrFilter();
113                for (String value : (Iterable<String>) entry.getValue()) {
114                    if (value.startsWith("!")) {
115                        orFilter.or(new NotFilter(new LikeFilter(entry.getKey(), value.substring(1))));
116                    } else {
117                        orFilter.or(new LikeFilter(entry.getKey(), value));
118                    }
119                }
120                filter.and(orFilter);
121            }
122            else {
123                if (((String)entry.getValue()).startsWith("!")) {
124                    filter.and(new NotFilter(new LikeFilter(entry.getKey(), ((String)entry.getValue()).substring(1))));
125                } else {
126                    filter.and(new LikeFilter(entry.getKey(), (String) entry.getValue()));
127                }
128            }
129        };
130        
131        info("Using filter ", filter);
132
133        debug("Looking up mapper for ", type.getSimpleName());
134        final ContextMapper customMapper = contextMappers.get(type.getSimpleName());
135
136        ContextMapperCallbackHandler callbackHandler = new CustomContextMapperCallbackHandler(customMapper);
137        
138        try {
139            getLdapTemplate().search(DistinguishedName.EMPTY_PATH, 
140                                     filter.encode(), 
141                                     getSearchControls(), callbackHandler);
142        }
143        catch (SizeLimitExceededException e) {
144            // Ignore this. We want to limit our results.
145        }
146
147        return callbackHandler.getList();
148    }
149
150    protected SearchControls getSearchControls() {
151        SearchControls retval = new SearchControls();
152        retval.setCountLimit(getSearchResultsLimit(PersonImpl.class).longValue());
153        retval.setSearchScope(SearchControls.SUBTREE_SCOPE);
154        return retval;
155    }
156
157        /**
158     * FIND entity objects based on the given criteria. 
159     * 
160     * @param entityId of user/person to grab entity information for
161     * @return {@link Entity}
162     */
163        public Entity getEntity(String entityId) {
164            if (entityId == null) {
165                return null;
166            }
167        Map<String, Object> criteria = new HashMap();
168        criteria.put(getKimConstants().getKimLdapIdProperty(), entityId);
169
170        List<Entity> results = search(Entity.class, criteria);
171
172        debug("Got results from info lookup ", results, " with size ", results.size());
173
174        if (results.size() > 0) {
175            return results.get(0);
176        }
177        
178        return null;
179    }
180        
181        /**
182         * Fetches full entity info, populated from EDS, based on the Entity's principal id
183         * @param principalId the principal id to look the entity up for
184         * @return the corresponding entity info
185         */
186        public Entity getEntityByPrincipalId(String principalId) {
187            if (principalId == null) {
188                return null;
189            }
190           final Principal principal = getPrincipal(principalId);
191           if (principal != null && !StringUtils.isBlank(principal.getEntityId())) {
192               return getEntity(principal.getEntityId());
193           }
194           return null;
195        }
196
197        public EntityDefault getEntityDefault(String entityId) {
198            if (entityId == null) {
199                return null;
200            }
201        Map<String, Object> criteria = new HashMap();
202        criteria.put(getKimConstants().getKimLdapIdProperty(), entityId);
203
204        List<EntityDefault> results = search(EntityDefault.class, criteria);
205
206        debug("Got results from info lookup ", results, " with size ", results.size());
207
208        if (results.size() > 0) {
209            return results.get(0);
210        }
211        
212        return null;
213    }
214
215    /**
216     * entityid and principalId are treated as the same.
217     * 
218     * @see #getEntityDefaultInfo(String)
219     */
220        public EntityDefault getEntityDefaultByPrincipalId(String principalId) {
221        return getEntityDefault(principalId);
222    }
223
224        public EntityDefault getEntityDefaultByPrincipalName(String principalName) {
225        Map<String, Object> criteria = new HashMap();
226        criteria.put(getKimConstants().getKimLdapNameProperty(), principalName);
227
228        List<EntityDefault> results = search(EntityDefault.class, criteria);
229        if (results.size() > 0) {
230            return results.get(0);
231        }
232        
233        return null;
234    }
235
236        public Entity getEntityByPrincipalName(String principalName) {
237        Map<String, Object> criteria = new HashMap();
238        criteria.put(getKimConstants().getKimLdapNameProperty(), principalName);
239
240        List<Entity> results = search(Entity.class, criteria);
241        if (results.size() > 0) {
242            return results.get(0);
243        }
244        
245        return null;
246    }
247
248        public List<EntityDefault> lookupEntityDefault(Map<String,String> searchCriteria, boolean unbounded) {
249        List<EntityDefault> results = new ArrayList();
250        Map<String, Object> criteria = getLdapLookupCriteria(searchCriteria);
251        
252        results = search(EntityDefault.class, criteria);
253
254        return results;
255    }
256
257        public List<String> lookupEntityIds(Map<String,String> searchCriteria) {
258        final List<String> results = new ArrayList<String>();
259        final Map<String, Object> criteria = getLdapLookupCriteria(searchCriteria);
260        
261        for (final Entity entity : search(Entity.class, criteria)) {
262            results.add(entity.getId());
263        }
264        
265        return results;
266    }
267    
268    /**
269     * Converts Kuali Lookup parameters into LDAP query parameters
270     * @param searchCriteria kuali lookup info
271     * @return {@link Map} of LDAP query info
272     */
273    protected Map<String, Object> getLdapLookupCriteria(Map<String, String> searchCriteria) {
274        Map<String, Object> criteria = new HashMap();
275        boolean hasTaxId = false;
276        
277        for (Map.Entry<String, String> criteriaEntry : searchCriteria.entrySet()) {
278            debug(String.format("Searching with criteria %s = %s", criteriaEntry.getKey(), criteriaEntry.getValue()));
279            String valueName = criteriaEntry.getKey();            
280            Object value = criteriaEntry.getValue();
281            if (!criteriaEntry.getValue().equals("*")) {
282                valueName = String.format("%s.%s", criteriaEntry.getKey(), criteriaEntry.getValue());
283            }
284
285            if (!value.equals("*") && isMapped(valueName)) {
286                value = getLdapValue(valueName);
287                debug(value, " mapped to valueName ", valueName);
288            }
289        
290            if (isMapped(criteriaEntry.getKey())) {
291                debug(String.format("Setting attribute to (%s, %s)", 
292                                    getLdapAttribute(criteriaEntry.getKey()), 
293                                    value));
294                final String key = getLdapAttribute(criteriaEntry.getKey());
295                if (!criteria.containsKey(key)) {
296                    criteria.put(key, value);
297                }
298            }
299            else if (criteriaEntry.getKey().equalsIgnoreCase(getKimConstants().getExternalIdProperty())) {
300                criteria.put(getKimConstants().getKimLdapIdProperty(), value);
301            }
302            else if (criteriaEntry.getKey().equalsIgnoreCase(getKimConstants().getExternalIdTypeProperty()) 
303                     && value.toString().equals(getKimConstants().getTaxExternalIdTypeCode())) {
304                hasTaxId = true;
305            }
306        }
307        return criteria;
308    }
309
310        public EntityPrivacyPreferences getEntityPrivacyPreferences(String entityId) {
311            if (entityId == null) {
312                return null;
313            }
314        Map<String, Object> criteria = new HashMap();
315        criteria.put(getKimConstants().getKimLdapIdProperty(), entityId);
316
317        List<EntityPrivacyPreferences> results = search(EntityPrivacyPreferences.class, criteria);
318        if (results.size() > 0) {
319            return results.get(0);
320        }
321        
322        return null;
323    }
324        
325    public Map<String, EntityNamePrincipalName> getDefaultNamesForPrincipalIds(List<String> principalIds) {
326        Map<String, Object> criteria = new HashMap();
327        Map<String, EntityNamePrincipalName> retval = new HashMap();
328        criteria.put(getKimConstants().getKimLdapIdProperty(), principalIds);
329
330        List<EntityNamePrincipalName> results = search(EntityNamePrincipalName.class, criteria);
331
332        for (EntityNamePrincipalName nameInfo : results) {
333            retval.put(nameInfo.getPrincipalName(), nameInfo);
334        }
335        return retval;
336    }
337
338    public Map<String, EntityNamePrincipalName> getDefaultNamesForEntityIds(List<String> entityIds) {
339        return getDefaultNamesForPrincipalIds(entityIds);
340    }
341
342    protected Matcher getKimAttributeMatcher(String kimAttribute) {
343        String mappedParamValue = getParameterService().getParameterValueAsString(getKimConstants().getParameterNamespaceCode(),
344                                                                                  getKimConstants().getParameterDetailTypeCode(),
345                                                                                  getKimConstants().getMappedParameterName());
346
347        String regexStr = String.format("(%s|.*;%s)=([^=;]*).*", kimAttribute, kimAttribute);
348        debug("Matching KIM attribute with regex ", regexStr);
349        Matcher retval = Pattern.compile(regexStr).matcher(mappedParamValue);
350        
351        if (!retval.matches()) {
352            mappedParamValue = getParameterService().getParameterValueAsString(getKimConstants().getParameterNamespaceCode(),
353                                                                          getKimConstants().getParameterDetailTypeCode(),
354                                                                          getKimConstants().getMappedValuesName());
355            retval = Pattern.compile(regexStr).matcher(mappedParamValue);
356        }
357
358        return retval;
359    }
360
361    protected boolean isMapped(String kimAttribute) {
362        debug("Matching " + kimAttribute);
363        debug("Does ", kimAttribute, " match? ", getKimAttributeMatcher(kimAttribute).matches());
364        return getKimAttributeMatcher(kimAttribute).matches();
365    }
366
367    protected String getLdapAttribute(String kimAttribute) {
368        Matcher matcher = getKimAttributeMatcher(kimAttribute);
369        debug("Does ", kimAttribute, " match? ", matcher.matches());
370        if (matcher.matches()) { 
371            return matcher.group(2);
372        } else {
373            return null;
374        }
375    }
376
377    protected Object getLdapValue(String kimAttribute) {
378        Matcher matcher = getKimAttributeMatcher(kimAttribute);
379        debug("Does ", kimAttribute, " match? ", matcher.matches());
380        if (!matcher.matches()) {
381            return null;
382        }
383        String value = matcher.group(2);
384
385        // If it's actually a list. It can only be a list if there are commas
386        if (value.contains(",")) {
387            return Arrays.asList(value.split(","));
388        }
389
390        return value;
391    }
392
393    public void setKimConstants(Constants constants) {
394        this.kimConstants = constants;
395    }
396
397    public Constants getKimConstants() {
398        return kimConstants;
399    }
400
401    public ParameterService getParameterService() {
402        return this.parameterService;
403    }
404
405    public void setParameterService(ParameterService service) {
406        this.parameterService = service;
407    }
408
409    public LdapTemplate getLdapTemplate() {
410        return template;
411    }
412
413    public void setLdapTemplate(LdapTemplate template) {
414        this.template = template;
415    }
416    
417    public Map<String, ContextMapper> getContextMappers() {
418        return this.contextMappers;
419    }
420
421    public void setContextMappers(final Map<String, ContextMapper> contextMappers) {
422        this.contextMappers = contextMappers;
423    }
424
425    /**
426     * Overrides the existing {@link ContextMapperCallbackHandler} because we want to 
427     * intercede when there is invalid results from EDS.
428     * 
429     * @author Leo Przybylski (przybyls@arizona.edu)
430     */
431    private static final class CustomContextMapperCallbackHandler extends ContextMapperCallbackHandler {
432        public CustomContextMapperCallbackHandler(ContextMapper mapper) {
433            super(mapper);
434        }
435    }
436}