Adam Warski

31 Mar 2010

Simple security interceptor in Weld/JSF2

jee
java
jsf

Waiting 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, loggedInUser is a @Named Weld (CDI) bean, and arg0 is 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 to true, 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 @InterceptorBinding meta-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 @Secure annotation 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

comments powered by Disqus

Any questions?

Can’t find the answer you’re looking for?