You should be able to do this with spring security and grails without much trouble.
I used the following 2 ways in the past for similar tasks. Both require the spring security ACL plugin which provides the @PreAuthorize
and @PostAuthorize
annotations.
Custom PermissionEvaluator
You can use the hasPermission()
methods within security annotations and create a custom PermissionEvaluator
. Within code this looks like this:
@PreAuthorize("hasPermission(#myObject, 'update')")
public void updateSomething(myObject) {
..
}
The hasPermission()
calls are routed to a PermissionEvaluator
by spring security. To write your own implementation you have to implement the PermissionEvaluator
interface:
class MyPermissionEvaluator implements PermissionEvaluator {
@Override
public boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission) {
// your custom logic..
}
@Override
public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) {
// your custom logic
}
}
To register your PermissionEvaluator
you have to override the bean named expressionHandler
. You do this by adding the following lines in conf/spring/resources.groovy
:
beans = {
expressionHandler(MyExpressionHandler) {
parameterNameDiscoverer = ref('parameterNameDiscoverer')
permissionEvaluator = ref('myPermissionEvaluator') // your PermissionEvaluator
roleHierarchy = ref('roleHierarchy')
trustResolver = ref('authenticationTrustResolver')
}
myPermissionEvaluator(MyPermissionEvaluator)
}
Within resources.groovy
you can define beans like you would do in applicationContext.xml
when using spring. The above lines create a bean of type MyPermissionEvaluator
with the bean name myPermissionEvaluator
. Spring securities expressionHandler
bean is overridden with a bean of type MyExpressionHandler
. The other dependencies are copied from the configuration file of the spring security ACL plugin.
Service calls in security annotations
If the design of the hasPermission()
methods does not achive all you requirements you can use simple service calls instead. The @PostAuthorize
and @PreAuthorize
annotations use SPEL to evaluate the expression. Within SPEL you can use the @
symbol to access beans. For example:
@PreAuthorize("@securityService.canAccess(#myObject)")
public void doSomething(myObject) {
..
}
This calls the canAccess
method of the bean named securityService
and passes the method argument to it.
To use this approach you have to register a BeanResolver on the EvaluationContext. To do this you have to override the DefaultMethodSecurityExpressionHandler which is configured by the spring security ACL plugin.
This can look like the following:
class MyExpressionHandler extends DefaultMethodSecurityExpressionHandler {
BeanResolver beanResolver
@Override
public EvaluationContext createEvaluationContext(Authentication auth, MethodInvocation mi) {
StandardEvaluationContext ctx = (StandardEvaluationContext) super.createEvaluationContext(auth, mi)
ctx.setBeanResolver(beanResolver) // set BeanResolver here
return ctx;
}
}
BeanResolver
is a simple interface that resolves a bean name to a bean instance:
class GrailsBeanResolver implements BeanResolver {
GrailsApplication grailsApplication
@Override
public Object resolve(EvaluationContext evaluationContext, String beanName) throws AccessException {
return grailsApplication.mainContext.getBean(beanName)
}
}
And finally add the beans to resources.groovy
:
expressionHandler(MyExpressionHandler) {
parameterNameDiscoverer = ref('parameterNameDiscoverer')
permissionEvaluator = ref('permissionEvaluator')
roleHierarchy = ref('roleHierarchy')
trustResolver = ref('authenticationTrustResolver')
beanResolver = ref('beanResolver') // this is your BeanResolver
}
// This is the service called within security expressions
// If you place your service in the grails service folder you can skip this line
securityService(MySecurityService)
// this is your BeanResolver
beanResolver(GrailsBeanResolver) {
grailsApplication = ref('grailsApplication')
}
Update (2013-10-22): Recently I wrote a blog post about exactly this which provides some additional information.