Revision: 587
http://svn.sourceforge.net/stripes/?rev=587&view=rev
Author: bengunter
Date: 2007-07-18 21:29:38 -0700 (Wed, 18 Jul 2007)
Log Message:
-----------
Resolved STS-396: Provide ability to prevent binding into named properties.
This code is not well tested yet, but if @StrictBinding is not applied to any
ActionBean then it should have no effect on existing code.
Modified Paths:
--------------
trunk/stripes/src/net/sourceforge/stripes/controller/DefaultActionBeanPropertyBinder.java
Added Paths:
-----------
trunk/stripes/src/net/sourceforge/stripes/action/StrictBinding.java
trunk/stripes/src/net/sourceforge/stripes/controller/BindingPolicyManager.java
Added: trunk/stripes/src/net/sourceforge/stripes/action/StrictBinding.java
===================================================================
--- trunk/stripes/src/net/sourceforge/stripes/action/StrictBinding.java
(rev 0)
+++ trunk/stripes/src/net/sourceforge/stripes/action/StrictBinding.java
2007-07-19 04:29:38 UTC (rev 587)
@@ -0,0 +1,78 @@
+/* Copyright 2007 Ben Gunter
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package net.sourceforge.stripes.action;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * <p>
+ * When applied to an [EMAIL PROTECTED] ActionBean}, this annotation indicates
that binding access controls
+ * are in effect. Property binding can be enabled or disabled on a
case-by-case basis through the
+ * use of the [EMAIL PROTECTED] #allow()} and [EMAIL PROTECTED] #deny()}
elements.
+ * </p>
+ * <p>
+ * Property may be named explicitly or by using globs. A single star (*)
matches any property of an
+ * element. Two stars (**) indicate any property of an element, including
properties of that
+ * property and so on. For security reasons, partial matches are not allowed
so globs like
+ * user.pass* will never match anything. Some examples:
+ * <ul>
+ * <li>[EMAIL PROTECTED] *} - any property of the [EMAIL PROTECTED]
ActionBean} itself</li>
+ * <li>[EMAIL PROTECTED] **} - any property of the [EMAIL PROTECTED]
ActionBean} itself or its properties or their
+ * properties, and so on</li>
+ * <li>[EMAIL PROTECTED] user.username, user.email} - the username and email
property of the user property of
+ * the [EMAIL PROTECTED] ActionBean}</li>
+ * <li>[EMAIL PROTECTED] user, user.*} - the user property and any property of
the user
+ * </ul>
+ * </p>
+ * <p>
+ * The [EMAIL PROTECTED] #allow()} and [EMAIL PROTECTED] #deny()} elements are
of type String[], but each string in the
+ * array may be a comma-separated list of properties. Thus the
+ * [EMAIL PROTECTED] @StrictBinding(allow="user, user.*")} is equivalent to
+ * [EMAIL PROTECTED] @StrictBinding(allow={ "user", "user.*" }}.
+ * </p>
+ *
+ * @author Ben Gunter
+ */
[EMAIL PROTECTED](RetentionPolicy.RUNTIME)
[EMAIL PROTECTED]( { ElementType.TYPE })
[EMAIL PROTECTED]
+public @interface StrictBinding {
+ /**
+ * The options for the [EMAIL PROTECTED] StrictBinding#defaultPolicy()}
element.
+ */
+ public enum Policy {
+ /** In the event of a conflict, binding is allowed */
+ ALLOW,
+
+ /** In the event of a conflict, binding is denied */
+ DENY
+ }
+
+ /**
+ * The policy to observe when a property name matches both the deny and
allow lists, or when a
+ * property name does not match either list.
+ */
+ Policy defaultPolicy() default Policy.DENY;
+
+ /** The list of properties that may be bound. */
+ String[] allow() default "";
+
+ /** The list of properties that may <em>not</em> be bound. */
+ String[] deny() default "";
+}
Added:
trunk/stripes/src/net/sourceforge/stripes/controller/BindingPolicyManager.java
===================================================================
---
trunk/stripes/src/net/sourceforge/stripes/controller/BindingPolicyManager.java
(rev 0)
+++
trunk/stripes/src/net/sourceforge/stripes/controller/BindingPolicyManager.java
2007-07-19 04:29:38 UTC (rev 587)
@@ -0,0 +1,252 @@
+/* Copyright 2007 Ben Gunter
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package net.sourceforge.stripes.controller;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import net.sourceforge.stripes.action.StrictBinding;
+import net.sourceforge.stripes.action.StrictBinding.Policy;
+import net.sourceforge.stripes.util.Log;
+import net.sourceforge.stripes.util.bean.Node;
+import net.sourceforge.stripes.util.bean.PropertyExpression;
+import net.sourceforge.stripes.util.bean.PropertyExpressionEvaluation;
+
+/**
+ * The policies observed by [EMAIL PROTECTED] DefaultActionBeanPropertyBinder}
when binding properties to an
+ * [EMAIL PROTECTED] ActionBean}.
+ *
+ * @author Ben Gunter
+ */
[EMAIL PROTECTED](defaultPolicy = Policy.ALLOW)
+public class BindingPolicyManager {
+ /** The regular expression that a property name must match */
+ private static final String PROPERTY_REGEX =
"\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*";
+
+ /** The compiled form of [EMAIL PROTECTED] #PROPERTY_REGEX} */
+ private static final Pattern PROPERTY_PATTERN =
Pattern.compile(PROPERTY_REGEX);
+
+ /** Log */
+ private static final Log log = Log.getInstance(BindingPolicyManager.class);
+
+ /** Singleton instance */
+ private static final BindingPolicyManager instance = new
BindingPolicyManager();
+
+ /** Get the singleton instance of the class */
+ public static BindingPolicyManager getInstance() {
+ return instance;
+ }
+
+ /** Maps classes to a default policy */
+ private Map<Class<?>, Policy> policy = new HashMap<Class<?>, Policy>();
+
+ /** Maps classes to a regex that matches the expressions that are allowed
to bind */
+ private Map<Class<?>, Pattern> allow = new HashMap<Class<?>, Pattern>();
+
+ /** Maps classes to a regex that matches the expressions that are not
allowed to bind */
+ private Map<Class<?>, Pattern> deny = new HashMap<Class<?>, Pattern>();
+
+ /** Does nothing */
+ protected BindingPolicyManager() {
+ }
+
+ /**
+ * Indicates if binding is allowed for the given expression.
+ *
+ * @param eval a property expression that has been evaluated against an
[EMAIL PROTECTED] ActionBean}
+ * @return true if binding is allowed; false if not
+ */
+ public boolean isBindingAllowed(PropertyExpressionEvaluation eval) {
+ PropertyExpression expression = eval.getExpression();
+ Node node = expression.getRootNode();
+ StringBuilder buf = new StringBuilder();
+ do {
+ buf.append(node.getStringValue()).append('.');
+ } while ((node = node.getNext()) != null);
+ if (buf.length() > 0)
+ buf.setLength(buf.length() - 1);
+
+ Class<?> beanType = eval.getBean().getClass();
+ Pattern denyPattern = getDeniedPattern(beanType);
+ Pattern allowPattern = getAllowedPattern(beanType);
+ boolean deny = denyPattern != null &&
denyPattern.matcher(buf.toString()).matches();
+ boolean allow = allowPattern != null &&
allowPattern.matcher(buf.toString()).matches();
+
+ /*
+ * if path appears on neither or both lists ( i.e. !(allow ^ deny) )
and default policy is
+ * to deny access, then fail
+ */
+ if (getDefaultPolicy(beanType) == Policy.DENY && !(allow ^ deny))
+ return false;
+
+ /*
+ * regardless of default policy, if it's in the deny list but not in
the allow list, then
+ * fail
+ */
+ if (!allow && deny)
+ return false;
+
+ // any other conditions pass the test
+ return true;
+ }
+
+ /**
+ * Get the [EMAIL PROTECTED] StrictBinding} annotation for a class,
checking all its superclasses if
+ * necessary.
+ *
+ * @param beanType the class to get the [EMAIL PROTECTED] StrictBinding}
annotation for
+ * @return
+ */
+ protected StrictBinding getAnnotation(Class<?> beanType) {
+ StrictBinding annotation;
+ do {
+ annotation = beanType.getAnnotation(StrictBinding.class);
+ } while (annotation == null && (beanType = beanType.getSuperclass())
!= null);
+ if (annotation == null) {
+ annotation = getClass().getAnnotation(StrictBinding.class);
+ }
+ return annotation;
+ }
+
+ /**
+ * Get the default policy for the given class.
+ *
+ * @param beanType the class whose policy is to be looked up
+ * @return the policy
+ */
+ protected Policy getDefaultPolicy(Class<?> beanType) {
+ if (policy.containsKey(beanType))
+ return policy.get(beanType);
+
+ Policy defaultPolicy = getAnnotation(beanType).defaultPolicy();
+ policy.put(beanType, defaultPolicy);
+ return defaultPolicy;
+ }
+
+ /**
+ * Get the [EMAIL PROTECTED] Pattern} against which property names that
are allowed to bind will be
+ * matched.
+ *
+ * @param beanType [EMAIL PROTECTED] ActionBean} type
+ * @return A pattern against which property names will be matched. This
method must not return
+ * null.
+ */
+ protected Pattern getAllowedPattern(Class<?> beanType) {
+ if (allow.containsKey(beanType))
+ return allow.get(beanType);
+
+ StrictBinding annotation = getAnnotation(beanType);
+ Pattern pattern = globToPattern(annotation.allow());
+ allow.put(beanType, pattern);
+ return pattern;
+ }
+
+ /**
+ * Get the [EMAIL PROTECTED] Pattern} against which property names that
are not allowed to bind will be
+ * matched.
+ *
+ * @param beanType [EMAIL PROTECTED] ActionBean} type
+ * @return A pattern against which property names will be matched. This
method must not return
+ * null.
+ */
+ protected Pattern getDeniedPattern(Class<?> beanType) {
+ if (deny.containsKey(beanType))
+ return deny.get(beanType);
+
+ StrictBinding annotation = getAnnotation(beanType);
+ Pattern pattern = globToPattern(annotation.deny());
+ deny.put(beanType, pattern);
+ return pattern;
+ }
+
+ /**
+ * Converts a glob to a regex [EMAIL PROTECTED] Pattern}.
+ *
+ * @param globArray an array of property name globs, each of which may be
a comma separated list
+ * of globs
+ * @return
+ */
+ protected Pattern globToPattern(String... globArray) {
+ if (globArray == null || globArray.length == 0)
+ return null;
+
+ // things are much easier if we convert to a single list
+ List<String> globs = new ArrayList<String>();
+ for (String glob : globArray) {
+ String[] subs = glob.split("(\\s*,\\s*)+");
+ for (String sub : subs) {
+ globs.add(sub);
+ }
+ }
+
+ List<String> subs = new ArrayList<String>();
+ StringBuilder buf = new StringBuilder();
+ for (String glob : globs) {
+ buf.setLength(0);
+ String[] properties = glob.split("\\.");
+ for (int i = 0; i < properties.length; i++) {
+ String property = properties[i];
+ if ("*".equals(property)) {
+ if (i > 0)
+ buf.append("\\.");
+ buf.append(PROPERTY_REGEX);
+ }
+ else if ("**".equals(property)) {
+ if (i > 0)
+ buf.append("\\.");
+
buf.append(PROPERTY_REGEX).append("(\\.").append(PROPERTY_REGEX).append(")*");
+ }
+ else if (property.length() > 0) {
+ Matcher matcher = PROPERTY_PATTERN.matcher(property);
+ if (matcher.matches()) {
+ buf.append(property);
+ }
+ else {
+ log.warn("Invalid property name: " + property);
+ return null;
+ }
+ }
+
+ // add a literal dot after all but the last
+ if (i < properties.length - 1)
+ buf.append("\\.");
+ }
+
+ // add to the list of subs
+ if (buf.length() != 0)
+ subs.add(buf.toString());
+ }
+
+ // join subs together with pipes and compile
+ buf.setLength(0);
+ for (String sub : subs) {
+ buf.append(sub).append('|');
+ }
+ if (buf.length() > 0)
+ buf.setLength(buf.length() - 1);
+ log.debug("Translated globs ", Arrays.toString(globArray), " to regex
", buf);
+
+ // return null if pattern is empty
+ if (buf.length() == 0)
+ return null;
+ else
+ return Pattern.compile(buf.toString());
+ }
+}
Modified:
trunk/stripes/src/net/sourceforge/stripes/controller/DefaultActionBeanPropertyBinder.java
===================================================================
---
trunk/stripes/src/net/sourceforge/stripes/controller/DefaultActionBeanPropertyBinder.java
2007-07-07 00:30:59 UTC (rev 586)
+++
trunk/stripes/src/net/sourceforge/stripes/controller/DefaultActionBeanPropertyBinder.java
2007-07-19 04:29:38 UTC (rev 587)
@@ -306,8 +306,12 @@
protected boolean isBindingAllowed(PropertyExpressionEvaluation eval) {
// Ensure no-one is trying to bind into the ActionBeanContext!!
Type firstNodeType = eval.getRootNode().getValueType();
- return !(firstNodeType instanceof Class &&
- ActionBeanContext.class.isAssignableFrom((Class)
firstNodeType));
+ if (firstNodeType instanceof Class &&
+ ActionBeanContext.class.isAssignableFrom((Class)
firstNodeType)) {
+ return false;
+ } else {
+ return BindingPolicyManager.getInstance().isBindingAllowed(eval);
+ }
}
/**
This was sent by the SourceForge.net collaborative development platform, the
world's largest Open Source development site.
-------------------------------------------------------------------------
This SF.net email is sponsored by DB2 Express
Download DB2 Express C - the FREE version of DB2 express and take
control of your XML. No limits. Just data. Click to get it now.
http://sourceforge.net/powerbar/db2/
_______________________________________________
Stripes-development mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/stripes-development