-
Simple security interceptor in Weld/JSF2
Posted on March 31st, 2010 7 commentsWaiting for the Seam3 security module, I wrote a simple security interceptor (inspired by the Seam2 security annotation). You can use it like this:
@Secure("#{loggedInUser.name == arg0.name}") public List<Message> listMessages(User owner) { ... }Here,
loggedInUseris a@NamedWeld (CDI) bean, andarg0is the first argument of the method. This is a slight modification to the Seam2 security annotation, where it wasn’t possible to reference arguments. If the EL expression doesn’t evaluate totrue, an exception is thrown.The implementation is pretty straightforward, as adding an interceptor in Weld is really simple. First, we need the annotation:
/** * @author Adam Warski (adam at warski dot org) */ @Retention(RUNTIME) @Target({TYPE, METHOD}) @InterceptorBinding public @interface Secure { /** * @return The EL expression that should be evaluated. If it evaluates to * {@code true}, access will be granted. The EL expression may reference * any objects that are in any context, as well as the arguments of the method, * under the names {@code arg0, arg1, arg2, ...}. */ @Nonbinding String value(); }The key components here is the
@InterceptorBindingmeta-annotation, and specifying the value of to be@Nonbinding. If the value element was binding, then we would need to define an interceptor for each possible String value.Next, the interceptor itself:
@Interceptor @Secure("") public class SecurityInterceptor { private String getArgName(int index) { return "arg" + index; } @AroundInvoke public Object invoke(InvocationContext ctx) throws Exception { FacesContext facesCtx = FacesContext.getCurrentInstance(); ELContext elCtx = facesCtx.getELContext(); Secure secure = getSecureAnnotation(ctx.getMethod()); String expression = secure.value(); // Populating the request map so that parameters are available (arg0, ...) Map<String, Object> requestMap = facesCtx.getExternalContext() .getRequestMap(); for (int i = 0; i < ctx.getParameters().length; i++) { Object parameter = ctx.getParameters()[i]; requestMap.put(getArgName(i), parameter); } Boolean expressionValue = (Boolean) facesCtx.getApplication() .getExpressionFactory() .createValueExpression(elCtx, expression, Boolean.class) .getValue(elCtx); // Removing the parameters (arg0, arg1, ...) for (int i = 0; i < ctx.getParameters().length; i++) { requestMap.remove(getArgName(i)); } if (expressionValue == null || !expressionValue) { throw new SecurityException(); } return ctx.proceed(); } private Secure getSecureAnnotation(Method m) { for (Annotation a: m.getAnnotations()) { if (a instanceof Secure) { return (Secure) a; } } for (Annotation a: m.getDeclaringClass().getAnnotations()) { if (a instanceof Secure) { return (Secure) a; } } throw new RuntimeException("@Secure not found on method " + m.getName() + " or its class " + m.getClass().getName()); } }And finally, we need an entry in
beans.xml, enabling the interceptor:<interceptors> <class>util.security.SecurityInterceptor</class> </interceptors>
One shortcoming is that it’s not currently possible to place one
@Secureannotation on the class, and another on the methods (see this thread on the forum). The idea is that then the class-level annotation expresses general security constraints, which can be later refined on the method level.Another missing feature, which could be easily added, is a message parameter to the annotation, which would be included in the exception in case the check fails.
Adam
-
JSF2 navigation: post->redirect->get
Posted on March 11th, 2010 2 commentsJSF2 improves a lot both how navigation can be done (you can now return a view id from an action method, no need to describe every navigation case in faces-config.xml) and how URLs are handled (finally, GET support). JSF2 introduces view parameters (for those who know Seam: standardized page parameters). Each page can define a metadata section, where the view parameters are described, bound to bean values, converted and validated.
As an example, a blog-entry-viewing page would define the id of the entry to be displayed as follows:
<f:metadata> <f:viewParam name="entry_id" value="#{blog.entry}" required="true"> <f:converter converterId="blog-entry-converter" /> </f:viewParam> </f:metadata>Unfortunately I had some trouble with one thing: how to redirect to a page, including the view parameters, after a POST? This post->redirect->get pattern is very common. E.g. when you post a new comment for a blog entry and press submit, the data is persisted and you want to be redirected back to
view.jsf?entry_id=819(using the default JSF command-button behavior, you would land on a plain, non-bookmarkableview.jsf).Dan Allen wrote a series of very good introductory articles to JSF2 on DZone. There, he writes that what I described above should be possible to achieve by adding a
<redirect include-view-params="true"/>tag to the appropriate<navigation-case>infaces-config.xml. Unfortunately, the xsd doesn’t allow such an attribute and it doesn’t work – I suppose that this construct didn’t make it into the final version of the spec (although somebody may correct me if I’m wrong).Another solution, this time working, can be found on Ed Burns’s blog. The trick is to return a string containing the view id and some additional parameters from the action method or use them as the command button/link action, e.g.:
public String action() { // business logic ... return "view.xhtml?faces-redirect=true&includeViewParams=true" }However this way you’ll have to repeat the combination of the “magical parameters” a lot in your code. And it’s pretty easy to do a spelling mistake in one of the strings you return. Furthermore, it’s not possible to easily include one view parameter, without repeating the value mapping.
The way I solved this is by introducing a
Navcomponent (I’m using Weld), which holds information about pages. It contains a nestedPageclass, which has a “fluent” interface for building a link. Navigation then looks as follows:@Inject private Nav nav; public String action() { // business logic ... return nav.getViewEntry().redirect().includeViewParams().s(); }Or, if you want to include only one parameter:
public String action() { // business logic ... return nav.getViewEntry().redirect().includeViewParam("name").s(); }In xhtml pages, you can also use the
navcomponent to generate links:<h:link outcome="#{nav.manageIndex.s}">Manage</h:link>Notice that you completely abstract away from the actual names of the xhtml views (pages) – they are stored centrally only in the
navcomponent! This makes any refactorings really easy.Speaking of the
navcomponent, here’s the code:/** * @author Adam Warski (adam at warski dot org) */ @Named @ApplicationScoped public class Nav { public static class Page { private final String viewId; private final Map<String, String> params; private Page(String viewId) { this.viewId = viewId; this.params = new LinkedHashMap<String, String>(); } private Page(String viewId, Map<String, String> params) { this.viewId = viewId; this.params = params; } public Page redirect() { return includeParam("faces-redirect", "true"); } public Page includeViewParams() { return includeParam("includeViewParams", "true"); } public Page includeViewParam(String name) { // Getting the metadata facet of the view FacesContext ctx = FacesContext.getCurrentInstance(); ViewDeclarationLanguage vdl = ctx.getApplication().getViewHandler() .getViewDeclarationLanguage(ctx, viewId); ViewMetadata viewMetadata = vdl.getViewMetadata(ctx, viewId); UIViewRoot viewRoot = viewMetadata.createMetadataView(ctx); UIComponent metadataFacet = viewRoot.getFacet( UIViewRoot.METADATA_FACET_NAME); // Looking for a view parameter with the specified name UIViewParameter viewParam = null; for (UIComponent child : metadataFacet.getChildren()) { if (child instanceof UIViewParameter) { UIViewParameter tempViewParam = (UIViewParameter) child; if (name.equals(tempViewParam.getName())) { viewParam = tempViewParam; break; } } } if (viewParam == null) { throw new FacesException("Unknown parameter: '" + name + "' for view: " + viewId); } // Getting the value String value = viewParam.getStringValue(ctx); return includeParam(name, value); } public Page includeParam(String name, String value) { Map<String, String> newParams = new LinkedHashMap<String, String>(params); newParams.put(name, value); return new Page(viewId, newParams); } public String s() { StringBuilder sb = new StringBuilder(); sb.append(viewId); String paramSeparator = "?"; for (Map.Entry<String, String> nameValue : params.entrySet()) { sb.append(paramSeparator).append(nameValue.getKey()) .append("=").append(nameValue.getValue()); paramSeparator = "&amp;"; } return sb.toString(); } public String getS() { return s(); } } private final Page manageIndex = new Page("/manage/index.xhtml"); private final Page manageUsers = new Page("/manage/users.xhtml"); private final Page home = new Page("/home.xhtml"); private final Page thisPage = new Page(""); // other pages ... public Page getManageIndex() { return manageIndex; } // other getters ... }Looking forward, the
Pageclass may also include e.g. security management, however that would require some more JSF bindings.Adam

