This subject still keeps coming up on the mailing list and I thought I'd show a little bit about how I tackle this problem generally. People have been asking for a single solution to handling security ... but I don't see any single solution satisfying even the majority of projects? Why? There are too many variables. For example, are you using LDAP, OpenAuth or some ad-hoc user registry (in your database)? Are pages accessible by default, or in-accessible? Are you using role-based security? How do you represent roles then? Creating a single solution that's pluggable enough for all these possibilities seems like an insurmountable challenge ... but perhaps we can come up with a toolkit so that you can assemble your own custom solution (more on that later). One approach to security could be to define a base class, ProtectedPage, that enforced the basic rules (you must be logged in to use this page). You can accomplish such a thing using the activate event handler ... but I find such an approach clumsy. Anytime you can avoid inheritance, you'll find your code easier to understand, easier to manage, easier to test and easier to evolve. Instead, let's pursue a more declarative approach, where we use an annotation to mark pages that require that the user be logged in. We'll start with these ground rules: - Pages are freely accessible by anyone, unless they have a @RequiresLogin annotation - Any static resource (in the web context directory) is accessible to anybody - There's already some kind of UserAuthentication service that knows if the user is currently logged in or not, and (if logged in) who they are, as a User object So, we need to define a RequiresLogin annotation, and we need to enforce it, by preventing any access to the page unless the user is logged in. That poses a challenge: how do you get "inside" Tapestry to enforce this annotation? What you really want to do is "slip in" a little bit of your code into existing Tapestry code ... the code that analyzes the incoming request, determines what type of request it is (a page render request vs. a component event request), and ultimately starts calling into the page code to do the work. This is a great example of the central design of Tapestry and it's IoC container: to natively supporting this kind of extensibility. Through the use of service configurations it's possible to do exactly that: slip a piece of code into the middle of that default Tapestry code. The trick is to identify where. This image gives a rough map to how Tapestry handles incoming requests:
In fact, there's a specific place for this kind of extension: the ComponentRequestHandler pipeline service1. As a pipeline service, ComponentRequestHandler has a configuration of filters, and adding a filter to this pipeline is just what we need. Defining the Annotation First, lets define our annotation: @Target( { ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface RequiresLogin { } This annotation is designed to be placed on a page class to indicate that the user must be logged in to access the page. The retention policy is important here: it needs to be visible at runtime for our runtime code to see it and act on its presence. An annotation by itself does nothing ... we need the code that checks for the annotation. Creating a ComponentRequestFilter Filters for the ComponentRequestHandler pipeline are instances of the interface ComponentRequestFilter: /** * Filter interface for {...@link org.apache.tapestry5.services.ComponentRequestHandler}. */ public interface ComponentRequestFilter { /** * Handler for a component action request which will trigger an event on a component and use the return value to * send a response to the client (typically, a redirect to a page render URL). * * @param parameters defining the request * @param handler next handler in the pipeline */ void handleComponentEvent(ComponentEventRequestParameters parameters, ComponentRequestHandler handler) throws IOException; /** * Invoked to activate and render a page. In certain cases, based on values returned when activating the page, a * {...@link org.apache.tapestry5.services.ComponentEventResultProcessor} may be used to send an alternate response * (typically, a redirect). * * @param parameters defines the page name and activation context * @param handler next handler in the pipeline */ void handlePageRender(PageRenderRequestParameters parameters, ComponentRequestHandler handler) throws IOException; } Our implementation of this filter will check the page referenced in the request to see if it has the annotation. If the annotation is present and the user has not yet logged in, we'll redirect to the Login page. When a redirect is not necessary, we delegate to the next handler in the pipeline2 public class RequiresLoginFilter implements ComponentRequestFilter { private final PageRenderLinkSource renderLinkSource; private final ComponentSource componentSource; private final Response response; private final AuthenticationService authService; public PageAccessFilter(PageRenderLinkSource renderLinkSource, ComponentSource componentSource, Response response, AuthenticationService authService) { this.renderLinkSource = renderLinkSource; this.componentSource = componentSource; this.response = response; this.authService = authService; } public void handleComponentEvent( ComponentEventRequestParameters parameters, ComponentRequestHandler handler) throws IOException { if (dispatchedToLoginPage(parameters.getActivePageName())) { return; } handler.handleComponentEvent(parameters); } public void handlePageRender(PageRenderRequestParameters parameters, ComponentRequestHandler handler) throws IOException { if (dispatchedToLoginPage(parameters.getLogicalPageName())) { return; } handler.handlePageRender(parameters); } private boolean dispatchedToLoginPage(String pageName) throws IOException { if (authService.isLoggedIn()) { return false; } Component page = componentSource.getPage(pageName); if (! page.getClass().isAnnotationPresent(RequiresLogin.class)) { return false; } Link link = renderLinkSource.createPageRenderLink("Login"); response.sendRedirect(link); return true; } } The above code makes a bunch of assumptions and simplifications. First, it assumes the name of the page to redirect to is "Login". It also doesn't try to capture any part of the incoming request to allow the application to continue after the user logs in. Finally, the AuthenticationService is not part of Tapestry ... it is something specific to the application. You'll notice that the dependencies (PageRenderLinkSource, etc.) are injected through constructor parameters and then stored in final fields. This is the preferred, if more verbose approach. We could also have used no constructor, a non-final fields with an @Inject annotation (it's largely a style choice, though constructor injection with final fields is more guaranteed to be fully thread safe). The class on its own is not enough, however: we have to get Tapestry to actually use this class. Contributing the Filter The last part of this is hooking the above code into the flow. This is done by making a contribution to the ComponentEventHandler service's configuration. Service contributions are implemented as methods of a Tapestry module class, such as AppModule: public static void contributeComponentRequestHandler( OrderedConfiguration configuration) { configuration.addInstance("RequiresLogin", RequiresLoginFilter.class); } Contributing modules contribute into an OrderedConfiguration: after all modules have had a chance to contribute, the configuration is converted into a List that's passed to the service implementation. The addInstance() method makes it easy to contribute the filter: Tapestry will look at the class, see the constructor, and inject dependencies into the filter via the constructor parameters. It's all very declarative: the code needs the PageRenderLinkSource, so it simply defines a final field and a constructor parameter ... Tapestry takes care of the rest. You might wonder why we need to specify a name ("RequiresLogin") for the contribution? The answer addresses a somewhat rare but still important case: multiple contributions to the same configuration that have some form of interaction. By giving each contribution a unique id, it's possible to set up ordering rules (such as "contribution 'Foo' comes after contribution 'Bar'"). Here, there is no need for ordering because there aren't any other filters (Tapestry provides this service and configuration, but doesn't make any contributions of its own into it). Improvements and Conclusions This is just a first pass at security. For my clients, I've built more elaborate solutions, that include capturing the page name and activation context to allow the application to "resume" after the login is complete, as well as approaches for automatically logging the user in as needed (via a cookie or other mechanism). Other improvements would be to restrict access to pages based on some set of user roles; again, how this is represented both in code and annotations, and in the data model is quite up for grabs. My experience with different clients really underscores what a fuzzy world security can be: there are so many options for how you represent, identify and authenticate the user. Even basic decisions are underpinnings are subject to interpretation; for example, one of my clients wants all pages to require login unless a specific annotation is found. Perhaps over time enough of these use cases can be worked out to build the toolkit I mentioned earlier. Even so, the amount of code to build a solid, custom security implementation is still quite small ... though the trick, as always, is writing just the write code and hooking it into Tapestry in just the right way. I expect to follow up this article with part 2, which will expand on the solution a bit more, addressing some more of the real world constraints my customers demand. Stay tuned! 1 In fact, this service and pipeline were created in Tapestry 5.1 specifically to address this use case. In Tapestry 5.0, this approach required two very similar filter contributions to two similar pipelines. 2 If there are multiple filters, you'd think that you'd delegate to the next filter. Actually you do, but Tapestry provides a bridge: a wrapper around the filter that uses the main interface for the service. In this way, each filter delegates to either the next filter, or the terminator (the service implementation after all filters) in a uniform manner. More details about this are in the pipeline documentation. -- Posted By Howard to Tapestry Central at 12/28/2009 02:49:00 PM