001/** 002 * Copyright 2005-2018 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.krad.web.bind; 017 018import com.sun.accessibility.internal.resources.accessibility; 019import org.apache.commons.lang.ObjectUtils; 020import org.kuali.rice.core.api.CoreApiServiceLocator; 021import org.kuali.rice.core.api.encryption.EncryptionService; 022import org.kuali.rice.krad.uif.UifConstants; 023import org.kuali.rice.krad.uif.lifecycle.ViewPostMetadata; 024import org.kuali.rice.krad.uif.util.CopyUtils; 025import org.kuali.rice.krad.uif.util.ObjectPropertyUtils; 026import org.kuali.rice.krad.uif.view.ViewModel; 027import org.kuali.rice.krad.util.KRADUtils; 028import org.springframework.beans.BeanWrapperImpl; 029import org.springframework.beans.BeansException; 030import org.springframework.beans.NotReadablePropertyException; 031import org.springframework.beans.NullValueInNestedPathException; 032import org.springframework.beans.PropertyAccessorUtils; 033import org.springframework.beans.PropertyValue; 034import org.springframework.util.StringUtils; 035import org.springframework.web.bind.annotation.RequestMethod; 036import org.springframework.web.context.request.RequestContextHolder; 037import org.springframework.web.context.request.ServletRequestAttributes; 038 039import javax.servlet.http.HttpServletRequest; 040import java.beans.PropertyEditor; 041import java.security.GeneralSecurityException; 042import java.util.ArrayList; 043import java.util.HashSet; 044import java.util.List; 045import java.util.Set; 046import java.util.regex.Matcher; 047import java.util.regex.Pattern; 048 049/** 050 * Class is a top level BeanWrapper for a UIF View Model. 051 * 052 * <p>Registers custom property editors configured on the field associated with the property name for which 053 * we are getting or setting a value. In addition determines if the field requires encryption and if so applies 054 * the {@link UifEncryptionPropertyEditorWrapper}</p> 055 * 056 * @author Kuali Rice Team (rice.collab@kuali.org) 057 */ 058public class UifViewBeanWrapper extends UifBeanWrapper { 059 private static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(UifViewBeanWrapper.class); 060 061 // this stores all properties this wrapper has already checked 062 // with the view so the service isn't called again 063 private Set<String> processedProperties; 064 065 private final UifBeanPropertyBindingResult bindingResult; 066 067 public UifViewBeanWrapper(ViewModel model, UifBeanPropertyBindingResult bindingResult) { 068 super(model); 069 070 this.bindingResult = bindingResult; 071 this.processedProperties = new HashSet<String>(); 072 } 073 074// /** 075// * {@inheritDoc} 076// */ 077// @Override 078// public Class<?> getPropertyType(String propertyName) throws BeansException { 079// try { 080// PropertyDescriptor pd = getPropertyDescriptorInternal(propertyName); 081// if (pd != null) { 082// return pd.getPropertyType(); 083// } 084// 085// // Maybe an indexed/mapped property... 086// Object value = super.getPropertyValue(propertyName); 087// if (value != null) { 088// return value.getClass(); 089// } 090// 091// // Check to see if there is a custom editor, 092// // which might give an indication on the desired target type. 093// Class<?> editorType = guessPropertyTypeFromEditors(propertyName); 094// if (editorType != null) { 095// return editorType; 096// } 097// } catch (InvalidPropertyException ex) { 098// // Consider as not determinable. 099// } 100// 101// return null; 102// } 103 104 105 /** 106 * Override to register property editors from the view before the value is retrieved. 107 * 108 * {@inheritDoc} 109 */ 110 @Override 111 protected Object getPropertyValue(String propertyName, boolean autoGrowNestedPaths) { 112 registerEditorFromView(propertyName); 113 114 return super.getPropertyValue(propertyName, autoGrowNestedPaths); 115 } 116 117 /** 118 * Attempts to find a corresponding data field for the given property name in the current view or previous view, 119 * then if the field has a property editor configured it is registered with the property editor registry to use 120 * for this property. 121 * 122 * @param propertyName name of the property to find field and editor for 123 */ 124 protected void registerEditorFromView(String propertyName) { 125 // check if we already processed this property for this BeanWrapper instance 126 if (processedProperties.contains(propertyName)) { 127 return; 128 } 129 130 if (LOG.isDebugEnabled()) { 131 LOG.debug("Attempting to find property editor for property '" + propertyName + "'"); 132 } 133 134 ViewPostMetadata viewPostMetadata = ((ViewModel) getWrappedInstance()).getViewPostMetadata(); 135 if (viewPostMetadata == null) { 136 return; 137 } 138 139 PropertyEditor propertyEditor = viewPostMetadata.getFieldEditor(propertyName); 140 if (propertyEditor != null) { 141 registerCustomEditor(null, propertyName, propertyEditor); 142 } 143 144 processedProperties.add(propertyName); 145 } 146 147 /** 148 * Overridden to perform processing before and after the value is set. 149 * 150 * <p>First binding security is checked to determine whether the path allows binding. Next, 151 * access security is checked to determine whether the value needs decrypted. Finally, if 152 * change tracking is enabled, the original value is compared with the new for indicating a 153 * modified path.</p> 154 * 155 * {@inheritDoc} 156 */ 157 @Override 158 public void setPropertyValue(PropertyValue pv) throws BeansException { 159 boolean isPropertyAccessible = checkPropertyBindingAccess(pv.getName()); 160 if (!isPropertyAccessible) { 161 return; 162 } 163 164 Object value = processValueBeforeSet(pv.getName(), pv.getValue()); 165 166 pv = new PropertyValue(pv, value); 167 168 // save off the original value if we are change tracking 169 boolean originalValueSaved = true; 170 Object originalValue = null; 171 if (bindingResult.isChangeTracking()) { 172 try { 173 originalValue = getPropertyValue(pv.getName(), true); 174 } catch (Exception e) { 175 // be failsafe here, if an exception happens here then we can't make any assumptions about whether 176 // the property value changed or not 177 originalValueSaved = false; 178 } 179 } 180 181 // set the actual property value 182 super.setPropertyValue(pv); 183 184 // if we are change tracking and we saved original value, check if it's modified 185 if (bindingResult.isChangeTracking() && originalValueSaved) { 186 try { 187 Object newValue = getPropertyValue(pv.getName()); 188 if (ObjectUtils.notEqual(originalValue, newValue)) { 189 // if they are not equal, it's been modified! 190 bindingResult.addModifiedPath(pv.getName()); 191 } 192 } catch (Exception e) { 193 // failsafe here as well 194 } 195 } 196 } 197 198 /** 199 * Overridden to perform processing before and after the value is set. 200 * 201 * <p>First binding security is checked to determine whether the path allows binding. Next, 202 * access security is checked to determine whether the value needs decrypted. Finally, if 203 * change tracking is enabled, the original value is compared with the new for indicating a 204 * modified path.</p> 205 * 206 * {@inheritDoc} 207 */ 208 @Override 209 public void setPropertyValue(String propertyName, Object value) throws BeansException { 210 boolean isPropertyAccessible = checkPropertyBindingAccess(propertyName); 211 if (!isPropertyAccessible) { 212 return; 213 } 214 215 value = processValueBeforeSet(propertyName, value); 216 217 // save off the original value 218 boolean originalValueSaved = true; 219 Object originalValue = null; 220 try { 221 originalValue = getPropertyValue(propertyName, true); 222 } catch (Exception e) { 223 // be failsafe here, if an exception happens here then we can't make any assumptions about whether 224 // the property value changed or not 225 originalValueSaved = false; 226 } 227 228 // set the actual property value 229 super.setPropertyValue(propertyName, value); 230 231 // only check if it's modified if we were able to save the original value 232 if (originalValueSaved) { 233 try { 234 Object newValue = getPropertyValue(propertyName); 235 if (ObjectUtils.notEqual(originalValue, newValue)) { 236 // if they are not equal, it's been modified! 237 bindingResult.addModifiedPath(propertyName); 238 } 239 } catch (Exception e) { 240 // failsafe here as well 241 } 242 } 243 } 244 245 /** 246 * Determines whether request binding is allowed for the given property name/path. 247 * 248 * <p>Binding access is determined by default based on the view's post metadata. A set of 249 * accessible binding paths (populated during the view lifecycle) is maintained within this data. 250 * Overrides can be specified using the annotations {@link org.kuali.rice.krad.web.bind.RequestProtected} 251 * and {@link org.kuali.rice.krad.web.bind.RequestAccessible}.</p> 252 * 253 * <p>If the path is not accessible, it is recorded in the binding results suppressed fields. Controller 254 * methods can accept the binding result and further handle these properties if necessary.</p> 255 * 256 * @param propertyName name/path of the property to check binding access for 257 * @return boolean true if binding access is allowed, false if not allowed 258 */ 259 protected boolean checkPropertyBindingAccess(String propertyName) { 260 boolean isAccessible = false; 261 262 // check for explicit property annotations that indicate access 263 Boolean bindingAnnotationAccess = checkBindingAnnotationsInPath(propertyName); 264 if (bindingAnnotationAccess != null) { 265 isAccessible = bindingAnnotationAccess.booleanValue(); 266 } else { 267 // default access, must be in view's accessible binding paths 268 ViewPostMetadata viewPostMetadata = ((ViewModel) getWrappedInstance()).getViewPostMetadata(); 269 if ((viewPostMetadata != null) && (viewPostMetadata.getAccessibleBindingPaths() != null)) { 270 isAccessible = viewPostMetadata.getAccessibleBindingPaths().contains(propertyName); 271 272 if (!isAccessible && propertyName.contains("[")) { 273 String wildcardedPropertyName = propertyName.substring(0, propertyName.lastIndexOf("[")) 274 + "[*" + propertyName.substring(propertyName.lastIndexOf("]")); 275 isAccessible = viewPostMetadata.getAccessibleBindingPaths().contains(wildcardedPropertyName); 276 } 277 } 278 } 279 280 if (!isAccessible) { 281 LOG.debug("Request parameter sent for inaccessible binding path: " + propertyName); 282 283 bindingResult.recordSuppressedField(propertyName); 284 } 285 286 return isAccessible; 287 } 288 289 /** 290 * Determines whether one of the binding annotations is present within the given property path, and if 291 * so returns whether access should be granted based on those annotation(s). 292 * 293 * <p>Binding annotations may occur anywhere in the property path. For example, if the path is 'object.field1', 294 * a binding annotation may be present on the 'object' property or the 'field1' property. If multiple annotations 295 * are found in the path, the annotation at the deepest level is taken. If both the protected and accessible 296 * annotation are found at the same level, the protected access is used.</p> 297 * 298 * @param propertyPath path to look for annotations 299 * @return Boolean true if an annotation is found and the access is allowed, false if an annotation is found 300 * and the access is protected, null if no annotations where found in the path 301 */ 302 protected Boolean checkBindingAnnotationsInPath(String propertyPath) { 303 HttpServletRequest request = 304 ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest(); 305 306 while (!StringUtils.isEmpty(propertyPath)) { 307 String nestedPath = ObjectPropertyUtils.getPathTail(propertyPath); 308 String parentPropertyPath = ObjectPropertyUtils.removePathTail(propertyPath); 309 310 Class<?> parentPropertyClass = getWrappedClass(); 311 312 // for nested paths, we need to get the class of the immediate parent 313 if (!StringUtils.isEmpty(parentPropertyPath)) { 314 parentPropertyClass = ObjectPropertyUtils.getPropertyType(getWrappedInstance(), parentPropertyPath); 315 } 316 317 // remove index or map key to get the correct property name 318 if (org.apache.commons.lang.StringUtils.endsWith(nestedPath, "]")) { 319 nestedPath = org.apache.commons.lang.StringUtils.substringBefore(nestedPath, "["); 320 } 321 322 RequestProtected protectedAnnotation = (RequestProtected) CopyUtils.getFieldAnnotation(parentPropertyClass, 323 nestedPath, RequestProtected.class); 324 if ((protectedAnnotation != null) && annotationMatchesRequestMethod(protectedAnnotation.method(), 325 request.getMethod())) { 326 return Boolean.FALSE; 327 } 328 329 RequestAccessible accessibleAnnotation = (RequestAccessible) CopyUtils.getFieldAnnotation( 330 parentPropertyClass, nestedPath, RequestAccessible.class); 331 if (accessibleAnnotation != null) { 332 boolean isAnnotationRequestMethod = annotationMatchesRequestMethod(accessibleAnnotation.method(), 333 request.getMethod()); 334 boolean isAnnotationMethodToCalls = annotationMatchesMethodToCalls(accessibleAnnotation.methodToCalls(), 335 request.getParameter(UifConstants.CONTROLLER_METHOD_DISPATCH_PARAMETER_NAME)); 336 if (isAnnotationRequestMethod && isAnnotationMethodToCalls) { 337 //((UifFormBase) this.bindingResult.getTarget()).getMethodToCall())) { 338 return Boolean.TRUE; 339 } 340 } 341 342 propertyPath = parentPropertyPath; 343 } 344 345 return null; 346 } 347 348 /** 349 * Indicates whether one of the given request accessible methods to call in the given array matches the 350 * actual methodToCall of the request. 351 * 352 * @param annotationMethodToCalls array of request accessible methods to call to check against 353 * @param methodToCall method to call of the request 354 * @return boolean true if one of the annotation methods to call match, false if none match 355 */ 356 protected boolean annotationMatchesMethodToCalls(String[] annotationMethodToCalls, String methodToCall) { 357 // empty array of methods should match all 358 if ((annotationMethodToCalls == null) || (annotationMethodToCalls.length == 0)) { 359 return true; 360 } 361 362 for (String annotationMethodToCall : annotationMethodToCalls) { 363 if (org.apache.commons.lang.StringUtils.equals(annotationMethodToCall, methodToCall)) { 364 return true; 365 } 366 } 367 368 return false; 369 } 370 371 /** 372 * Indicates whether one of the given request methods in the given array matches the actual method of 373 * the request. 374 * 375 * @param annotationMethods array of request methods to check 376 * @param requestMethod method of the request to match on 377 * @return boolean true if one of the annotation methods match, false if none match 378 */ 379 protected boolean annotationMatchesRequestMethod(RequestMethod[] annotationMethods, String requestMethod) { 380 // empty array of methods should match all 381 if ((annotationMethods == null) || (annotationMethods.length == 0)) { 382 return true; 383 } 384 385 for (RequestMethod annotationMethod : annotationMethods) { 386 if (org.apache.commons.lang.StringUtils.equals(annotationMethod.name(), requestMethod)) { 387 return true; 388 } 389 } 390 391 return false; 392 } 393 394 /** 395 * Registers any custom property editor for the property name/path, converts empty string values to null, and 396 * calls helper method to decrypt secure values. 397 * 398 * @param propertyName name of the property 399 * @param value value of the property to process 400 * @return updated (possibly) property value 401 */ 402 protected Object processValueBeforeSet(String propertyName, Object value) { 403 registerEditorFromView(propertyName); 404 405 Object processedValue = value; 406 407 // Convert blank string values to null so empty strings are not set on the form as values (useful for legacy 408 // checks) Jira: KULRICE-11424 409 if (value instanceof String) { 410 String propertyValue = (String) value; 411 412 if (StringUtils.isEmpty(propertyValue)) { 413 processedValue = null; 414 } else { 415 processedValue = decryptValueIfNecessary(propertyName, propertyValue); 416 } 417 } 418 419 return processedValue; 420 } 421 422 /** 423 * If the given property name is secure, decrypts the value by calling the encryption service. 424 * 425 * @param propertyName name of the property 426 * @param propertyValue value of the property 427 * @return String decrypted property value (or original value if not secure) 428 */ 429 protected String decryptValueIfNecessary(String propertyName, String propertyValue) { 430 // check security on field 431 boolean isSecure = isSecure(getWrappedClass(), propertyName); 432 433 if (org.apache.commons.lang.StringUtils.endsWith(propertyValue, EncryptionService.ENCRYPTION_POST_PREFIX)) { 434 propertyValue = org.apache.commons.lang.StringUtils.removeEnd(propertyValue, 435 EncryptionService.ENCRYPTION_POST_PREFIX); 436 isSecure = true; 437 } 438 439 // decrypt if the value is secure 440 if (isSecure) { 441 try { 442 if (CoreApiServiceLocator.getEncryptionService().isEnabled()) { 443 propertyValue = CoreApiServiceLocator.getEncryptionService().decrypt(propertyValue); 444 } 445 } catch (GeneralSecurityException e) { 446 throw new RuntimeException(e); 447 } 448 } 449 450 return propertyValue; 451 } 452 453 /** 454 * Checks whether the given property is secure. 455 * 456 * @param wrappedClass class the property is associated with 457 * @param propertyPath path to the property 458 * @return boolean true if the property is secure, false if not 459 */ 460 protected boolean isSecure(Class<?> wrappedClass, String propertyPath) { 461 if (KRADUtils.isSecure(propertyPath, wrappedClass)) { 462 return true; 463 } 464 465 // since this is part of a set, we want to make sure nested paths grow 466 setAutoGrowNestedPaths(true); 467 468 BeanWrapperImpl beanWrapper; 469 try { 470 beanWrapper = getPropertyAccessorForPropertyPath(propertyPath); 471 } catch (NotReadablePropertyException | NullValueInNestedPathException e) { 472 LOG.debug("Bean wrapper was not found for " + propertyPath 473 + ", but since it cannot be accessed it will not be set as secure.", e); 474 return false; 475 } 476 477 if (org.apache.commons.lang.StringUtils.isNotBlank(beanWrapper.getNestedPath())) { 478 PropertyTokenHolder tokens = getPropertyNameTokens(propertyPath); 479 String nestedPropertyPath = org.apache.commons.lang.StringUtils.removeStart(tokens.canonicalName, 480 beanWrapper.getNestedPath()); 481 482 return isSecure(beanWrapper.getWrappedClass(), nestedPropertyPath); 483 } 484 485 return false; 486 } 487 488 /** 489 * Overridden to copy property editor registration to the new bean wrapper. 490 * 491 * <p>This is necessary because spring only copies over the editors when a new bean wrapper is 492 * created. The wrapper is then cached and use for subsequent calls. But the get calls could bring in 493 * new custom editors we need to copy.</p> 494 * 495 * {@inheritDoc} 496 */ 497 @Override 498 protected BeanWrapperImpl getPropertyAccessorForPropertyPath(String propertyPath) { 499 BeanWrapperImpl beanWrapper = (BeanWrapperImpl) super.getPropertyAccessorForPropertyPath(propertyPath); 500 501 PropertyTokenHolder tokens = getPropertyNameTokens(propertyPath); 502 String canonicalName = tokens.canonicalName; 503 504 int pos = PropertyAccessorUtils.getFirstNestedPropertySeparatorIndex(canonicalName); 505 if (pos != -1) { 506 canonicalName = canonicalName.substring(0, pos); 507 } 508 509 copyCustomEditorsTo(beanWrapper, canonicalName); 510 511 return beanWrapper; 512 } 513 514 /** 515 * Parse the given property name into the corresponding property name tokens. 516 * 517 * @param propertyName the property name to parse 518 * @return representation of the parsed property tokens 519 */ 520 private PropertyTokenHolder getPropertyNameTokens(String propertyName) { 521 PropertyTokenHolder tokens = new PropertyTokenHolder(); 522 String actualName = null; 523 List<String> keys = new ArrayList<String>(2); 524 int searchIndex = 0; 525 while (searchIndex != -1) { 526 int keyStart = propertyName.indexOf(PROPERTY_KEY_PREFIX, searchIndex); 527 searchIndex = -1; 528 if (keyStart != -1) { 529 int keyEnd = propertyName.indexOf(PROPERTY_KEY_SUFFIX, keyStart + PROPERTY_KEY_PREFIX.length()); 530 if (keyEnd != -1) { 531 if (actualName == null) { 532 actualName = propertyName.substring(0, keyStart); 533 } 534 String key = propertyName.substring(keyStart + PROPERTY_KEY_PREFIX.length(), keyEnd); 535 if ((key.startsWith("'") && key.endsWith("'")) || (key.startsWith("\"") && key.endsWith("\""))) { 536 key = key.substring(1, key.length() - 1); 537 } 538 keys.add(key); 539 searchIndex = keyEnd + PROPERTY_KEY_SUFFIX.length(); 540 } 541 } 542 } 543 tokens.actualName = (actualName != null ? actualName : propertyName); 544 tokens.canonicalName = tokens.actualName; 545 if (!keys.isEmpty()) { 546 tokens.canonicalName += PROPERTY_KEY_PREFIX + 547 StringUtils.collectionToDelimitedString(keys, PROPERTY_KEY_SUFFIX + PROPERTY_KEY_PREFIX) + 548 PROPERTY_KEY_SUFFIX; 549 tokens.keys = StringUtils.toStringArray(keys); 550 } 551 return tokens; 552 } 553 554 private static class PropertyTokenHolder { 555 556 public String canonicalName; 557 558 public String actualName; 559 560 public String[] keys; 561 } 562}