NIHVIVO-2492 First pass at a mechanism to restrict pages by policy

This commit is contained in:
j2blake 2011-04-15 15:25:13 +00:00
parent 32e4f81be7
commit 83ac0750b5
10 changed files with 530 additions and 23 deletions

View file

@ -0,0 +1,211 @@
/* $This file is distributed under the terms of the license in /doc/license.txt$ */
package edu.cornell.mannlib.vitro.webapp.auth.policy;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import edu.cornell.mannlib.vitro.webapp.auth.identifier.IdentifierBundle;
import edu.cornell.mannlib.vitro.webapp.auth.identifier.RequestIdentifiers;
import edu.cornell.mannlib.vitro.webapp.auth.policy.PolicyHelper.RequiresAuthorizationFor.NoAction;
import edu.cornell.mannlib.vitro.webapp.auth.policy.ifaces.Authorization;
import edu.cornell.mannlib.vitro.webapp.auth.policy.ifaces.PolicyDecision;
import edu.cornell.mannlib.vitro.webapp.auth.policy.ifaces.PolicyIface;
import edu.cornell.mannlib.vitro.webapp.auth.requestedAction.ifaces.RequestedAction;
import edu.cornell.mannlib.vitro.webapp.controller.VitroHttpServlet;
/**
* A collection of static methods to help determine whether requested actions
* are authorized by current policy.
*/
public class PolicyHelper {
private static final Log log = LogFactory.getLog(PolicyHelper.class);
/**
* A subclass of VitroHttpServlet may be annotated to say what actions
* should be checked for authorization before permitting the user to view
* the page that the servlet would create.
*
* Any RequestedAction can be specified, but the most common use will be to
* specify implementations of UsePagesRequestedAction.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public static @interface RequiresAuthorizationFor {
static class NoAction extends RequestedAction {
/* no fields */
}
Class<? extends RequestedAction>[] value() default NoAction.class;
}
/**
* Does this servlet require authorization?
*/
public static boolean isRestrictedPage(VitroHttpServlet servlet) {
Class<? extends VitroHttpServlet> servletClass = servlet.getClass();
return !getRequiredAuthorizationsForServlet(servletClass).isEmpty();
}
/**
* What RequestedActions does this servlet require authorization for?
*/
public static Set<RequestedAction> getRequiredAuthorizationsForServlet(
Class<? extends VitroHttpServlet> clazz) {
Set<RequestedAction> result = new HashSet<RequestedAction>();
RequiresAuthorizationFor annotation = clazz
.getAnnotation(RequiresAuthorizationFor.class);
if (annotation != null) {
for (Class<? extends RequestedAction> actionClass : annotation
.value()) {
if (NoAction.class != actionClass) {
RequestedAction action = instantiateAction(actionClass);
if (action != null) {
result.add(action);
}
}
}
}
return result;
}
/**
* Are the actions that this servlet requires authorized for the current
* user by the current policies?
*/
public static boolean areRequiredAuthorizationsSatisfied(
HttpServletRequest req, VitroHttpServlet servlet) {
Class<? extends VitroHttpServlet> servletClass = servlet.getClass();
return areRequiredAuthorizationsSatisfied(req,
getRequiredAuthorizationsForServlet(servletClass));
}
/**
* Are these action classes authorized for the current user by the current
* policies?
*/
public static boolean areRequiredAuthorizationsSatisfied(
HttpServletRequest req,
Class<? extends RequestedAction>... actionClasses) {
List<Class<? extends RequestedAction>> classList = Arrays
.asList(actionClasses);
Set<RequestedAction> actions = instantiateActions(classList);
if (actions == null) {
log.debug("not authorized: failed to instantiate actions");
return false;
}
return areRequiredAuthorizationsSatisfied(req, actions);
}
/**
* Are these actions authorized for the current user by the current
* policies?
*/
public static boolean areRequiredAuthorizationsSatisfied(
HttpServletRequest req,
Collection<? extends RequestedAction> actions) {
PolicyIface policy = ServletPolicyList.getPolicies(req);
IdentifierBundle ids = RequestIdentifiers.getIdBundleForRequest(req);
for (RequestedAction action : actions) {
if (isAuthorized(policy, ids, action)) {
log.debug("not authorized");
return false;
}
}
log.debug("authorized");
return true;
}
/**
* Is this action class authorized for the current user by the current
* policies?
*/
@SuppressWarnings("unchecked")
public static boolean isAuthorized(HttpServletRequest req,
Class<? extends RequestedAction> actionClass) {
return areRequiredAuthorizationsSatisfied(req, actionClass);
}
/**
* Is this action authorized for these IDs by this policy?
*/
private static boolean isAuthorized(PolicyIface policy,
IdentifierBundle ids, RequestedAction action) {
PolicyDecision decision = policy.isAuthorized(ids, action);
log.debug("decision for '" + action.getClass().getName() + "' was: "
+ decision);
return (decision == null)
|| (decision.getAuthorized() != Authorization.AUTHORIZED);
}
/**
* Instantiate actions from their classes. If any one of the classes cannot
* be instantiated, return null.
*/
private static Set<RequestedAction> instantiateActions(
Collection<Class<? extends RequestedAction>> actionClasses) {
Set<RequestedAction> actions = new HashSet<RequestedAction>();
for (Class<? extends RequestedAction> actionClass : actionClasses) {
RequestedAction action = instantiateAction(actionClass);
if (action == null) {
return null;
} else {
actions.add(action);
}
}
return actions;
}
/**
* Get an instance of the RequestedAction, from its class. If the class
* cannot be instantiated, return null.
*/
private static RequestedAction instantiateAction(
Class<? extends RequestedAction> actionClass) {
try {
Constructor<? extends RequestedAction> constructor = actionClass
.getConstructor();
RequestedAction instance = constructor.newInstance();
return instance;
} catch (NoSuchMethodException e) {
log.error("'" + actionClass.getName()
+ "' does not have a no-argument constructor.");
return null;
} catch (IllegalAccessException e) {
log.error("The no-argument constructor for '"
+ actionClass.getName() + "' is not public.");
return null;
} catch (Exception e) {
log.error("Failed to instantiate '" + actionClass.getName() + "'",
e);
return null;
}
}
/**
* No need to instantiate this helper class - all methods are static.
*/
private PolicyHelper() {
// nothing to do.
}
}

View file

@ -0,0 +1,92 @@
/* $This file is distributed under the terms of the license in /doc/license.txt$ */
package edu.cornell.mannlib.vitro.webapp.auth.policy;
import javax.servlet.ServletContext;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import edu.cornell.mannlib.vitro.webapp.auth.identifier.HasRoleLevel;
import edu.cornell.mannlib.vitro.webapp.auth.identifier.Identifier;
import edu.cornell.mannlib.vitro.webapp.auth.identifier.IdentifierBundle;
import edu.cornell.mannlib.vitro.webapp.auth.policy.ifaces.Authorization;
import edu.cornell.mannlib.vitro.webapp.auth.policy.ifaces.PolicyDecision;
import edu.cornell.mannlib.vitro.webapp.auth.policy.ifaces.PolicyIface;
import edu.cornell.mannlib.vitro.webapp.auth.requestedAction.ifaces.RequestedAction;
import edu.cornell.mannlib.vitro.webapp.auth.requestedAction.usepages.UseAdvancedDataToolsPages;
import edu.cornell.mannlib.vitro.webapp.beans.BaseResourceBean.RoleLevel;
/**
* Check the users role level to determine whether they are allowed to use
* restricted pages.
*/
public class UseRestrictedPagesByRoleLevelPolicy implements PolicyIface {
private static final Log log = LogFactory
.getLog(UseRestrictedPagesByRoleLevelPolicy.class);
@Override
public PolicyDecision isAuthorized(IdentifierBundle whoToAuth,
RequestedAction whatToAuth) {
if (whoToAuth == null) {
return defaultDecision("whomToAuth was null");
}
if (whatToAuth == null) {
return defaultDecision("whatToAuth was null");
}
RoleLevel userRole = getUsersRoleLevel(whoToAuth);
PolicyDecision result;
if (whatToAuth instanceof UseAdvancedDataToolsPages) {
result = isAuthorized(whatToAuth, RoleLevel.DB_ADMIN, userRole);
} else {
result = defaultDecision("Unrecognized action");
}
log.debug("decision for '" + whatToAuth + "' is " + result);
return result;
}
private PolicyDecision isAuthorized(RequestedAction whatToAuth,
RoleLevel requiredRole, RoleLevel currentRole) {
if (isRoleAtLeast(requiredRole, currentRole)) {
return authorized("User may view page: " + whatToAuth
+ ", requiredRole=" + requiredRole + ", currentRole="
+ currentRole);
} else {
return defaultDecision("User may not view page: " + whatToAuth
+ ", requiredRole=" + requiredRole + ", currentRole="
+ currentRole);
}
}
private boolean isRoleAtLeast(RoleLevel required, RoleLevel current) {
return (current.compareTo(required) >= 0);
}
/** If the user is explicitly authorized, return this. */
private PolicyDecision authorized(String message) {
String className = this.getClass().getSimpleName();
return new BasicPolicyDecision(Authorization.AUTHORIZED, className
+ ": " + message);
}
/** If the user isn't explicitly authorized, return this. */
private PolicyDecision defaultDecision(String message) {
return new BasicPolicyDecision(Authorization.INCONCLUSIVE, message);
}
/**
* The user is nobody unless they have a HasRoleLevel identifier.
*/
private RoleLevel getUsersRoleLevel(IdentifierBundle whoToAuth) {
RoleLevel userRole = RoleLevel.PUBLIC;
for (Identifier id : whoToAuth) {
if (id instanceof HasRoleLevel) {
userRole = ((HasRoleLevel) id).getRoleLevel();
}
}
return userRole;
}
}

View file

@ -14,6 +14,7 @@ import edu.cornell.mannlib.vitro.webapp.auth.identifier.CommonIdentifierBundleFa
import edu.cornell.mannlib.vitro.webapp.auth.policy.DisplayRestrictedDataByRoleLevelPolicy;
import edu.cornell.mannlib.vitro.webapp.auth.policy.DisplayRestrictedDataToSelfPolicy;
import edu.cornell.mannlib.vitro.webapp.auth.policy.ServletPolicyList;
import edu.cornell.mannlib.vitro.webapp.auth.policy.UseRestrictedPagesByRoleLevelPolicy;
import edu.cornell.mannlib.vitro.webapp.servlet.setup.AbortStartup;
/**
@ -36,6 +37,8 @@ public class CommonPolicyFamilySetup implements ServletContextListener {
new DisplayRestrictedDataByRoleLevelPolicy(ctx));
ServletPolicyList.addPolicy(ctx,
new DisplayRestrictedDataToSelfPolicy(ctx));
ServletPolicyList.addPolicy(ctx,
new UseRestrictedPagesByRoleLevelPolicy());
// This factory creates Identifiers for all of the above policies.
CommonIdentifierBundleFactory factory = new CommonIdentifierBundleFactory();

View file

@ -12,4 +12,8 @@ public abstract class RequestedAction {
return RequestActionConstants.actionNamespace + this.getClass().getName();
}
@Override
public String toString() {
return this.getClass().getSimpleName();
}
}

View file

@ -0,0 +1,11 @@
/* $This file is distributed under the terms of the license in /doc/license.txt$ */
package edu.cornell.mannlib.vitro.webapp.auth.requestedAction.usepages;
import edu.cornell.mannlib.vitro.webapp.auth.requestedAction.ifaces.RequestedAction;
/** Should we allow the user to use the pages for Advanced Data Tools? */
public class UseAdvancedDataToolsPages extends RequestedAction implements
UsePagesRequestedAction {
// no fields
}

View file

@ -0,0 +1,8 @@
/* $This file is distributed under the terms of the license in /doc/license.txt$ */
package edu.cornell.mannlib.vitro.webapp.auth.requestedAction.usepages;
/** Denotes a request to use a particular page or group of pages. */
public interface UsePagesRequestedAction {
/** marker interface */
}

View file

@ -23,6 +23,7 @@ import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import edu.cornell.mannlib.vedit.beans.LoginStatusBean;
import edu.cornell.mannlib.vitro.webapp.auth.policy.PolicyHelper;
import edu.cornell.mannlib.vitro.webapp.beans.DisplayMessage;
import edu.cornell.mannlib.vitro.webapp.controller.authenticate.LogoutRedirector;
@ -44,6 +45,38 @@ public class VitroHttpServlet extends HttpServlet {
public final static String TTL_MIMETYPE = "text/turtle"; // unofficial and
// unregistered
/**
* Check that any required authorizations are satisfied before processing
* the request.
*/
@Override
public final void service(ServletRequest req, ServletResponse resp)
throws ServletException, IOException {
if ((req instanceof HttpServletRequest)
&& (resp instanceof HttpServletResponse)) {
HttpServletRequest hreq = (HttpServletRequest) req;
HttpServletResponse hresp = (HttpServletResponse) resp;
if (log.isTraceEnabled()) {
dumpRequestHeaders(hreq);
}
if (PolicyHelper.isRestrictedPage(this)) {
LogoutRedirector.recordRestrictedPageUri(hreq);
}
if (!PolicyHelper.areRequiredAuthorizationsSatisfied(hreq, this)) {
if (LoginStatusBean.getBean(hreq).isLoggedIn()) {
redirectToInsufficientAuthorizationPage(hreq, hresp);
} else {
redirectToLoginPage(hreq, hresp);
}
}
}
super.service(req, resp);
}
/**
* Show this to the user if they are logged in, but still not authorized to
* view the page.
@ -87,6 +120,8 @@ public class VitroHttpServlet extends HttpServlet {
/**
* If not logged in, redirect them to the login page.
*
* TODO this goes away as it is replace by annotations.
*/
public static boolean checkLoginStatus(HttpServletRequest request,
HttpServletResponse response) {
@ -104,6 +139,8 @@ public class VitroHttpServlet extends HttpServlet {
/**
* If not logged in at the required level, redirect them to the appropriate
* page.
*
* TODO this goes away as it is replace by annotations.
*/
public static boolean checkLoginStatus(HttpServletRequest request,
HttpServletResponse response, int minimumLevel) {
@ -183,24 +220,17 @@ public class VitroHttpServlet extends HttpServlet {
* If logging is set to the TRACE level, dump the HTTP headers on the
* request.
*/
@SuppressWarnings("unchecked")
@Override
public void service(ServletRequest req, ServletResponse resp)
throws ServletException, IOException {
if (log.isTraceEnabled()) {
HttpServletRequest request = (HttpServletRequest) req;
Enumeration<String> names = request.getHeaderNames();
log.trace("----------------------request:"
+ request.getRequestURL());
while (names.hasMoreElements()) {
String name = names.nextElement();
if (!BORING_HEADERS.contains(name)) {
log.trace(name + "=" + request.getHeader(name));
}
private void dumpRequestHeaders(HttpServletRequest req) {
@SuppressWarnings("unchecked")
Enumeration<String> names = req.getHeaderNames();
log.trace("----------------------request:" + req.getRequestURL());
while (names.hasMoreElements()) {
String name = names.nextElement();
if (!BORING_HEADERS.contains(name)) {
log.trace(name + "=" + req.getHeader(name));
}
}
super.service(req, resp);
}
/** Don't dump the contents of these headers, even if log.trace is enabled. */

View file

@ -16,13 +16,7 @@ import edu.cornell.mannlib.vitro.webapp.controller.authenticate.LogoutRedirector
import edu.cornell.mannlib.vitro.webapp.filters.VitroRequestPrep;
/**
* JSP tag to generate the HTML of links for edit, delete or add of a Property.
*
* Maybe we should have a mode where it just sets a var to a map with "href" =
* "edit/editDatapropDispatch.jsp?subjectUri=..." and "type" = "delete"
*
* @author bdc34
*
* TODO This should go away as it is replaced by vitro:requiresAuthorizationFor
*/
public class ConfirmLoginStatus extends BodyTagSupport {
private static final Log log = LogFactory.getLog(ConfirmLoginStatus.class);

View file

@ -0,0 +1,125 @@
/* $This file is distributed under the terms of the license in /doc/license.txt$ */
package edu.cornell.mannlib.vitro.webapp.web.jsptags;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.jsp.JspException;
import javax.servlet.jsp.tagext.BodyTagSupport;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import edu.cornell.mannlib.vedit.beans.LoginStatusBean;
import edu.cornell.mannlib.vitro.webapp.auth.policy.PolicyHelper;
import edu.cornell.mannlib.vitro.webapp.auth.requestedAction.ifaces.RequestedAction;
import edu.cornell.mannlib.vitro.webapp.auth.requestedAction.usepages.UseAdvancedDataToolsPages;
import edu.cornell.mannlib.vitro.webapp.controller.VitroHttpServlet;
/**
* Confirm that the user is authorized to perform each of the RequestedActions.
*
* The user specifies the actions as a comma delimited list of names (with
* optional spaces). These names are matched against the map of recognized
* names. If no match is found, an error is logged and the authorization fails.
*/
public class RequiresAuthorizationFor extends BodyTagSupport {
private static final Log log = LogFactory
.getLog(RequiresAuthorizationFor.class);
/**
* These are the only action names that we recognize.
*/
private static final Map<String, RequestedAction> actionMap = new HashMap<String, RequestedAction>();
static {
actionMap.put("UseAdvancedDataToolsPages",
new UseAdvancedDataToolsPages());
}
String actionNames = "";
public void setActions(String actionNames) {
this.actionNames = actionNames;
}
/**
* This is all of it. If they are authorized, continue. Otherwise, redirect.
*/
@Override
public int doEndTag() throws JspException {
if (isAuthorized()) {
return EVAL_PAGE;
} else if (isLoggedIn()) {
return showInsufficientAuthorizationMessage();
} else {
return redirectToLoginPage();
}
}
/**
* They are authorized if we recognize the actions they ask for, and they
* are authorized for those actions.
*/
private boolean isAuthorized() {
Collection<RequestedAction> actions = parseActionNames();
if (actions == null) {
return false;
}
return PolicyHelper.areRequiredAuthorizationsSatisfied(getRequest(),
actions);
}
/**
* Parse the string and pull the corresponding actions from the map. If we
* can't do that, complain and return null.
*/
private Collection<RequestedAction> parseActionNames() {
Set<RequestedAction> actions = new HashSet<RequestedAction>();
for (String part : actionNames.split("[\\s],[\\s]")) {
String key = part.trim();
if (key.isEmpty()) {
continue;
}
if (actionMap.containsKey(key)) {
log.debug("checking authorization for '" + key + "'");
actions.add(actionMap.get(key));
} else {
log.error("JSP requested authorization for unknown action: '"
+ key + "'");
return null;
}
}
return actions;
}
private boolean isLoggedIn() {
return LoginStatusBean.getBean(getRequest()).isLoggedIn();
}
private int showInsufficientAuthorizationMessage() {
VitroHttpServlet.redirectToInsufficientAuthorizationPage(getRequest(),
getResponse());
return SKIP_PAGE;
}
private int redirectToLoginPage() throws JspException {
VitroHttpServlet.redirectToLoginPage(getRequest(), getResponse());
return SKIP_PAGE;
}
private HttpServletRequest getRequest() {
return ((HttpServletRequest) pageContext.getRequest());
}
private HttpServletResponse getResponse() {
return (HttpServletResponse) pageContext.getResponse();
}
}