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

Reply via email to