Chris has posted 1 posts at DZone. View Full User Profile

JSF Application Level Access Control

09.02.2009
| 14054 views |
  • submit to reddit

After trying to work out how to do generically, or at least easily configurable, I wrote a handy little tool for JSF which allows you to perform security control similar to how you can in the web.xml using security-constraint tags but allowing for application level authentication.

The trick is to use a PhaseListener, listening to the RESTORE_VIEW phase and checking the viewId in the afterPhase(). All requests go through this lifecycle, including the result of an action, allowing filtering to be performed on the viewId (URL).  For example a very simple check would be:

public class AccessControlPhaseListener implements PhaseListener
{
public void afterPhase(PhaseEvent event)
{
FacesContext context = event.getFacesContext();
HttpSession session = (HttpSession) context.getExternalContext().getSession(true);
SessionBean sessionBean = (SessionBean) session.getAttribute("sessionBean");
if (!sessionBean.isLoggedIn() && !"/login.xhtml".equals(context.getViewRoot().getViewId()))
context.getApplication().getNavigationHandler().handleNavigation(context, null, "login");
}
public PhaseId getPhaseId()
{
//ALL access go through RESTORE_VIEW and RENDER_VIEW (even direct url)
return PhaseId.RESTORE_VIEW;
}
}

To see my full code posting check out my blog post Access Control in JSF using a PhaseListener. I have written it to use configurable URL filters to set different required security levels.

package devgrok.jsf;

import static devgrok.jsf.AccessControlPhaseListener.AccessLevel.ADMIN;
import static devgrok.jsf.AccessControlPhaseListener.AccessLevel.LOGGED_IN;
import static devgrok.jsf.AccessControlPhaseListener.AccessLevel.NONE;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

import javax.faces.application.FacesMessage;
import javax.faces.context.FacesContext;
import javax.faces.event.PhaseEvent;
import javax.faces.event.PhaseId;
import javax.faces.event.PhaseListener;
import javax.servlet.http.HttpSession;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.sun.faces.util.MessageFactory;

import devgrok.jsf.SessionForm;
import devgrok.jsf.UrlFilter;

/**
* Phase Listener that checks the viewId (URL) against a set of filters to determine the required access level. If the
* correct level is not there then redirect.
*
* See {@link UrlFilter} for details on the url matching.
*
* @author Chris Watts 2009
*
*/
public class AccessControlPhaseListener implements PhaseListener
{
/** Logger for this class */
private static final Logger log = LoggerFactory.getLogger(AccessControlPhaseListener.class);

/** */
private static final long serialVersionUID = 1L;
private final static String SESSION_BEAN = "sessionBean";
private final HashMap<AccessLevel, List<UrlFilter>> levelFilters = new HashMap<AccessLevel, List<UrlFilter>>();

public enum AccessLevel
{
NONE, LOGGED_IN, USER_ACTIVE, ADMIN;
}

/**
*
*/
public AccessControlPhaseListener()
{
initLevels();

requires(LOGGED_IN)
.include("*")
.exclude("/index.xhtml")
.exclude("/login.xhtml")
.exclude("/user/newUser.xhtml");

requires(USER_ACTIVE)
.include("/user/*")
.exclude("/user/newUser.xhtml");

requires(ADMIN)
.include("/admin/*");
}

private void initLevels()
{
AccessLevel[] levels = AccessLevel.values();
for (int i = 1; i < levels.length; i++)
{
levelFilters.put(levels[i], new ArrayList<UrlFilter>());
}
}

private UrlFilter requires(AccessLevel level)
{
//ALL is default
if (level == NONE)
return null;

UrlFilter filter = new UrlFilter();
List<UrlFilter> list = levelFilters.get(level);
list.add(filter);
return filter;
}

/*
* (non-Javadoc)
*
* @see javax.faces.event.PhaseListener#afterPhase(javax.faces.event.PhaseEvent)
*/
public void afterPhase(PhaseEvent event)
{
try
{
//check have correct access
FacesContext context = event.getFacesContext();
HttpSession session = (HttpSession) context.getExternalContext().getSession(true);
SessionForm sessionBean = (SessionForm) session.getAttribute(SESSION_BEAN);
if (sessionBean == null)
{
log.error("Could not obtain instance of sessionBean");
return;
}

//can't use this here. only valid at render response phase?
String viewId = context.getViewRoot().getViewId();
AccessLevel required = requiredLevel(viewId);
log.debug("Required level={} for viewId={}", required, viewId);

//check if page require access:
switch (required) {
case NONE:
break;
case LOGGED_IN:
if (!sessionBean.isLoggedIn())
redirectLogin(event.getFacesContext(), sessionBean);
break;
case USER_ACTIVE:
if (!sessionBean.isActive())
redirectActive(event.getFacesContext());
break;
case ADMIN:
if (!sessionBean.isAdmin())
redirectAdmin(event.getFacesContext());
break;
default:
//error
log.error("huh?");
throw new IllegalArgumentException("Not a valid access level");
}
}
catch (Exception e)
{
// TODO Auto-generated catch block
log.error("beforePhase caught exception", e);
}

}

/*
* (non-Javadoc)
*
* @see javax.faces.event.PhaseListener#beforePhase(javax.faces.event.PhaseEvent)
*/
public void beforePhase(PhaseEvent event)
{

}

private void redirectLogin(FacesContext context, SessionForm sessionForm)
{
//trigger login popup to be shown on render.
sessionForm.logIn();
addError(context, "access.loginrequired");
context.getApplication().getNavigationHandler().handleNavigation(context, null, "index");
}

private void redirectActive(FacesContext context)
{
addError(context, "access.activerequired");
context.getApplication().getNavigationHandler().handleNavigation(context, null, "userActivate");
}

private void redirectAdmin(FacesContext context)
{
addError(context, "access.adminrequired");
context.getApplication().getNavigationHandler().handleNavigation(context, null, "home");
}

/**
* Add keyed error/message.
*
* @param level
* @param key
* message key
*/
private void addError(FacesContext context, String key)
{
FacesMessage fMessage = MessageFactory.getMessage(key);
if (fMessage != null)
{
FacesContext facesContext = FacesContext.getCurrentInstance();
fMessage.setSeverity(FacesMessage.SEVERITY_ERROR);
facesContext.addMessage(null, fMessage);
}
}

/**
* Checks defined filters for view id, checks starting at the highest level down to NONE.
*
* @return the matching level or {@link AccessLevel#NONE} if none matching.
*/
private AccessLevel requiredLevel(String viewId)
{
AccessLevel[] levels = AccessLevel.values();
for (int i = levels.length - 1; i > 0; i--)
{
if (checkLevel(levels[i], viewId))
return levels[i];
}

return AccessLevel.NONE;
}

private boolean checkLevel(AccessLevel level, String viewId)
{
return matchUri(levelFilters.get(level), viewId);
}

private boolean matchUri(List<UrlFilter> list, String uri)
{
for (UrlFilter filter : list)
{
if (filter.matches(uri))
return true;
}
return false;
}

/*
* (non-Javadoc)
*
* @see javax.faces.event.PhaseListener#getPhaseId()
*/
public PhaseId getPhaseId()
{
//ALL access go through RESTORE_VIEW and RENDER_VIEW (even direct url)
return PhaseId.RESTORE_VIEW;
}

}

 

package devgrok.jsf;

import java.util.ArrayList;
import java.util.regex.Pattern;

/**
* An inclusion/exclusion filterset, similar to ant's fileset but does not support directories in the same style(**,
* etc).
*
* For example:
* <ul>
* <li>/servlet/* matches all urls starting with "/servlet/" e.g. /servlet/this.html
* <li>*.do matches all urls that end in ".do" - e.g. mypage.do
* <li>/servlet/*.do matches all urls starting with "/servlet/" and end in ".do" - e.g. /servlet/mypage.do
* </ul>
*
* @author Chris Watts 2009
*
*/
public class UrlFilter
{
private ArrayList<Pattern> include = new ArrayList<Pattern>();
private ArrayList<Pattern> exclude = new ArrayList<Pattern>();

public UrlFilter()
{

}

/**
* Include the wildcard(*) built pattern.
*
* @param pattern
* @return
*/
public UrlFilter include(String pattern)
{
include.add(generateExpression(pattern));
return this;
}

/**
* Exclude the wildcard(*) built pattern.
*
* @param pattern
* @return
*/
public UrlFilter exclude(String pattern)
{
exclude.add(generateExpression(pattern));
return this;
}

/**
* Checks to see if uri matches at least ONE inclusion filter and doesn't match ANY exclusion filters.
*
* @param uri
* @return
*/
public boolean matches(String uri)
{
boolean match = false;

//check inclusions
for (Pattern pattern : include)
{
match = match || pattern.matcher(uri).matches();
}

if (!match)
return false;

//check exclusions
for (Pattern pattern : exclude)
{
match = match && !pattern.matcher(uri).matches();
}
return match;
}

/** regular expression special character */
private static char[] specialChars = { '[', '\\', '^', '$', '.', '|', '?', '*', '+', '(', ')' };

/**
*
* @param input
* @return
*/
private static Pattern generateExpression(String input)
{
StringBuilder sb = new StringBuilder();
for (int i = 0; i < input.length(); i++)
{
char letter = input.charAt(i);
if (letter == '*')
{
sb.append(".*");
}
else if (contains(specialChars, letter))
{
sb.append("\\" + letter);
}
else
{
sb.append(letter);
}
}
return Pattern.compile(sb.toString());
}

private static boolean contains(char[] array, char value)
{
if (array == null || array.length == 0)
{
return false;
}

for (int i = 0; i < array.length; i++)
{
char o = array[i];
if (o == value)
{
return true;
}
}

return false;
}
}
0
Your rating: None
Published at DZone with permission of its author, Chris Watts.

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)

Comments

Harihara Sudhan... replied on Sat, 2010/10/02 - 1:02am

Good one.I am searching for JSF access control mechanism for a while.Let me try this. Thanks Hari

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.