/*
* Copyright 2005 Joe Walker
*
* 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 org.directwebremoting.impl;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import org.directwebremoting.WebContextFactory;
import org.directwebremoting.extend.AccessControl;
import org.directwebremoting.extend.AccessDeniedException;
import org.directwebremoting.extend.Creator;
/**
* Control who should be accessing which methods on which classes.
* @author Joe Walker [joe at getahead dot ltd dot uk]
*/
public class DefaultAccessControl implements AccessControl
{
/* (non-Javadoc)
* @see org.directwebremoting.extend.AccessControl#assertExecutionIsPossible(org.directwebremoting.extend.Creator, java.lang.String, java.lang.reflect.Method)
*/
public void assertExecutionIsPossible(Creator creator, String className, Method method) throws SecurityException
{
assertIsRestrictedByRole(className, method);
assertIsDisplayable(creator, className, method);
}
/* (non-Javadoc)
* @see org.directwebremoting.AccessControl#getReasonToNotDisplay(org.directwebremoting.Creator, java.lang.String, java.lang.reflect.Method)
*/
public void assertIsDisplayable(Creator creator, String className, Method method) throws SecurityException
{
assertIsMethodPublic(method);
assertIsExecutable(className, method.getName());
assertIsNotOnBaseObject(method);
if (!exposeInternals)
{
assertIsClassDwrInternal(creator);
assertAreParametersDwrInternal(method);
}
}
/* (non-Javadoc)
* @see org.directwebremoting.AccessControl#addRoleRestriction(java.lang.String, java.lang.String, java.lang.String)
*/
public void addRoleRestriction(String scriptName, String methodName, String role)
{
String key = scriptName + '.' + methodName;
Set<String> roles = roleRestrictMap.get(key);
if (roles == null)
{
roles = new HashSet<String>();
roleRestrictMap.put(key, roles);
}
roles.add(role);
}
/* (non-Javadoc)
* @see org.directwebremoting.AccessControl#addIncludeRule(java.lang.String, java.lang.String)
*/
public void addIncludeRule(String scriptName, String methodName)
{
Policy policy = getPolicy(scriptName);
// If the policy for the given type is defaultAllow then we need to go
// to default disallow mode, and check that the are not rules applied
if (policy.defaultAllow)
{
if (!policy.rules.isEmpty())
{
throw new IllegalArgumentException("The Creator '" + scriptName + "' uses mixed include and exclude statements");
}
policy.defaultAllow = false;
}
// Add the rule to this policy
policy.rules.add(methodName);
}
/* (non-Javadoc)
* @see org.directwebremoting.AccessControl#addExcludeRule(java.lang.String, java.lang.String)
*/
public void addExcludeRule(String scriptName, String methodName)
{
Policy policy = getPolicy(scriptName);
// If the policy for the given type is defaultAllow then we need to go
// to default disallow mode, and check that the are not rules applied
if (!policy.defaultAllow)
{
if (!policy.rules.isEmpty())
{
throw new IllegalArgumentException("The Creator '" + scriptName + "' uses mixed include and exclude statements");
}
policy.defaultAllow = true;
}
// Add the rule to this policy
policy.rules.add(methodName);
}
/**
* @param scriptName The name of the creator to Javascript
* @param method The method to execute
*/
protected void assertIsRestrictedByRole(String scriptName, Method method)
{
String methodName = method.getName();
// What if there is some J2EE role based restriction?
Set<String> roles = getRoleRestrictions(scriptName, methodName);
if (roles != null && !roles.isEmpty())
{
HttpServletRequest req = WebContextFactory.get().getHttpServletRequest();
assertAuthenticationIsValid(req);
assertAllowedByRoles(req, roles);
}
}
/**
* @param scriptName The name of the creator to Javascript
* @param methodName The name of the method (without brackets)
* @return A Set of all the roles for the given script and method
*/
protected Set<String> getRoleRestrictions(String scriptName, String methodName)
{
String key = scriptName + '.' + methodName;
return roleRestrictMap.get(key);
}
/**
* Check the users session for validity
* @param req The users request
* @throws SecurityException if the users session is invalid
*/
protected static void assertAuthenticationIsValid(HttpServletRequest req) throws SecurityException
{
// ensure that at least the next call has a valid session
req.getSession();
// if there was an expired session, the request has to fail
if (!req.isRequestedSessionIdValid())
{
throw new LoginRequiredException("Session timed out, or invalid");
}
if (req.getRemoteUser() == null)
{
throw new LoginRequiredException("No valid authentication details");
}
}
/**
* Is this current user in the given list of roles
* @param req The users request
* @param roles The list of roles to check
* @throws SecurityException if this user is not allowed by the list of roles
*/
protected static void assertAllowedByRoles(HttpServletRequest req, Set<String> roles) throws SecurityException
{
for (String role : roles)
{
if ("*".equals(role) || req.isUserInRole(role))
{
return;
}
}
throw new AccessDeniedException("User is not in role for this method.");
}
/**
* Is the method public?
* @param method The method that we wish to execute
*/
protected static void assertIsMethodPublic(Method method)
{
if (!Modifier.isPublic(method.getModifiers()))
{
throw new SecurityException("The method is not declared public");
}
}
/**
* We ban some methods from {@link java.lang.Object}
* @param method The method that should not be owned by {@link java.lang.Object}
*/
protected static void assertIsNotOnBaseObject(Method method)
{
if (method.getDeclaringClass() == Object.class)
{
throw new SecurityException("Methods defined in java.lang.Object are not accessible");
}
}
/**
* Test to see if a method is excluded or included.
* @param scriptName The name of the creator to Javascript
* @param methodName The name of the method (without brackets)
* @throws SecurityException if the method is allowed by the rules in addIncludeRule()
* @see AccessControl#addIncludeRule(String, String)
*/
protected void assertIsExecutable(String scriptName, String methodName) throws SecurityException
{
Policy policy = policyMap.get(scriptName);
if (policy == null)
{
return;
}
// Find a match for this method in the policy rules
String match = null;
for (Iterator<String> it = policy.rules.iterator(); it.hasNext() && match == null;)
{
String test = it.next();
// If at some point we wish to do regex matching on rules, here is
// the place to do it.
if (methodName.equals(test))
{
match = test;
}
}
if (policy.defaultAllow && match != null)
{
// We are in default allow mode so the rules are exclusions and we
// have a match, so this method is excluded.
//log.debug("method excluded for creator " + type + " due to defaultAllow=" + policy.defaultAllow + " and rule: " + match);
throw new SecurityException("Method access is denied by rules in dwr.xml");
}
// There may be a more optimized if statement here, but I value code
// clarity over performance.
//noinspection RedundantIfStatement
if (!policy.defaultAllow && match == null)
{
// We are in default deny mode so the rules are inclusions and we
// do not have a match, so this method is excluded.
//log.debug("method excluded for creator " + type + " due to defaultAllow=" + policy.defaultAllow + " and rule: " + match);
throw new SecurityException("Method access is denied by rules in dwr.xml");
}
}
/**
* Check the parameters are not DWR internal either
* @param method The method that we want to execute
*/
protected static void assertAreParametersDwrInternal(Method method)
{
for (int j = 0; j < method.getParameterTypes().length; j++)
{
Class<?> paramType = method.getParameterTypes()[j];
// Access to org.directwebremoting is denied except for .io
if (paramType.getName().startsWith(PACKAGE_DWR_DENY) && !paramType.getName().startsWith(PACKAGE_ALLOW_CONVERT))
{
throw new SecurityException("Methods containing parameters defined by DWR can not be remoted");
}
}
}
/**
* Is the class that we are executing a method on part of DWR?
* @param creator The {@link Creator} that exposes the class
*/
protected static void assertIsClassDwrInternal(Creator creator)
{
String name = creator.getType().getName();
// Access to org.directwebremoting is denied except for .export
if (name.startsWith(PACKAGE_DWR_DENY) && !name.startsWith(PACKAGE_ALLOW_CREATE))
{
throw new SecurityException("Methods defined by DWR can not be remoted");
}
}
/**
* Find the policy for the given type and create one if none exists.
* @param type The name of the creator
* @return The policy for the given Creator
*/
protected Policy getPolicy(String type)
{
Policy policy = policyMap.get(type);
if (policy == null)
{
policy = new Policy();
policyMap.put(type, policy);
}
return policy;
}
/**
* @param exposeInternals the exposeInternals to set
*/
public void setExposeInternals(boolean exposeInternals)
{
this.exposeInternals = exposeInternals;
}
/**
* Do we allow DWR classes to be remoted?
* @see #PACKAGE_DWR_DENY
*/
protected boolean exposeInternals = false;
/**
* A map of Creators to policies
*/
protected Map<String, Policy> policyMap = new HashMap<String, Policy>();
/**
* What role based restrictions are there?
*/
protected Map<String, Set<String>> roleRestrictMap = new HashMap<String, Set<String>>();
/**
* A struct that contains a method access policy for a Creator
*/
static class Policy
{
boolean defaultAllow = true;
List<String> rules = new ArrayList<String>();
}
/**
* My package name, so we can ban DWR classes from being created or marshalled
*/
protected static final String PACKAGE_DWR_DENY = "org.directwebremoting.";
/**
* Special dwr package name from which classes may be created
*/
protected static final String PACKAGE_ALLOW_CREATE = "org.directwebremoting.export.";
/**
* Special dwr package name from which classes may be converted
*/
protected static final String PACKAGE_ALLOW_CONVERT = "org.directwebremoting.io.";
}