Using MyBatis Annotations with Spring 3.0 and Maven
April 5th, 2011
Examples on how to use some of the MyBatis annotations and Spring.
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
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…
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.
Each of the controller’s actions should be secured like this:
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 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 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 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:
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 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.
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.
Examples on how to use some of the MyBatis annotations and Spring.
Object Partners is proud to be the premier sponsor of this years GR8 in the US Conference. The conference, dedicated to Groovy, Grails, Griffon and other GR8 technologies is now open for registration at <a href=http://gr8conf.us>gr8conf.us</a>.
Recently developers of the continuous integration tool, Hudson, have created a new project, Jenkins.
Insert bio here