View Javadoc
1   /**
2    * Copyright 2005-2015 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.rice.krad.web.bind;
17  
18  import com.sun.accessibility.internal.resources.accessibility;
19  import org.apache.commons.lang.ObjectUtils;
20  import org.kuali.rice.core.api.CoreApiServiceLocator;
21  import org.kuali.rice.core.api.encryption.EncryptionService;
22  import org.kuali.rice.krad.uif.UifConstants;
23  import org.kuali.rice.krad.uif.lifecycle.ViewPostMetadata;
24  import org.kuali.rice.krad.uif.util.CopyUtils;
25  import org.kuali.rice.krad.uif.util.ObjectPropertyUtils;
26  import org.kuali.rice.krad.uif.view.ViewModel;
27  import org.kuali.rice.krad.util.KRADUtils;
28  import org.springframework.beans.BeanWrapperImpl;
29  import org.springframework.beans.BeansException;
30  import org.springframework.beans.NotReadablePropertyException;
31  import org.springframework.beans.NullValueInNestedPathException;
32  import org.springframework.beans.PropertyAccessorUtils;
33  import org.springframework.beans.PropertyValue;
34  import org.springframework.util.StringUtils;
35  import org.springframework.web.bind.annotation.RequestMethod;
36  import org.springframework.web.context.request.RequestContextHolder;
37  import org.springframework.web.context.request.ServletRequestAttributes;
38  
39  import javax.servlet.http.HttpServletRequest;
40  import java.beans.PropertyEditor;
41  import java.security.GeneralSecurityException;
42  import java.util.ArrayList;
43  import java.util.HashSet;
44  import java.util.List;
45  import java.util.Set;
46  import java.util.regex.Matcher;
47  import java.util.regex.Pattern;
48  
49  /**
50   * Class is a top level BeanWrapper for a UIF View Model.
51   *
52   * <p>Registers custom property editors configured on the field associated with the property name for which
53   * we are getting or setting a value. In addition determines if the field requires encryption and if so applies
54   * the {@link UifEncryptionPropertyEditorWrapper}</p>
55   *
56   * @author Kuali Rice Team (rice.collab@kuali.org)
57   */
58  public class UifViewBeanWrapper extends UifBeanWrapper {
59      private static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(UifViewBeanWrapper.class);
60  
61      // this stores all properties this wrapper has already checked
62      // with the view so the service isn't called again
63      private Set<String> processedProperties;
64  
65      private final UifBeanPropertyBindingResult bindingResult;
66  
67      public UifViewBeanWrapper(ViewModel model, UifBeanPropertyBindingResult bindingResult) {
68          super(model);
69  
70          this.bindingResult = bindingResult;
71          this.processedProperties = new HashSet<String>();
72      }
73  
74  //    /**
75  //     * {@inheritDoc}
76  //     */
77  //    @Override
78  //    public Class<?> getPropertyType(String propertyName) throws BeansException {
79  //        try {
80  //            PropertyDescriptor pd = getPropertyDescriptorInternal(propertyName);
81  //            if (pd != null) {
82  //                return pd.getPropertyType();
83  //            }
84  //
85  //            // Maybe an indexed/mapped property...
86  //            Object value = super.getPropertyValue(propertyName);
87  //            if (value != null) {
88  //                return value.getClass();
89  //            }
90  //
91  //            // Check to see if there is a custom editor,
92  //            // which might give an indication on the desired target type.
93  //            Class<?> editorType = guessPropertyTypeFromEditors(propertyName);
94  //            if (editorType != null) {
95  //                return editorType;
96  //            }
97  //        } catch (InvalidPropertyException ex) {
98  //            // Consider as not determinable.
99  //        }
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 = getBeanWrapperForPropertyPath(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 getBeanWrapperForPropertyPath(String propertyPath) {
499         BeanWrapperImpl beanWrapper = super.getBeanWrapperForPropertyPath(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 }