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}