Prehistory
We develop a small portal on Grails and use Spring Security to manage security. The spring-security plugin for Grails is quite convenient and until the last moment it did not require sophisticated functionality.
An embarrassing moment has recently been discovered in using @Secured annotations for Grails controller methods. The problem is that annotations are processed at runtime and are converted into rules for addresses “Address -> Required Roles”. Such an approach causes a number of problems in Grails controllers in the data saving / deleting actions, since they send data to the controller's main URL, you have to first annotate the controller, and secondly, it is impossible to set various restrictions for such requests.
It will be about how to solve the problem and get a good tool for managing safety rules.
Possible solutions
I don’t understand why the developers of the Grails plugin acted so carelessly about users, it’s probably just easier for them.
')
Alternative solutions:
- AOP for controllers (difficult to configure a large number of rules)
- Bytecode generation from annotations at compile time
The second approach is unpleasant to implement in Java, since the assembly steps need to be organized. But not in Groovy. In Groovy, Meta programming or AST (Abastract Syntax Tree) Transformations are used for such purposes.
We will not consider meta-programming for filtering requests to controllers, since this is more like a hack than a stable solution.
Transformations
Transformations are widely used in Grails, for example, to add id and version fields to model classes. We will use them to filter calls to controller methods.
Transformation is a simple Java class that implements the ASTTransformation interface and annotated @GroovyASTTransformation, which contains only one visit method - and in fact is a typical representative of the Visitor pattern. For transformation, you can set the compilation phase at which it is applied. And to select the nodes that come to the visitor, an annotation class annotated by GroovyASTTransformationClass is required. As a result, the transformation changes the syntax tree, can add / change / delete nodes, affecting the resulting byte code.
Total we need some annotation and transformation class. For simplicity, let's call them @SuperSecured and SuperSecuredTransformation.
The annotation contains in the value an array of strings — the necessary roles for accessing the method.
@Retention (RetentionPolicy.SOURCE) indicates that the summary will not be present in the resulting bytecode.
package com.example; import org.codehaus.groovy.transform.GroovyASTTransformationClass; import java.lang.annotation.*; @Target({ElementType.METHOD}) @Retention(RetentionPolicy.SOURCE) @GroovyASTTransformationClass("com.example.SuperSecuredTransformation") public @interface SuperSecured { String[] value() default {}; }
Transformation:
package com.example; import org.codehaus.groovy.ast.*; import org.codehaus.groovy.ast.expr.*; import org.codehaus.groovy.ast.stmt.*; import org.codehaus.groovy.control.*; import org.codehaus.groovy.transform.*; import java.util.List; @GroovyASTTransformation(phase = CompilePhase.SEMANTIC_ANALYSIS) public class SuperSecuredTransformation implements ASTTransformation { @Override public void visit(ASTNode[] astNodes, SourceUnit sourceUnit) { if (astNodes != null) { for (ASTNode node : astNodes) { if (node instanceof MethodNode) { MethodNode methodNode = (MethodNode) node; List<AnnotationNode> annotations = methodNode.getAnnotations(new ClassNode(SuperSecured.class)); if (annotations != null && !annotations.isEmpty()) { injectRolesCheck(methodNode, annotations); } } } } } private void injectRolesCheck(MethodNode method, List<AnnotationNode> annotations) { for (AnnotationNode annotationNode : annotations) { BlockStatement code = (BlockStatement) method.getCode(); Expression rolesValue = annotationNode.getMember("value"); Expression checkRolesExpression = new StaticMethodCallExpression( new ClassNode(SuperSecuredInspector.class), "rejectByRoles", new ArgumentListExpression( rolesValue ) ); code.getStatements().add(0, new ExpressionStatement(checkRolesExpression)); } } }
The following happened: the transformation takes the syntax tree nodes annotated as @SuperSecured and, if it is a method, adds to the beginning a call to the static method SuperSecuredInspector.rejectByRoles with a list of roles in the annotation value. This method throws an AccessDeniedException exception if the current user does not satisfy the security conditions.
To use such annotations in the end is a pleasure.
Conclusion
This approach allows you to select complex rules of access to objects and not duplicate them in the code. Annotations are quite expressive, and code generation is statically typed, which allows to avoid errors during execution.
Transformations are a worthy alternative to Groovy AOP.
Links
PS Habra-developers, please fix the display of the @ sign in the source tag.