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}