Saturday, November 17, 2007

Tapestry 5 authorization

I figure if I design Buccoo modularly enough, I should be able to plug functionality from other modules in pretty easily with a little modification. One of these is the security authentication and authorization. From the Tapestry wiki there seem to be three types of approaches:
  1. In each page class check the security on the onRender method (I think that's the method name).
  2. Implement a dispatcher or a request filter that checks if the user has access.
  3. Integrate something like Acegi security.
Option one is simple, but not scalable. Also each page may need modification. Not a good option. Option two seems be covered on the Tapesty wiki and while a little more complicated seems good. It's scalable and could do everything option 1 could do. Option three looks like the best, scalable, provides for multiple authentication mechanisms. I have no clue how to work with Acegi as yet so until I get some more time, it looks like option two wins.

Option two is detailed in how to create a dispatcher and the follow up. For the purposes of Buccoo, I'll do a minimal implementation that I should be able to fill in later.

The first thing I'll do is create a UserPermissions class in the src/org/tobago/buccoo/services folder. When the current user is logged in, it should populate with what they have permission to see. There should be a function, I'll call it canAccess, which will return true if the user has permission to access the resource in the Request or not.


package org.tobago.buccoo.services;

import org.apache.tapestry.services.Request;

public class UserPermissions {

public boolean canAccess(Request request) {
boolean result = false;

return result;
}

}


The particulars of populating the permissions and actually implementing the access is something I would focus on later.

The next class I would need would be the AccessController class that I would also put in the src/org/tobago/buccoo/services folder. This is pretty much what Chris Lewis had in the Tapestry wiki except I have a couple more import statements.


package org.tobago.buccoo.services;

import java.io.IOException;

import org.apache.tapestry.services.ApplicationStateManager;
import org.apache.tapestry.services.Request;
import org.apache.tapestry.services.Response;
import org.apache.tapestry.services.Dispatcher;

import org.tobago.buccoo.services.UserPermissions;


public class AccessController implements Dispatcher {

/* Our state manager. */
private ApplicationStateManager asm;

/**
* Receive our state manager as a constructor argument. When we bind this
* service, T5 IoC will intelligently provide the state manager - batteries included!
*/
public AccessController(ApplicationStateManager asm) {
this.asm = asm;
}

public boolean dispatch(Request request, Response response) throws IOException {
boolean canAccess = true;

/*
* Per the application state documentation, we check for the existence of an
* ASO before attempting access. These prevents any unnecessary overhead
* (automatic session creation).
*/
if(asm.exists(UserPermissions.class)) {
UserPermissions perms = asm.get(UserPermissions.class);
/*
* The object referenced by 'perms' is an instance specific to
* the current request, which is what we need. Now check the
* permissions against the resource - how you do this of course
* depends on your resource restriction implementation. However
* you will most likely base this on the page (page name or
* class).
*/
canAccess = perms.canAccess(request);
}

/*
* Access control logic goes here. If the user is allowed to access the
* resource, canAccess should be set to true.
*/

if(!canAccess) {
/*
* This is an unauthorized request, so throw an exception. We'll need
* more grace than this, such as a customized exception page and/or
* redirection to a login page...
*/
throw new RuntimeException("Access violation!");
}

return false;
}
}


The very last thing to do is to register this AccessController class with Tapestry and configure it to be invoked before any request is fulfilled. We do this in the AppModule.java file. As we were not using it, I removed the timing filter example code that came with AppModule.java.


package org.tobago.buccoo.services;

import java.io.IOException;

import org.apache.tapestry.ioc.MappedConfiguration;
import org.apache.tapestry.ioc.OrderedConfiguration;
import org.apache.tapestry.ioc.ServiceBinder;
import org.apache.tapestry.ioc.annotations.InjectService;
import org.apache.tapestry.services.Request;
import org.apache.tapestry.services.RequestFilter;
import org.apache.tapestry.services.RequestHandler;
import org.apache.tapestry.services.Response;
import org.apache.tapestry.services.Dispatcher;
import org.slf4j.Logger;


/**
* This module is automatically included as part of the Tapestry IoC Registry, it's a good place to
* configure and extend Tapestry, or to place your own service definitions.
*/
public class AppModule
{

public static void bind(ServiceBinder binder)
{
// binder.bind(MyServiceInterface.class, MyServiceImpl.class);

// Make bind() calls on the binder object to define most IoC services.
// Use service builder methods (example below) when the implementation
// is provided inline, or requires more initialization than simply
// invoking the constructor.

binder.bind(AccessController.class).withId("AccessController");

}


public static void contributeApplicationDefaults(
MappedConfiguration configuration)
{
// Contributions to ApplicationDefaults will override any contributions to
// FactoryDefaults (with the same key). Here we're restricting the supported
// locales to just "en" (English). As you add localised message catalogs and other assets,
// you can extend this list of locales (it's a comma seperated series of locale names;
// the first locale name is the default when there's no reasonable match).

configuration.add("tapestry.supported-locales", "en");
}


public void contributeMasterDispatcher(OrderedConfiguration configuration,
@InjectService("AccessController") Dispatcher accessController) {

configuration.add("AccessController", accessController, "before:PageRender");
}

}


Our additions were a two step process. Calling binder.bind sets up our class to be callable by the service name we give it - in this case AccessController. The method contributeMasterDispatcher has the AccessController passed to it dynamically (via an annotation InjectService) and then we modify the configuration to call the AccessController before the PageRender stage is reached.

Remember if the access controller returns false, the rendering continues as if there's no problem. If it returns true then it's handled the request itself and there's no need to go further. Currently our code doesn't do much, but it's a simple framework for getting some simple security into Buccoo.

No comments: