All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.sourceforge.stripes.controller.BindingPolicyManager Maven / Gradle / Ivy

There is a newer version: 1.7.0-async-beta
Show newest version
/* 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.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpSession;

import net.sourceforge.stripes.action.ActionBean;
import net.sourceforge.stripes.action.ActionBeanContext;
import net.sourceforge.stripes.action.StrictBinding;
import net.sourceforge.stripes.action.StrictBinding.Policy;
import net.sourceforge.stripes.exception.StripesRuntimeException;
import net.sourceforge.stripes.util.Log;
import net.sourceforge.stripes.util.bean.NodeEvaluation;
import net.sourceforge.stripes.util.bean.PropertyExpressionEvaluation;
import net.sourceforge.stripes.validation.ValidationMetadata;
import net.sourceforge.stripes.validation.ValidationMetadataProvider;

/**
 * Manages the policies observed by {@link DefaultActionBeanPropertyBinder} when binding properties
 * to an {@link ActionBean}.
 * 
 * @author Ben Gunter
 * @see StrictBinding
 */
@StrictBinding(defaultPolicy = Policy.ALLOW)
public class BindingPolicyManager {
    /** List of classes that, for security reasons, are not allowed as a {@link NodeEvaluation} value type. */
    private static final List> ILLEGAL_NODE_VALUE_TYPES = Arrays.> asList(
            ActionBeanContext.class,
            Class.class,
            ClassLoader.class,
            HttpSession.class,
            ServletRequest.class,
            ServletResponse.class);

    /** The regular expression that a property name must match */
    private static final String PROPERTY_REGEX = "\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*";

    /** The compiled form of {@link #PROPERTY_REGEX} */
    private static final Pattern PROPERTY_PATTERN = Pattern.compile(PROPERTY_REGEX);

    /** Log */
    private static final Log log = Log.getInstance(BindingPolicyManager.class);

    /** Cached instances */
    private static final Map, BindingPolicyManager> instances = new HashMap, BindingPolicyManager>();

    /**
     * Get the policy manager for the given class. Instances are cached and returned on subsequent
     * calls.
     * 
     * @param beanType the class whose policy manager is to be retrieved
     * @return a policy manager
     */
    public static BindingPolicyManager getInstance(Class beanType) {
        if (instances.containsKey(beanType))
            return instances.get(beanType);

        BindingPolicyManager instance = new BindingPolicyManager(beanType);
        instances.put(beanType, instance);
        return instance;
    }

    /** The class to which the binding policy applies */
    private Class beanClass;

    /** The default policy to honor, in case of conflicts */
    private Policy defaultPolicy;

    /** The regular expression that allowed properties must match */
    private Pattern allowPattern;

    /** The regular expression that denied properties must match */
    private Pattern denyPattern;

    /** The regular expression that matches properties with {@literal @Validate} */
    private Pattern validatePattern;

    /**
     * Create a new instance to handle binding security for the given type.
     * 
     * @param beanClass the class to which the binding policy applies
     */
    protected BindingPolicyManager(Class beanClass) {
        try {
            log.debug("Creating ", getClass().getName(), " for ", beanClass,
                    " with default policy ", defaultPolicy);
            this.beanClass = beanClass;

            // process the annotation
            StrictBinding annotation = getAnnotation(beanClass);
            if (annotation != null) {
                // set default policy
                this.defaultPolicy = annotation.defaultPolicy();

                // construct the allow pattern
                this.allowPattern = globToPattern(annotation.allow());

                // construct the deny pattern
                this.denyPattern = globToPattern(annotation.deny());

                // construct the validated properties pattern
                this.validatePattern = globToPattern(getValidatedProperties(beanClass));
            }
        }
        catch (Exception e) {
            log.error(e, "%%% Failure instantiating ", getClass().getName());
            StripesRuntimeException sre = new StripesRuntimeException(e.getMessage(), e);
            sre.setStackTrace(e.getStackTrace());
            throw sre;
        }
    }

    /**
     * Indicates if binding is allowed for the given expression.
     * 
     * @param eval a property expression that has been evaluated against an {@link ActionBean}
     * @return true if binding is allowed; false if not
     */
    public boolean isBindingAllowed(PropertyExpressionEvaluation eval) {
        // Ensure no-one is trying to bind into a protected type
        if (usesIllegalNodeValueType(eval)) {
            return false;
        }

        // check parameter name against access lists
        String paramName = new ParameterName(eval.getExpression().getSource()).getStrippedName();
        boolean deny = denyPattern != null && denyPattern.matcher(paramName).matches();
        boolean allow = (allowPattern != null && allowPattern.matcher(paramName).matches())
                || (validatePattern != null && validatePattern.matcher(paramName).matches());

        /*
         * if path appears on neither or both lists ( i.e. !(allow ^ deny) ) and default policy is
         * to deny access, then fail
         */
        if (defaultPolicy == 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;
    }

    /**
     * Indicates if any node in the given {@link PropertyExpressionEvaluation} has a value type that is assignable from
     * any of the classes listed in {@link #ILLEGAL_NODE_VALUE_TYPES}.
     * 
     * @param eval a property expression that has been evaluated against an {@link ActionBean}
     * @return true if the expression uses an illegal node value type; false otherwise
     */
    protected boolean usesIllegalNodeValueType(PropertyExpressionEvaluation eval) {
        for (NodeEvaluation node = eval.getRootNode(); node != null; node = node.getNext()) {
            Type type = node.getValueType();
            if (type instanceof ParameterizedType) {
                type = ((ParameterizedType) type).getRawType();
            }
            if (type instanceof Class) {
                final Class nodeClass = (Class) type;
                for (Class protectedClass : ILLEGAL_NODE_VALUE_TYPES) {
                    if (protectedClass.isAssignableFrom(nodeClass)) {
                        return true;
                    }
                }
            }
        }
        return false;
    }

    /**
     * Get the {@link StrictBinding} annotation for a class, checking all its superclasses if
     * necessary. If no annotation is found, then one will be returned whose default policy is to
     * allow binding to all properties.
     * 
     * @param beanType the class to get the {@link StrictBinding} annotation for
     * @return An annotation. This method never returns null.
     */
    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 all the properties and nested properties of the given class for which there is a
     * corresponding {@link ValidationMetadata}, as returned by
     * {@link ValidationMetadataProvider#getValidationMetadata(Class, ParameterName)}. The idea
     * here is that if the bean property must be validated, then it is expected that the property
     * may be bound to the bean.
     * 
     * @param beanClass a class
     * @return The validated properties. If no properties are annotated then null.
     * @see ValidationMetadataProvider#getValidationMetadata(Class)
     */
    protected String[] getValidatedProperties(Class beanClass) {
        Set properties = StripesFilter.getConfiguration().getValidationMetadataProvider()
                .getValidationMetadata(beanClass).keySet();
        return new ArrayList(properties).toArray(new String[properties.size()]);
    }

    /**
     * Get the bean class.
     * 
     * @return the bean class
     */
    public Class getBeanClass() {
        return beanClass;
    }

    /**
     * Get the default policy.
     * 
     * @return the policy
     */
    public Policy getDefaultPolicy() {
        return defaultPolicy;
    }

    /**
     * Converts a glob to a regex {@link Pattern}.
     * 
     * @param globArray an array of property name globs, each of which may be a comma separated list
     *            of globs
     * @return the pattern
     */
    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 globs = new ArrayList();
        for (String glob : globArray) {
            String[] subs = glob.split("(\\s*,\\s*)+");
            for (String sub : subs) {
                globs.add(sub);
            }
        }

        List subs = new ArrayList();
        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)) {
                    buf.append(PROPERTY_REGEX);
                }
                else if ("**".equals(property)) {
                    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());
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy