In my previous post, I described how to create a simple security interceptor, which checks conditions defined using EL expressions, e.g.:
@Secure("#{loggedInUser.name == arg0.name}")
public List<Message> listMessages(User owner) { ... }
Now, it would be nice to be able to stack such annotations, so that they can be placed:
- on methods
- on classes – then the constraint applies to all methods
- on other annotations, to create “security bindings”
An example usage could be:
@Secure("#{loggedInUser != null}")
public class Messages {
@AdminOnly
public void deleteAllMessages() { ... }
@Secure("#{loggedInUser.maxMessageListCount == count}")
public List<Message> listMessages(@ELVar("user") User owner, @ELVar("count") int count) { ... }
}
where @AdminOnly
is defined as:
@SecureBinding
@Secure("#{loggedInUser.isAdministrator}")
public @interface AdminOnly { }
This way common security constraints can be expressed as annotations or on the class. I’m also using an improvement suggested by Dan Allen, to name the method arguments using @ELVar
, instead of naming them arg0
, arg1
, etc. This also allows to refer to method arguments in the “security binding” annotations, whatever the position of the argument is.
How to implement such annotations? Well, the first step is to create a portable extension (meaning it will work with any CDI implementation, not just Weld), which will gather, for each method, all the @Secure
annotations, and their values, into one single annotation, @InterceptSecure
. Having such a set of constraints to check for each method, we add the annotation to the method meta-data, using a utility class from Weld Extensions: the NewAnnotatedTypeBuilder
. As the @InterceptSecure
is an interceptor binding, the security interceptor will be called whenever the method is invoked.
The extension observes the ProcessAnnotatedType
event, which is fired for each bean type. If necessary, the type can be modified, to include the new annotations. The annotation is only added to Weld meta-data, not to the method itself (so there’s no bytecode manipulation or such).
One last obstacle to overcome is to get the value of the generated @InterceptSecure
annotation in the interceptor. Currently this is not possible using e.g. BeanManager
, but should be address in CDI Maintenance Release (see here), so as a temporary solution all the generated annotations are stored in a map in the extension. All extensions are application-scoped beans, so the information can be accessed from the interceptor. One shortcoming of the solution is that one method may belong to several, differently annotated CDI beans.
The code for the annotation and extension:
@InterceptorBinding
public @interface InterceptSecure {
@Nonbinding
String[] value();
}
public class SecurityExtension implements Extension {
private final Map<Method, InterceptSecure> interceptSecureForMethods = new HashMap<Method, InterceptSecure>();
public InterceptSecure getInterceptSecure(Method m) {
return interceptSecureForMethods.get(m);
}
public <T> void processAnnotatedType(@Observes ProcessAnnotatedType<T> event) {
// A flag indicating if the builder was used to modify the annotations
boolean used = false;
NewAnnotatedTypeBuilder<T> builder = new NewAnnotatedTypeBuilder<T>(event.getAnnotatedType());
// We need to read the values of the @Secure annotation that are present on:
// 1. types (classes)
// 2. methods
// 3. arbitrarily nested on @SecureBinding annotations
// Gathering the initial secure values from the type
List<String> initialSecureValues = new ArrayList<String>();
for (Annotation annotation : event.getAnnotatedType().getAnnotations()) {
collectSecureValues(annotation, initialSecureValues);
}
for (AnnotatedMethod<?> m : event.getAnnotatedType().getMethods()) {
// Gathering the secure values from the method
final List<String> values = new ArrayList<String>(initialSecureValues);
collectSecureValues(m, values);
// If any values have been gathered, adding the annotation to the method and storing it
// in the map.
if (values.size() > 0) {
InterceptSecure is = new InterceptSecureImpl(values.toArray(new String[values.size()]));
builder.addToMethod(m.getJavaMember(), is);
used = true;
interceptSecureForMethods.put(m.getJavaMember(), is);
}
}
// Setting the new annotated type, if any changed were made
if (used) {
event.setAnnotatedType(builder.create());
}
}
private void collectSecureValues(AnnotatedMethod m, List<String> values) {
for (Annotation annotation : m.getAnnotations()) {
collectSecureValues(annotation, values);
}
}
private void collectSecureValues(Annotation annotation, List<String> values) {
if (Secure.class.isAssignableFrom(annotation.annotationType())) {
values.add(((Secure) annotation).value());
} else {
if (annotation.annotationType().getAnnotation(SecureBinding.class) != null) {
for (Annotation nestedAnnotation : annotation.annotationType().getAnnotations()) {
collectSecureValues(nestedAnnotation, values);
}
}
}
}
private static class InterceptSecureImpl extends AnnotationLiteral<InterceptSecure> implements InterceptSecure {
private final String[] values;
private InterceptSecureImpl(String[] values) {
this.values = values;
}
@Override
public String[] value() {
return values;
}
}
}
And for the interceptor:
@Interceptor
@InterceptSecure("")
public class SecurityInterceptor {
@Inject
private SecurityExtension se;
@AroundInvoke
public Object checkSecurity(InvocationContext ctx) throws Exception {
// Getting the generated @InterceptSecure annotation for the method.
// After the CDI Maintenance Release is released, it should be possible to get the annotated
// type of the currently invoked bean, see:
// http://old.nabble.com/Retrieving-the-Bean-object-for-an-interceptor-td28147499.html
// For now, we just use a map in the extension. One limitation is that this doesn't allow
// different security annotations for methods which are used in several beans.
InterceptSecure is = se.getInterceptSecure(ctx.getMethod());
String[] toCheck = is == null ? null : is.value();
// Check the el conditions as in the previous post
// (...)
return ctx.proceed();
}
}
Adam
comments powered by Disqus