Fine-Grained Security Simplified
Securing an application’s actions by user and role is easy, but what about this fine-grained security? For many applications it’s important to restrict access to specific domain object instances. We could use Spring security ACLs but there is a simpler way to solve this problem — all it takes is
- a few annotations,
- a filter and
- a little bit of meta-programming.
Here’s a bit of before/after controller code so you can see where I’m headed. First, a couple actions from an unsecured Grails controller for accessing and manipulating a simple document domain object (I simplified it a bit for blog readability):
def edit = { def documentInstance = Document.get(params.id) if (!documentInstance) { flash.message = "Can't find a document with id:${params.id}!" render view:"/error/notAuthorized" } else { return [documentInstance:documentInstance] } } def update = { def documentInstance = Document.get(params.id) if (!documentInstance) { flash.message = "Can't find a document with id:${params.id}!" render view:"/error/notAuthorized" } else { documentInstance.properties = params if (!documentInstance.hasErrors() && documentInstance.save(flush:true)) { flash.message = "Error updating document id:${params.id}!" redirect action:"edit", id:documentInstance.id } else { render view:"edit", model:[documentInstance:documentInstance] } } }
And now with fine-grained security:
@WithDocumentInUsersCourt def edit = { [documentInstance:request.document] } @WithDocumentInUsersCourt def update = { request.document = params if (request.document.hasErrors() || !request.document.save(flush:true)) { flash.message = "Error updating document id:${params.id}!" redirect action:"edit", id:request.document.id } else { render view:"list", model:[documentInstance:request.document] } }
The code is much cleaner and it implements instance level, fine-grained security: nobody can mess around with the url and find their way into off-limits data. It’s all implemented with a couple annotation classes and a filter that checks each action allowing access only when a specific condition is met. If you’re paying attention I’m sure you have a few questions. Like “how does that annotation secure anything?” and “how did the document get onto the request?” Let’s build on this controller example so you can see how it works.
Imagine if you will an application that allows two users to collaboratively edit a document. Like this…
- First Joey creates a new Document.
- Then he sends it on to Steve.
- Steve edits the document and sends it back to Joey.
- and so on.
As the document is sent back and forth the users each in turn can either edit or view the document. The fine-grained security let’s a user edit the document only when it’s in his court. So when the document is in Steve’s court Joey can’t edit it. When the document is in Joey’s court Steve can’t edit it. Here’s a document domain object:
class Document implements Serializable { String text User party1 User party2 Integer court = 1 // must be 1 or 2 static constraints = { court inList:[1, 2] } boolean isFor(user) { (party1 == user) || (party2 == user) } boolean inUsersCourt(user) { ((party1 == user) && (court == 1)) || ((party2 == user) && (court == 2)) } }
Source code for a sample application is available here: fine-grained.tar.gz.
Adding Security
Each of the controller’s actions should be secured like this:
- document/list — lets any user see the list of documents
- document/create — lets any user create a new document
- document/save — lets any user save a new document
- document/show — lets a user see a document only when it is in his court
- document/edit — lets a user edit a document only when it is in his court
- document/update — lets a user save changes to a document only when it is in his court
- document/delete — lets a user delete a document only when he’s party to it
The first step in securing the application is to ensure that the user is authenticated (logged in) before they use any of these actions. Easy to do by adding a Spring security annotation to the controller (look at the Spring Security doc for details):
import grails.plugins.springsecurity.Secured @Secured(["hasRole('ROLE_USER)"]) class DocumentController { ...
With that done, the second step is to add the fine grained security. In the controller class this is done with annotations:
@WithDocument def show = { [documentInstance:request.document] } @WithDocumentInUsersCourt def edit = { [documentInstance:request.document] } @WithDocumentInUsersCourt def update = { request.document = params if (request.document.hasErrors() || !request.document.save(flush:true)) { flash.message = "Error updating document id:${params.id}!" redirect action:"edit", id:request.document.id } else { render view:"list", model:[documentInstance:request.document] } } @WithDocumentInUsersCourt def delete = { try { request.document.delete(flush:true) flash.message = "Deleted document id:${params.id}." redirect action:"list" } catch (DataIntegrityViolationException e) { flash.message = "Error deleting document id:${params.id}." redirect action:"show", id:params.id } }
The Annotations
The annotations are straight forward:
import java.lang.annotation.* /** Marker annotation indicating an action (or entire controller) * requires a document. */ @Target([ElementType.FIELD, ElementType.TYPE]) @Retention(RetentionPolicy.RUNTIME) @Documented @interface WithDocument {}
and
import java.lang.annotation.* /** Marker annotation indicating an action (or entire controller) * requires a document that is in the logged in user's court. */ @Target([ElementType.FIELD, ElementType.TYPE]) @Retention(RetentionPolicy.RUNTIME) @Documented @interface WithDocumentInUsersCourt {}
The Filter
The security checks are carried out by a web filter that check each action. When the annotation is present, access is only allowed if the annotation’s rule passes. When the rule test fails the user is shown an /error/notAuthorized
page. (A pleasant side effect is the consistency of the message back to the user.)
/** Restricts access to (some) actions based on the state of the domain data. */ class DomainSecurityFilters { def springSecurityService def filters = { domainSecurity(controller:"*", action:"*") { before = { def user = springSecurityService?.isLoggedIn() ? User.findById(springSecurityService.principal.id) : null // Loop through a list of annocation classes and check each one in turn... for ( annotation in ControllerAnnotationHelper.ANNOTATION_RULE_MAP.keySet() ) { if (ControllerAnnotationHelper.requiresAnnotation(annotation, controllerName, actionName)) { request.document = request.document ?: Document.get(parseLong(params.id)) def rule = ControllerAnnotationHelper.ANNOTATION_RULE_MAP[annotation] if (!rule(request.document, user)) { log.warn "${controllerName}/${actionName} FAILED ${annotation.simpleName} with document id:'${params.id}' and user:${user}." render view:"/error/notAuthorized" return false } } } // If we got this far everything is a-okay! return true } } } /** Wrapper around Long.parseLong that doesn't throw NumberFormatException. */ private parseLong(hopeItsALong) { try { return Long.parseLong(hopeItsALong) } catch (NumberFormatException ex) { return null } } }
The Meta-programming
The trick that makes the filter work is ensuring that we know which actions are annotated with what. And that’s accomplished by the ControllerAnnotationHelper
. It does two significant things:
- At bootstrap init time, it creates a map of the controllers and actions that are annotated.
- It provides a place for the filter to look up the rules that apply to each annotation.
Heres’ the helper code:
import org.apache.commons.lang.WordUtils import org.apache.log4j.Logger import org.codehaus.groovy.grails.commons.ApplicationHolder /** * Must call the init method from BootStrap.init(). * * Based on Burt Beckwith's code from http://burtbeckwith.com/blog/?p=80 . */ class ControllerAnnotationHelper { static def log = Logger.getLogger(ControllerAnnotationHelper.class) private static Map<String, Map<String, List<Class>>> _actionMap = [:] private static Map<String, Class> _controllerAnnotationMap = [:] /* * A map of annotation/closure pairs. Note that the closure should * return true if the condition of the annotation _is_ satisfied. */ static ANNOTATION_RULE_MAP = [ (WithDocument): { document, user -> document?.isFor(user) }, (WithDocumentInUsersCourt): { document, user -> document?.inUsersCourt(user) }, ] /** Find controller annotation information. Must be called by BootStrap.init(). */ static void init() { log.debug "init()..." ApplicationHolder.application.controllerClasses.each { controllerClass -> def ctrlClass = controllerClass.clazz String controllerName = WordUtils.uncapitalize(controllerClass.name) for (annotationClass in ANNOTATION_RULE_MAP.keySet()) { mapClassAnnotation(ctrlClass, annotationClass, controllerName) } Map<String, List<Class>> annotatedClosures = findAnnotatedClosures(ctrlClass) if (annotatedClosures) { _actionMap[controllerName] = annotatedClosures } } } private static void mapClassAnnotation(clazz, annotationClass, controllerName) { if (clazz.isAnnotationPresent(annotationClass)) { def list = _controllerAnnotationMap[controllerName] ?: [] list << annotationClass _controllerAnnotationMap[controllerName] = list } } private static Map<String, List<Class>> findAnnotatedClosures(Class clazz) { // since action closures are defined as "def foo = ..." they're fields, but they end up private def map = [:] for (field in clazz.declaredFields) { def fieldAnnotations = [] for (annotationClass in ANNOTATION_RULE_MAP.keySet()) { if (field.isAnnotationPresent(annotationClass)) { fieldAnnotations << annotationClass } } if (fieldAnnotations) { map[field.name] = fieldAnnotations } } return map } /** * Check if the specified controller action includes an annotation class. * * @param annotationClass the annotation class * @param controllerName the controller name * @param actionName the action name (closure name) */ static boolean requiresAnnotation(Class annotationClass, String controllerName, String actionName) { // see if the controller has the annotation def annotations = _controllerAnnotationMap[controllerName] if (annotations && annotations.contains(annotationClass)) { return true } else { // otherwise check the action Map<String, List<Class>> controllerClosureAnnotations = _actionMap[controllerName] ?: [:] List<Class> annotationClasses = controllerClosureAnnotations[actionName] return annotationClasses && annotationClasses.contains(annotationClass) } } }
And…
And that all there is too it. Of course there are other ways to do this (configure Spring Security voters for one) but this annotation+filter technique is very simple.
If you do this on your project I bet you’ll end up with a little more sophistication in the helper and filter to deal with multiple types of domain objects; and it’s likely that you’ll need a taglib that accesses the same set of annotation rules.
Have fun.
References
Sample application: fine-grained.tar.gz
Annotations in Grails Controllers, http://burtbeckwith.com/blog/?p=80
Spring Security Core: http://burtbeckwith.github.com/grails-spring-security-core/
Joey & Steve: two names for the same cat.
One thought on “Fine-Grained Security Simplified”