Saturday, May 12, 2007

Clean and Flexible Authorisation for Tapestry Applications

Currently we'r working on an application that will be used by 4 different departments. And I can already anticipate people dropping in to casually remark someting like "Oh, by the way, we've hired this guy who needs Access to module X of the invoicing system, but of course he mustn't be allowed to press button Y, and he needs to be able to change the comment-field. And don't hurry, he'll not really need it until today afternoon."

Furthermore, the app is meant to be re-usable by subsidiaries in other countries. So putting conditionals in the view-templates (anyway bad) or cluttering component tags with disabled="ognl: admin || hasRole('blub')" kind of parameters was clearly no option.

The solution I liked best was to introduce an "@AuthorisedBlock" component which iterates over all form components it contains (in its tag-body), looks up the current users permissions (r, rw, neither) for them and, accordingly decides whether to render them enabled, disabled or not at all (see source-code at the end of this post).

The permission lookup is delegated to a simple rules engine. A rule is basically a triple (role, id-regular-expression, permission). The rules engine determines the rule with the longest pattern-match for the id-regexp - considering, of course, only rules which apply to the current user's roles.

So, in the template there's only the <span jwcid="@AuthorisedBlock"> tag enclosing the part of the template subject to authorisation checking. The actual authorisation rules can be put into a config file or into the database.

Of course, this pattern should be implementable using any web-framework with a notion of components. But Tapestry has a couple of nice features that make it particularly easy and straightforward to implement:

  • Tapestry's form-components implement a well-known, useful interface. Thus @AuthorisedBlock works with any of them without the need to build cumbersome wrappers around 3rd party library components just to support authorisation properly.

  • A component can easily control the rendering of its body, i.e. the components belonging to the child tags of its own.

  • A component takes part in the construction of a page's component trees in an oo-manner. This makes it easy to setup the binding for the disabled parameter.


In short, Tapestry makes it easy to write the rather frameworky parts of an application, because it has nicely coherent inner workings and it's very liberal in exposing these to the application programmer and letting him change them. At least, this is true for versions 3 and 4 of Tapestry. Version 5 may become a bit more secretive. I'm not sure, whether I'll actually like that. Of course, I see the intended win: A Tap 3/4 app's GUI code will typically depend quite heavily on the framework, even on parts that were perhaps never intended to be publicly (ab-)used. So, a framework update to a new major version will be hard. However, I don't think this is really harmful. After all: Your frontend code doesn't contain any business logic, right? So, when you really need to go for a major frontend framework update, what could be the reason for this need? You'll probably want to give your application a fairly complete overhaul of the whole GUI anyway and you want to do it in a new way using the new features of your new framework (-version). What's the big point in backwards compatibility then?


public abstract class AuthorisedBlock extends AbstractComponent {
@InjectObject("service:iis.web.AuthorisationSvc")
public abstract AuthorisationSvc getAuthSvc();

@Override
protected void renderComponent(IMarkupWriter writer, IRequestCycle cycle) {
renderBody(writer, cycle);
}

@Override
public void renderBody(IMarkupWriter writer, IRequestCycle cycle) {
for (int i = 0; i < _bodyCount; i++) {
IRender r = _body[i];
if ( r instanceof IFormComponent ) {
IFormComponent formComp = (IFormComponent) r;
AccessPermission permission
= getAuthSvc().permissionFor( formComp.getExtendedId() );
if ( permission == invisible )
continue;

}
cycle.getResponseBuilder().render(writer, r, cycle);
}
}

@Override
public void addBody(IRender r) {
super.addBody(r);
if ( r instanceof IFormComponent ) {
IFormComponent formComp = (IFormComponent) r;
formComp.setBinding("disabled",
new DisabledBinding(formComp.getExtendedId()) );
}
}

private class DisabledBinding implements IBinding {

private String extendedId;

public DisabledBinding(String extendedId) {
this.extendedId = extendedId;
}

public Location getLocation() {
return null;
}

public String getDescription() {
return toString();
}

public Object getObject() {
return getAuthSvc().permissionFor(extendedId) == readOnly;
}

public Object getObject(Class type) {
return getObject();
}

public boolean isInvariant() {
return false;
}

public void setObject(Object value) {
throw new UnsupportedOperationException(this+" is a read-only binding");
}

@Override
public String toString() {
return "sythetic disabled binding for " + extendedId+" to AuthorisedBlock";
}

}

}