This blog is mainly about Java...

Saturday, February 13, 2010

Advanced Seam series part 1 of 3: Seam Interceptors

Seam Interceptors

EJB 3.0 introduced a standard interceptor model for session bean components. Interceptors are a great way of performing extra functionality. I will not go into detail of what Interceptors are. If you want to know more, google is your friend. 

Prerequisites you should know:
  • Seam has a few built in Interceptors, these include but are not limited to:
    • BijectionInterceptor
    • MethodContextInterceptor 
    • ConversationInterceptor
    • SynchronizationInterceptor
    • ConversationalInterceptor
    • RemoveInterceptor
    • SeamInterceptor
    • SecurityInterceptor
    • TransactionInterceptor
    • EventInterceptor
    • HibernateSessionProxyInterceptor   
  • @BypassInterceptors will skip Interceptors 
In this example I will show you two examples on how you can use your own interceptor as a way to enhance your security domain and measure how long each method takes to execute.

First example
Imagine this simple model. A user is connected to one organization, and only the user connected to that Organization is allowed to view information about that organization.



Let's say you print out the users organization, and you have a page with detailed information about that organization like this:

<s:link view="myOrganization.xhtml" value="#{organizationAction.choose}">
    <f:param name="id" value="#{organization.id}"/>
</s:link>

In the request, something like this will appear:
http://mydomain.com/myOrganization.xhtml?actionMethod=myOrganization.xhtml%3organizationAction%choose()&organizationId=3

And this is how your simple OrganizationAdmin.java component looks like:

@Name("organizationAdmin")
@Scope(ScopeType.CONVERSATION)
@Restrict("#{s:hasRole('user')}&;quot;)
public class OrganizationAction {

    @RequestParameter
    Long id;

    @In
    EntityManager entityManager;

    Organization myOrganization;
   
    @Begin
    public void choose() {
        if(id != null) {
            this.myOrganization = (Organization) entityManager.find(Organization.class, id);
        }
    }

    public Organization getMyOrganization() {
          return this.myOrganization;
    }
}

Now, imagine changing the organizationId in the request to show information about an organization you should initially not be allowed to see.

This is obviously very bad. We have restricted the component to role user, but another user can easily change the request to another id, and get hold of another organization.

To fix this, you can add this extra security check:

@Name("organizationAdmin")
@Scope(ScopeType.CONVERSATION)
@Restrict("#{s:hasRole('user')}")
public class OrganizationAction {

    @RequestParameter
    Long id;

    @In
    EntityManager entityManager;

    Organization myOrganization;
   
    @In(create = true)
    User currentUser; //The current user logged in
   
    @Begin
    public void choose() {
        if(id != null) {
            this.myOrganization = (Organization) entityManager.find(Organization.class, id);
            if(!currentUser.getOrganization().getId().equals(myOrganization.getId()))
                throw new SecurityException("You are not allowed to view this page");
        }
    }

    public Organization getMyOrganization() {
          return this.myOrganization;
    }
}

Voila! Now if an authorized user tries to "hack" the system, you will throw a SecurityException. This is fine and works just fine. However, it is cumbersome to do and you have to remember to do this everywhere where these exploits exists. 
Wouldn't it be nice to just do this:

@Name("organizationAdmin")
@Scope(ScopeType.CONVERSATION)
@Restrict("#{s:hasRole('user')}")
@MySecurityAuthorized
public class OrganizationAction {

    @RequestParameter
    Long id;

    @In
    EntityManager entityManager;

    Organization myOrganization;
 
    @Begin
    public void choose() {
        if(id != null) {
            this.myOrganization = (Organization) entityManager.find(Organization.class, id);
        }
    }
    @AuthorizeOrganization
    public Organization getMyOrganization() {
          return this.myOrganization;
    }
}

I for one think this is much easier to do. You can even reuse the @AuthorizeOrganization annotation to apply the same security other places. You only need to write the logic once.

Our first Interceptor
We can do this by using @Interceptor or @Interceptors. The latter is pre Java EE 5 compatible. I will cover both, but will show the latter in this example.

We start by creating our two annotations.
@AuthorizeOrganization
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AuthorizeOrganization {}
@MySecurityAuthorized
/**
 * This annotation can be added to any Seam component to hook in interception of our custom annotations.
 * Important: Unless a custom specific @AuthorizeX annotation (like @AuthorizeOrganization) is also present, this annotation won't do anything 
 *
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@org.jboss.seam.annotations.intercept.Interceptors(MyInterceptor.class)
public @interface MySecurityAuthorized {}

Note the @Interceptors(MyInterceptor.class). It is here we register this annotation to the Interceptor MyInterceptor.
AnnotationHandler.java
Note that this is just an interface that all our custom annotations will inherit from


public interface AnnotationHandler {
    public void preProceed();
    public void postProceed(Object retObj);
    public boolean isCurrentlyUsed();
}

BasicHandler.java
An abstract class all handler will extend from. Read javadoc and comments in code to get real understanding of what's going on. Much of this code is really not necessary for this simple example. But this is a good portion of the one we are using, and it is quite flexible.

import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import javax.interceptor.InvocationContext;

import org.jboss.seam.Component;
import org.jboss.seam.core.Events;
import org.jboss.seam.log.Logging;

/**
 * Class that can be extended when creating new custom managed annotations.
 * Add extending classes to MyInterceptor.
 *
 * @param <T> The Annotation that should be looked for
 */
public abstract class BasicHandler<T extends Annotation, U> {

    private User user;
    private Boolean isAdmin;
    
    private boolean isMethodAnnotated;
    private List<U> allInstanceParams = new ArrayList<U>();
    private List<U> annotatedInstanceParams = new ArrayList<U>();
    private List<Long> annotatedIdParams = new ArrayList<Long>();
    
    private Class<U> authorizedClass;
    
    /**
     * This constructor parses the method signature for annotations,
     * validated annotated types and
     * if needed verifies the current User
     * @param ic, the current InvocationContext
     * @param annotationClazz, the class of the annotation to be scanned for
     * @param authorizedClass, the class of the object to authorize
     */
    @SuppressWarnings("unchecked")
    public BasicHandler(InvocationContext ic, Class<T> annotationClazz, Class<U> authorizedClass) {

        this.authorizedClass = authorizedClass;
        
        //method annotation
        Annotation annotation = ic.getMethod().getAnnotation(annotationClazz);
        if (annotation != null) {
            isMethodAnnotated = true;
        }
        
        //parameter annotations - why why why is there no Parameter class with methods for annotations, values, types?
        Annotation[][] aaa = ic.getMethod().getParameterAnnotations();
        for (int i = 0; i < aaa.length; i++) {
            Object obj = ic.getParameters()[i];    
            if (isMethodAnnotated) {
                allInstanceParams.add((U) obj);
            }
            Annotation[] aa = aaa[i];
            for (int j = 0; j < aa.length; j++) {
                Annotation a = aa[j];
                if (a.annotationType().equals(annotationClazz) && ic.getParameters()[i] != null) {
                    //So we've found an annotated not null param
                    if (authorizedClass.isInstance(obj)) {
                        //instance of class to be authorized
                        annotatedInstanceParams.add((U) obj);
                    } else if (obj instanceof Long) {
                        //id of instance of class to be authorized
                        annotatedIdParams.add((Long) obj);
                    } else {
                        //Annotation put on erronous type
                        illegalObjectAnnotated((U) obj, annotationClazz);
                    }
                    //params.add(ic.getParameters()[i]);
                }
            }    
        }

        //Verify user
        if (isCurrentlyUsed()) {
            getUser();
        }        
    }
    
//public methods
    
    public boolean isCurrentlyUsed() {
        return (isMethodAnnotated || !annotatedInstanceParams.isEmpty() || !annotatedIdParams.isEmpty() ? true : false);
    }
    
//protected methods
    
    @SuppressWarnings("unchecked")    
    protected List<U> getAllReturnInstances(Object retObj) {
        List<U> l = new ArrayList<U>();
        
        if (isMethodAnnotated()) {            
            if (authorizedClass.isInstance(retObj)) {
                //Simple object
                l.add((U) retObj);
            } else if (retObj instanceof Collection<?>) {
                //Collections
                for (Object obj : (Collection<?>) retObj) {
                    if (authorizedClass.isInstance(obj)) {
                        l.add((U) obj);
                    }                     
                }
            }
        }            
        return l;
    }

    /**
     * Authorizing Organization
     * Role "admin" may access everything
     * Other (like "user") must be within the organization to have privilege
     * @param organization
     */    
    protected boolean authorize(Organization organization) {
        if (organization != null) {
            return authorizeUser(organization.getId().toString());
        } else {
            //null object need no authorization                
            return true;            
        }        
    }
    
    /**
     * Getting current logged in user
     * @return Getting current logged in user
     */
    protected User getUser() {
        if (user == null) {
            user = (User) Component.getInstance("currentUser", true);
            if (user.getId() == null || user.getId() == 0) {
                //annotation is present but we don't have an user
                logAndThrow("User is null, must exsist to enable authentication");
            } 
        }
        return user;
    }
    
    /**
     * Is user admin?
     * @return
     */
    protected boolean isAdmin() {
        if (isAdmin == null) {
            return isRoleInRoles(user.getRoles(), "admin");
        }
        return isAdmin;
    }

    /**
     * Checks to see if the role is in the Collection<Role>
     * If one of the roles appears in the Collection it returns true
     * 
     * @param allroles - All the roles
     * @param roles - varargs containing the roles to check for
     * 
     * @return true if any of the role is found
     */
    private boolean isRoleInRoles(Collection<Role> allroles, String ...roles) {
        if (allroles == null || roles == null || roles.length == 0)
            return false;

        for (Role r : allroles)
            for(String role : roles)
                if (r.getName().equals(role))
                    return true;

        return false;
    }
    
    protected boolean logAndThrow(String s) {
        Logging.getLogProvider(this.getClass()).warn(s);
        throw new SecurityException(s);
    }

//private methods

    private boolean authorizeUser(String orgId) {
        User user = getUser();

        if (isAdmin()) {
            //Admin can do whatever            
            return true;
        } else if (user.getOrganization().getId().equals(orgId)) {
            //Correct organization, can also check role here if you want
            return true;                                
        } else {
            //wrong organization, fail authorization
            return logAndThrow("User '" + user.getUsername() + "' is not authorized to view this page");
        }                
    }
    
    private boolean isMethodAnnotated() {
        return isMethodAnnotated;
    }
    
    private void illegalObjectAnnotated(U obj, Class<? extends Annotation> annotationClass) {
        logAndThrow("Program error, annotation " + annotationClass.getSimpleName() + " cannot handle object of type " + obj.getClass().getSimpleName());
    }    
    
//Getters    

    public List<U> getAllInstanceParams() {
        return allInstanceParams;
    }    
    
    public List<U> getAnnotatedInstanceParams() {
        return annotatedInstanceParams;
    }    

    public List<Long> getAnnotatedIdParams() {
        return annotatedIdParams;
    }        
}

@AuthorizeOrganizationHandler.java

/**
 * This AnnotationHandler handles the @AuthorizeOrganization annotation.
 * It authorizes a User for an Organization
 *
 */
public class AuthorizeOrganizationHandler extends BasicHandler<AuthorizeOrganization, Organization> implements AnnotationHandler {
    
    /**
     * This constructor looks for @AuthorizeOrganization markers on method
     * If any exists, User is validated directly.
     * @param ic
     */
    public AuthorizeOrganizationHandler(InvocationContext ic) {
        super(ic, AuthorizeOrganization.class, Organization.class);
    }

//Public methods
    
    public void preProceed() {
        //Method level - input parameters
        for (Organization obj : getAllInstanceParams()) {
            authorize(obj);
        }
    }

    public void postProceed(Object retObj) {
        //return value(s)
        for (Organization obj : getAllReturnInstances(retObj)) {
            authorize(obj);
        }        
    }
        
}


Last but not least, our interceptor.
MyInterceptor.java

import javax.interceptor.AroundInvoke;
import javax.interceptor.InvocationContext;

/**
 * This interceptor handles method/param level annotations
 * (Seam only provides Class (ElementType.TYPE) level annotation support)
 * Thus this interceptor must be added to those beans that wish to use these annotations
 * If we implement new annotations, their corresponding AnnotationHandler must be registered below
 *
 */
public class MyInterceptor {
    @AroundInvoke
    public Object aroundInvoke(InvocationContext ic) throws Exception {
        
        //gather custom annotation handlers
        List<AnnotationHandler> ahs = new ArrayList<AnnotationHandler>();
        addHandlerIfNeeded(new AuthorizeOrganizationHandler(ic), ahs);
        //add new annotation handlers here - TODO: Use more clever generics instead
        
        //handle pre proceed
        for (AnnotationHandler ah : ahs) {
            ah.preProceed();
        }

        //proceed
        Object obj = ic.proceed();
        
        //handle post proceed
        Collections.reverse(ahs); //Reversing list in order to get same behavior as interceptors/filters
        for (AnnotationHandler ah : ahs) {
            ah.postProceed(obj);
        }
                
        return obj;
    }

    private void addHandlerIfNeeded(AnnotationHandler handler, List<AnnotationHandler> ahs) {
        if (handler.isCurrentlyUsed()) {
            ahs.add(handler);
        }
    }
}

Thats it! Now you can easily add new annotation and handlers and just add login in BasicHandler. If you look closely you will see that we automatically give all privileges to admin, but only check user's organization if they are role user. You can modify and do whatever fits your need.

Second Example

Finally I want to also mention the other Seam interceptor.
This is taken directly from the Seam forum (Link here). Which will print something like this:
284.94 ms   1   FooBean.getRandomDroplets()
284.56 ms   1   GahBean.getRandomDroplets()
201.60 ms   2   SohBean.searchRatedDoodlesWithinHead()
185.94 ms   1   FroBean.doSearchPopular()
157.63 ms   1   FroBean.doSearchRecent()
 42.34 ms   1   FooBean.fetchMostRecentYodel()
 41.94 ms   1   GahBean.getMostRecentYodel()
 15.89 ms   1   FooBean.getNoOfYodels()
 15.00 ms   1   GahBean.getNoOfYodels()
  9.14 ms   1   SohBean.mainYodels()
  1.11 ms   2   SohBean.trackHoorayEvent()
  0.32 ms   1   FroBean.reset()
  0.22 ms  43   NohBean.thumbPicture()
  0.03 ms  18   FooBean.getMostRecentYodels()
  0.01 ms   1   NohBean.profilePicture()
  0.01 ms   1   FroBean.setToDefault()
  0.01 ms   1   FroBean.getRecentMarker() 



The first column contains the accumulated time spent in this method. The second column contains the number of times the method has been called, and the final column shows which method was called.

This Interceptor will measure how long each method will take to execute in milliseconds.
TimingInterceptor.java

import java.lang.reflect.Method;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

import org.jboss.seam.annotations.intercept.AroundInvoke;
import org.jboss.seam.annotations.intercept.Interceptor;
import org.jboss.seam.core.BijectionInterceptor;
import org.jboss.seam.core.ConversationInterceptor;
import org.jboss.seam.core.ConversationalInterceptor;
import org.jboss.seam.core.EventInterceptor;
import org.jboss.seam.core.MethodContextInterceptor;
import org.jboss.seam.core.SynchronizationInterceptor;
import org.jboss.seam.ejb.RemoveInterceptor;
import org.jboss.seam.ejb.SeamInterceptor;
import org.jboss.seam.intercept.InvocationContext;
import org.jboss.seam.persistence.HibernateSessionProxyInterceptor;
import org.jboss.seam.security.SecurityInterceptor;
import org.jboss.seam.transaction.TransactionInterceptor;

/**
 * This interceptor will calculate how long it takes to execute each method
 * 
 * Copied and modified from @link http://www.seamframework.org/Community/SeamPerformanceProblemRewardingWorkaround
 *
 */
@Interceptor(
    around = {
        BijectionInterceptor.class,
        MethodContextInterceptor.class,
        ConversationInterceptor.class,
        SynchronizationInterceptor.class,
        ConversationalInterceptor.class,
        RemoveInterceptor.class,
        SeamInterceptor.class,
        SecurityInterceptor.class,
        TransactionInterceptor.class,
        EventInterceptor.class,
        HibernateSessionProxyInterceptor.class
})
public class TimingInterceptor {

    public final static CallChain callChain = new CallChain();

    @AroundInvoke
    public Object timeCall(InvocationContext invocation) throws Exception {
        long t0 = System.nanoTime();
        try {
            return invocation.proceed();
        } finally {
            long dt = System.nanoTime() - t0;
            callChain.addInvocation(invocation, dt);
        }
    }

    // -----------------------------------------------------------------------------

    /**
     * A call chain is the set of invocations on methods (annotated 
     * with MeasureCalls) that a request issued on its way through 
     * the application stack.
     */
    public static class CallChain extends ThreadLocal<Map<Method, TimedInvocation>> {

        @Override
        protected Map<Method, TimedInvocation> initialValue() {
            return new HashMap<Method, TimedInvocation>();
        }

        public void addInvocation(InvocationContext invocation, long dt) {
            Map<Method, TimedInvocation> invocations = get();
            Method method = invocation.getMethod();
            if (!invocations.containsKey(method)) {
                invocations.put(method, new TimedInvocation(invocation.getMethod(), dt));
            } else {
                TimedInvocation timedInvocation = invocations.get(method);
                timedInvocation.anotherCall(dt);
            }
        }

        public int totalNumberOfInvocations() {
            Map<Method, TimedInvocation> invocations = get();
            Collection<TimedInvocation> timedInvocationCollection = invocations.values();
            int totCalls = 0;
            for (TimedInvocation invocation : timedInvocationCollection)
                totCalls += invocation.getCalls();
            return totCalls;
        }
    }
}

TimedInvocation.java

import java.lang.reflect.Method;

import org.apache.commons.lang.StringUtils;

/**
 * TimedInvocation is an invocation (i.e. a method call) which is being
 * counted and timed. 
 */
public class TimedInvocation implements Comparable<TimedInvocation> {

    private long dt;
    private int calls = 1;
    private Method method;

    public TimedInvocation(Method method, long dt) {
        this.method = method;
        this.dt = dt;
    }

    public long getDt() {
        return dt;
    }

    public Method getMethod() {
        return method;
    }

    /**
     * The first column contains the accumulated time spent in this method. 
     * The second column contains the number of times the method has been called.
     * The third column contains the method which was called
     */
    public String toString() {
        String className = method.getDeclaringClass().getName();
        String shortendName = className.substring(method.getDeclaringClass().getPackage().getName().length() + 1);
        String duration = StringUtils.leftPad(Double.valueOf(dt / 1e6) + " ms", 11);
        String nCallStr = StringUtils.leftPad(String.valueOf(calls), 4);
        return duration + nCallStr + "   " + shortendName + "." + method.getName() + "()";
    }

    public void anotherCall(long dt) {
        this.dt += dt;
        calls++;
    }

    public int compareTo(TimedInvocation o) {
        return -Long.valueOf(dt).compareTo(o.dt);
    }

    public int getCalls() {
        return calls;
    }
}

Lastly, we will create a simple filter that will print out the result. The filter is only installed in debug mode so that it doesn't run on production. Be aware that the interceptor will still run if you don't forget to uncomment the interceptor.
TimingFilter.java

/**
 * This filter is used in conjunction with the TimingInterceptor. * 
 * It is defaulted to only work in debug mode (debug = true)
 * To use this filter you need to set value to true
 * 
 */
@Startup
@Scope(ScopeType.APPLICATION)
@Name("timingFilter")
@BypassInterceptors
@Filter
@Install(debug = true)
public class TimingFilter extends AbstractFilter {
    
    private static final LogProvider log = Logging.getLogProvider(TimingFilter.class);
    
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {

        if (!(req instanceof HttpServletRequest)) {
            chain.doFilter(req, res);
            return;
        }
        
        TimingInterceptor.callChain.remove();
        chain.doFilter(req, res);
        
        Map<Method, TimedInvocation> invocations = TimingInterceptor.callChain.get();
        for(TimedInvocation timedInvocation : invocations.values()) {
            log.debug(timedInvocation.toString());
        }
    }
}
Finally create an annotation where you register the interceptor.

@MeasureCalls

/**
 * This is an annotation that is used with the Interceptor TimingInterceptor,
 * that will measure time it takes to execute each method in a seam component.
 * 
 * The filter that uses this interceptor only works in debug mode and you will need to set value to true 
 * on the TimingFilter. Otherwize you will not see anything when running this in production or test. 
 * However, the interceptor will still run, so be "careful" where you put this annotation.
 * 
 * Usage: 
 * @AutoCreate
 * @Name("theBeanILikeToInvestigate")
 * @MeasureCalls
 * public class TheBeanILikeToInvestigare
 * 
 *
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Interceptors(TimingInterceptor.class)
public @interface MeasureCalls {}

Just put @MeasureCalls on your component and all methods on that component will be invoked on.

Tuesday, February 9, 2010

Advanced Seam series

In the upcoming weeks I will be posting some nice articles about some of the advanced features of Seam I know people have trouble understanding and incorporating. 

The articles will be a "three" (may be more) part series where I will cover the topics Interceptors, Filters and Asynchronous events.

I will show working real life examples of all three. You will learned:
  • What Interceptors are, and how you can use them and incorporate security techniques using interceptors.
  • Filters - Why they are useful to use. 
    • I will show how you can implement a filter that enables downloading of files without consumption of unnecessary memory.
  • Asynchronous - I will show how you can send a mail asynchronously

If there are any advanced areas you wish to know more about, let me know by writing in the comment section and I will consider adding them to the series.

Labels