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

@ -49,4 +49,33 @@
</attribute> </attribute>
</tag> </tag>
<tag>
<name>requiresAuthorizationFor</name>
<display-name>Confirm that the user is authorized for the actions that this page requires.</display-name>
<description>
Confirm that the user is authorized to perform all of the RequestedActions that
this page requires. A check is done for each such action, to see whether the
current policy will authorize that action for the current user. If any of the
actions is not authorized, the user will be redirected to the appropriate page.
If the user is not authorized because he is not logged in, he will be directed
to the login page, with the current request stored as a post-login destination.
If the user is logged in but without sufficient authorization, he will be
directed to the home page, which will display an "insufficient authorization"
message.
The requested actions are specified as a comma delimited list of names (with
optional spaces). These names must match against the map of classes in
JspPolicyHelper, or an error will be logged and the authorization will fail.
</description>
<tag-class>edu.cornell.mannlib.vitro.webapp.web.jsptags.RequiresAuthorizationFor</tag-class>
<body-content>empty</body-content>
<attribute>
<name>actions</name>
<required>true</required>
<rtexprvalue>true</rtexprvalue>
</attribute>
</tag>
</taglib> </taglib>

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

View file

@ -12,4 +12,8 @@ public abstract class RequestedAction {
return RequestActionConstants.actionNamespace + this.getClass().getName(); 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 org.apache.commons.logging.LogFactory;
import edu.cornell.mannlib.vedit.beans.LoginStatusBean; 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.beans.DisplayMessage;
import edu.cornell.mannlib.vitro.webapp.controller.authenticate.LogoutRedirector; 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 public final static String TTL_MIMETYPE = "text/turtle"; // unofficial and
// unregistered // 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 * Show this to the user if they are logged in, but still not authorized to
* view the page. * view the page.
@ -87,6 +120,8 @@ public class VitroHttpServlet extends HttpServlet {
/** /**
* If not logged in, redirect them to the login page. * 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, public static boolean checkLoginStatus(HttpServletRequest request,
HttpServletResponse response) { HttpServletResponse response) {
@ -104,6 +139,8 @@ public class VitroHttpServlet extends HttpServlet {
/** /**
* If not logged in at the required level, redirect them to the appropriate * If not logged in at the required level, redirect them to the appropriate
* page. * page.
*
* TODO this goes away as it is replace by annotations.
*/ */
public static boolean checkLoginStatus(HttpServletRequest request, public static boolean checkLoginStatus(HttpServletRequest request,
HttpServletResponse response, int minimumLevel) { HttpServletResponse response, int minimumLevel) {
@ -183,26 +220,19 @@ public class VitroHttpServlet extends HttpServlet {
* If logging is set to the TRACE level, dump the HTTP headers on the * If logging is set to the TRACE level, dump the HTTP headers on the
* request. * request.
*/ */
private void dumpRequestHeaders(HttpServletRequest req) {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@Override Enumeration<String> names = req.getHeaderNames();
public void service(ServletRequest req, ServletResponse resp)
throws ServletException, IOException { log.trace("----------------------request:" + req.getRequestURL());
if (log.isTraceEnabled()) {
HttpServletRequest request = (HttpServletRequest) req;
Enumeration<String> names = request.getHeaderNames();
log.trace("----------------------request:"
+ request.getRequestURL());
while (names.hasMoreElements()) { while (names.hasMoreElements()) {
String name = names.nextElement(); String name = names.nextElement();
if (!BORING_HEADERS.contains(name)) { if (!BORING_HEADERS.contains(name)) {
log.trace(name + "=" + request.getHeader(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. */ /** Don't dump the contents of these headers, even if log.trace is enabled. */
private static final List<String> BORING_HEADERS = new ArrayList<String>( private static final List<String> BORING_HEADERS = new ArrayList<String>(
Arrays.asList(new String[] { "host", "user-agent", "accept", Arrays.asList(new String[] { "host", "user-agent", "accept",

View file

@ -16,13 +16,7 @@ import edu.cornell.mannlib.vitro.webapp.controller.authenticate.LogoutRedirector
import edu.cornell.mannlib.vitro.webapp.filters.VitroRequestPrep; import edu.cornell.mannlib.vitro.webapp.filters.VitroRequestPrep;
/** /**
* JSP tag to generate the HTML of links for edit, delete or add of a Property. * TODO This should go away as it is replaced by vitro:requiresAuthorizationFor
*
* 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
*
*/ */
public class ConfirmLoginStatus extends BodyTagSupport { public class ConfirmLoginStatus extends BodyTagSupport {
private static final Log log = LogFactory.getLog(ConfirmLoginStatus.class); 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();
}
}