1. Introduction

The Enforcer Plugin, gives you the tools to enforce business rules, and or permissions. Enforcer is light weight, easy to maintain, extend and, use. The plugin works off of an EnforcerService in conjunction with the Enforce, Reinforce, and ReinforceFilter Annotations(AST transforms).

service.enforce({ true }, { throw new EnforcerException("not nice") }, { println "nice" })

For Enforcement the service takes up to 3 closures, a predicate, a failure(defaults to an EnforcerException if not specified) and a success(defaulted to a closure that returns true). The predicate is evaluated, if it returns true, the the success closure is evaluated, else the failure closure is evaluated. With this you can enforce any business rule you need. Reinforce does the same, but is injected at the end of the method before the return statement, rather than the beginning. The ReinforceFilter allows you to filter the returned value of any method.

    @Enforce({ true })
    def closureTrue() {
        println 'nice'
    }

    @Enforce(
        value = { true },
        failure = { throw new EnforcerException("not nice"),
        succes = {true} }
    )
    def closureTrueWithFailureClosure() {
        println 'nice'
    }

The annotations Enforce and Reinforce can be applied to any class or method. When applying the annotation to a class, it will then be applied to all the methods in the class, and be overridden if it is also applied at any of the methods. The ReinforceFilter can be applied to any method that returns a value. The Annotations are AST transforms, so they are applied at compile time, without any third party annotation processor. The annotations make it clear when you are enforcing business rules which gets out of the way of your logic code.

The default implementation installed with the plugin introduces a DomainRole for permissions. This is just a default implementation, which you can use, or replace with your own. Check out the Extending Enforcer section for more info.

The Enforcer plugin was inspired by the limitations, and rigidity of other frameworks, and the thought that a better alternative should exist.

2. Version History

  • 3.0.0

    • No functional changes, just updating dependency versions, and releasing to maven central.

      • Updating to The Latest Grails version

      • Updating to a new version of Gradle

      • Updating how installed service gets config to not use the deprecated navigable map, bug getProperty() instead

      • Adding github action for building

      • Building with Java 11 as min Java version

      • Adding nexus build code for releasing to Maven Central

  • 2.0.1

    • Removing extra import from InstalledEnforcerService template import grails.transaction.Transactional

  • 2.0.0

    • Upgraded plugin for use with Grails 3.3.x, because a change in Groovy broke, compatibility between @CompileStatic and the Enforcer annotations.

    • Added more annotations to give a workaround for dealing with @CompileStatic, and @Transactional.

    • Moved most of the functionality of the Annotations to a common Trait.

    • Made the annotations inject a reference to the Enforcer Service bean, at compile time, so that Spring will wire the reference.

    • Replaced generating a call to get the enforcerService, by getting Holders and looking, up the bean, with a direct call to the service.

    • Moved the service into the plugin, as a base implementation.

    • Updated the install script to install a version of the Enforcer service, and wire it in the resources.groovy.

    • Added a separate install script for generating more extensive tests, meant for debugging enforcer.

    • Added a new test application: testEnforcer33

    • Updated documentation

  • 1.3.4

    • Fixing Issue with arguments list in deployed war

  • 1.3.3

    • Found a conflict with @Transactional changed the Enforcer ast transforms to use the compile phase CANONICALIZATION rather than SEMANTIC_ANALYSIS

  • 1.2.2 and 1.3.2

    • Enhanced IDE compatibility using GDSL, so that methods from the Enforcer service and it’s traits are recognized in the Enforcer AST transform annotations.

      • This means that the service has to be installed to a know location com.security.enforcer.

      • All other installed files have been changed to match the package, but unlike the service can be moved without breaking IDE compatibility.

    • Added @DelegatesTo(EnforcerService) to the closures of the EnforcerService for better IDE support, for if the service is used directly.

    • This will probably be the last update for the Grails 2 version, unless a bug is found. Although I have no idea if anyone is actually using the plugin. If you are are using the plugin for either version, hit me up @virtualdogbert, on Twitter or the Grails Slack Channel.

  • 1.2.1 and 1.3.1

  • 0.2.1 and 0.3.1

    • Initial Release including the Enforce annotation that can only be applied to methods. Documentation is gdoc.

3. Getting Started

  1. Have the Spring Security Core plug-in installed.

  2. Make sure you’ve run the quick start(e.g. grails s2-quickstart com.security User Role) note the package name.

  3. Add the Enforcer dependency:

In Grails 5.3.2+ add the following dependency to your gradle.build:

implementation "io.github.virtualdogbert:enforcer:3.0.0"

In legacy versions of Grails 3.3.x+ add the following dependency to your gradle.build:

compile "org.grails.plugins:enforcer:2.0.1"

4.Run grails enforcer-quickstart <name of the package you installed spring security core under>

This is for imports, all files will be installed to the com.security.enforcer package. Any of the files can be moved, except the InstalledEnforcerService. Moving the InstalledEnforcerService will break the IDE integration.

5.Check the Usage section and start using Enforcer.

3.1. Example applications

4. Usage

4.1. Enforcer 2.0.0

In earlier versions of Grails, there was a change to the Groovy version that broke Enforcer’s compatibility with @CompileStatic, and by extension @Transactional, because @CompileStatic now statically checks the parameters used in annotations. This lead to compilation errors. To work around this @Enforce create a new method in the place of the original, and copy the original method to a new method, making a wrapper proxying the original method. With The wrapper @Enforcer applies the Transforms from @CompileStatic and @Transactional to the original copy, Which won’t have the enforcer annotation, and won’t throw a compilation error.

In addition to the annotations Enforce, Reinforce, and ReinforceFilter, there are now variations that add transactionality, static compilation, or both. the vacations are denoted with S, for compile static, and T for Transactional. The complete list being:

  • @Enforce // Original

    • @EnforceS //Statically compiles the original method.

    • @EnforceT //Add Transactionality to the original

    • @EnforceTS //Statically compiles, and adds Transactionality to the original method.

  • @Reinforce // Original

    • @ReinforceS //Statically compiles the original method.

    • @ReinforceT //Add Transactionality to the original

    • @ReinforceTS //Statically compiles, and adds Transactionality to the original method.

  • @ReinforceFilter // Original

    • @ReinforceFilterS //Statically compiles the original method.

    • @ReinforceFilterT //Add Transactionality to the original

    • @ReinforceFilterTS //Statically compiles, and adds Transactionality to the original method.

In addition to those annotations two more annotations were added that are the same as @Transactional and @CompileStatic, but they won’t interfere with the enforcer annotations:

  • @EnforcerCompileStatic

  • @EnforcerTransactional

4.2. Code Examples

Checks the domain role for the role owner on the Sprocket domain instance for a user:

def enforcerService
enforcerService.enforce({ hasDomainRole('owner', sprocket, testUser) })

Checks to see if the test user has the Role ROLE_USER:

def enforcerService
enforcerService.enforce({ hasRole('ROLE_USER', testUser) })

Those same checks on a method using @Enforce:

@Enforce({hasRole('ROLE_USER', testUser)  && hasDomainRole('owner', sprocket, testUser)})
def someMethod(){
    //some logic
}

Or the same check using Reinforce:

@Reinforce({hasRole('ROLE_USER', testUser)  && hasDomainRole('owner', sprocket, testUser)})
def someMethod(){
    //some logic
}

An example of ReinforceFilter which takes the original value that would be returned, and the result of the closure will take that returned values place, be careful that that returned value of the closure matches the returned value of the method, if you are not using def:

@ReinforceFilter({ Object o -> (o as List).findResults { it % 2 == 0 ? it : null } })
    List<Integer> reinforceFilter() {
        [1, 2, 3, 4, 5, 6, 7, 8, 9]
    }

For more examples check out the unit tests:

And the Integration tests:

5. DomainRole

The DomainRole, is the default for enforcing permissions, This should be seen as a starting point implementation, but can be replace, with your own implementation for permissions(see Extending Enforcer section).

5.1. DomainRole domain

The DomainRole is the default domain class for holding permission.

class DomainRole {
    String role        //The role to apply to the object
    Long   domainId    //The id of the object
    String domainName  //The domain name of the object
    User   user        //The user associated with the permission
    Date   dateCreated
    Date   lastUpdated

It is meant to be simple, linking a role to a domain object(id, and className), and a user

5.2. DomainRole trait

This trait, which is applied to the enforcerService by the install, gives you the following methods:

    /**
     *  This method will check if a user(defaulting to the currently logged in user),
     *  has a DomainRole on an object.
     *
     *  @param  role the role to check to see if the user has on the domainObject
     *  @param domainObject the instance of the domain object to check if the user
     *  has a DomainRole on
     *  @param user the user to check if  it has the role on the domain object,
     *  defaults to null which is swapped for springSecurityService.currentUser
     *  @return true if the user has the DomainRole or the DomainRole fall in to
     *  the following hierarchy and false other wise:
     *  Map roleHierarchy = [
     *            owner : ['owner', 'editor', 'viewer'],
     *            editor: ['editor', 'viewer'],
     *            viewer: ['viewer']
     *  ]
     */
    boolean hasDomainRole(String role, domainObject, User user = null)
    /**
     * This method checks the domain object to see if it has a reference to a
     * user(passed in or defaulted to springSecurityService.currentUser)
     * This makes it so that the original creator of an object can add permissions
     * to that object.
     *
     * @param domainObject The domain object to check for a user reference
     * domainObject.creator
     * @param user  the user(defaulted to springSecurityService.currentUser) to
     * compare to domainObject.creator
     * @return true if the user is the same as the creator user reference, false
     * otherwise
     */
    Boolean isCreator(domainObject, user = null)
    /**
     * This method changes the DomainRole of a domainObject for a user, and
     * creates one if one doesn't exist.
     *
     * @param role the role to set for the domainObject
     * @param domainObject the domain object to set a role for
     * @param user the user to set the DomainRole for defaulting to
     * springSecurityService.currentUser
     */
    void changeDomainRole(String role, domainObject, User user = null)
    /**
     * This method removes a DomainRole from an  domainObject
     *
     * @param domainObject the domainObject to remove the role from
     * @param user the use for which the role is being removed.
     */
    void removeDomainRole(domainObject, User user = null)

6. Extending Enforcer

After you run the Enforcer quick start script, in your services you will have the InstalledEnforcerService, the RoleTrait and the DomainRole Trait. In the InstalledEnforcerService, you can add your own traits to extent the DSL like functionality of the Enforcer annotations. This will help keep your Enforcer checks short and readable.

For example if you add a new CreatorTrait to the InstalledEnforcerService like this:

class InstalledEnforcerService extends EnforcerService implements RoleTrait,DomainRoleTrait, CreatorTrait{

With the trait being:

trait CreatorTrait {
    def springSecurityService

    Boolean isCreator(def domainObject, User user = null) {
        if (!user) {
            return domainObject.creator == springSecurityService.currentUser
        }

        domainObject.creator.id == user.id
    }

}

Then you could do enforcer checks like:

@EnforceT({isCreator(sprocket)})
Sprocket update(Sprocket sprocket, String material){
 //update the sprocket
}

7. Testing

The quick start will install the unit tests EnforcerServiceSpec.groovy, EnforcerAnnotationSpec.groovy and, ReinforceAnnotationSpec.groovy, which will allow you to test the Enforcer service and the Enforce, Reinforce, and ReinforceFilter annotations(AST Transforms):

By default the test for a DomainRole is commented out, you will have to comment it back in and replace the Sprocket domain, with one from your own application. You will also have to add that domain class to the mock section. Here are some example implementations of the enforcer unit test:

Also check out these tests from the test app:

8. Enforcer vs Spring Security ACL and others?

So why would you want to use Enforcer?

With Enforcer I try to set you up to be successful, in enforcing business rules/permissions, in a way that is easy to read, and is expendable, without the limitations I see in other frameworks. So lets take a look at alternatives, and how Enforcer addresses some of there limitations.

8.1. Spring Security ACL

  1. It’s annotations use the Spring EL language, which is a DSL for Spring, which is less powerful, and less flexible than Groovy.

  2. It uses a "bit mask" for storing permission, while this is "efficient", but hard to read, also we’re not in the 80’s anymore, and disk is cheap.

  3. It uses a highly normalized set for domains/tables for storing permission. This means that querying the db will involve many joins, which will slow down the writing and running of queries.

  4. Updating permission especially in bulk can be really slow, because of the built-in caching. If you use direct SQL you have to invalidate the plug-ins internal cache.

  5. The annotation is a Spring Proxy, so it only gets called if you are calling from outside the class, the annotation is in. This can lead to a false sense of security, or false negatives.

  6. If you are not using a hierarchy for your permission, which I don’t see a config option for, you will end up with a lot of permissions in the db. I’ve seen it as bad as 400,000 permission entries for just over a 1000 containers.

  7. Extending the plug-in is not straight forward. Adding new permissions isn’t bad, but you have to deal with the "bit mask". However if you want to add functions that can be called in the EL expression…​ good luck. Adding new functions that you can call would involve some been overrides, and extending classes. I’ve tried it twice and it didn’t work either time. I do think however you maybe able to call services from within the EL, with the current version, but don’t quote me on that.

  8. It’s somewhat hard to setup tests for your annotations.

8.2. Shiro

  1. I’ll be honest I don’t really get Shiro, and I haven’t taken the time to understand it. The permissions aren’t as readable as I would like.

  2. I know the library of Shiro, not the plugin does have annotations, but I suspect that they would have the same issue that the spring security has, being proxies.

8.3. Drools

  1. I never used it, but I haven’t heard good things, and from what I can see the plugins are not being actively maintained.

8.4. GContracts

  1. It seems the last commit was 4 years ago, and it wasn’t meant to work with Grails:

8.5. Enforcer in response:

  1. Use a closure, so you have the full flexibility of groovy, your IDE, will help you, and will do syntax highlighting.

  2. The default DomainRole, uses a string which is easy to read and query.

  3. The DomainRole is denormalized, so that it will be quicker, and involves less queries to read, and write.

  4. As said before DomainRole is denormalized so it’s quick to update.

  5. The Enforce AST transform/annotation runs every time a method it’s annotation is called, unless your are running from the test environment.

  6. DomainRole uses a hierarchy by default so you’ll have less entries in the db, which will be easier to query, and migrate if you have too. That 400,000 for 1000 contains was reduced to 20,000 entries using a hierarchy.

  7. Extending Enforcer is easy, by adding methods to either of the traits the InstalledEnforcerService uses, or adding your own trait, to just calling any service from with in the closure(s) you pass in.

  8. You could even use other frameworks, and integrate them in, using traits, based of the InstalledEnforcerService.

  9. Testing is easy see the EnforcerServiceSpec for examples.

For the rest I don’t see them as viable competition at this point, mainly, because most of them aren’t being maintained. All of these tools had their usage, but I think it’s time for something better, and in the future maybe a better plugin will be written, than Enforcer.

9. The Enforcer AST Transforms

9.1. How the Enforce AST Transform works

The Enforcer plugin has an EnforcerTrait, where all the work is done. This trait is used in all the variations of the Enforce, Reinforce, and ReinforceFilter transforms. For example:

@EnforceTS({ isCreator(sprocket) })
Sprocket updateSprocketCompileStatic(Sprocket sprocket) {
    sprocket.material = 'plastic'
    (Sprocket)sprocket.save()
}

Becomes:

 com.security.enforcer.EnforcerService enforcerService

@com.virtualdogbert.ast.EnforceTS(value = {
    this.isCreator(sprocket)
}, extensions = ['org.grails.compiler.ValidateableTypeCheckingExtension', 'org.grails.compiler.NamedQueryTypeCheckingExtension', 'org.grails.compiler.HttpServletRequestTypeCheckingExtension', 'org.grails.compiler.WhereQueryTypeCheckingExtension', 'org.grails.compiler.DynamicFinderTypeCheckingExtension', 'org.grails.compiler.DomainMappingTypeCheckingExtension', 'org.grails.compiler.RelationshipManagementMethodTypeCheckingExtension'])
public com.security.Sprocket updateSprocketCompileStatic(com.security.Sprocket sprocket) {
    return ((this.$Enforcer_wrapped_method_updateSprocketCompileStatic(sprocket)) as com.security.Sprocket)
}

@grails.gorm.transactions.Transactional
@groovy.transform.CompileStatic
protected com.security.Sprocket $Enforcer_wrapped_method_updateSprocketCompileStatic(com.security.Sprocket sprocket) {
    org.grails.datastore.mapping.transactions.CustomizableRollbackTransactionAttribute $transactionAttribute = new org.grails.datastore.mapping.transactions.CustomizableRollbackTransactionAttribute()
    grails.gorm.transactions.GrailsTransactionTemplate $transactionTemplate = new grails.gorm.transactions.GrailsTransactionTemplate( this .transactionManager, $transactionAttribute)
    return (($transactionTemplate.execute({ org.springframework.transaction.TransactionStatus transactionStatus ->
        this.$tt__$Enforcer_wrapped_method_updateSprocketCompileStatic(sprocket, transactionStatus)})) as com.security.Sprocket)
}

protected com.security.Sprocket $tt__$Enforcer_wrapped_method_updateSprocketCompileStatic(com.security.Sprocket sprocket, org.springframework.transaction.TransactionStatus transactionStatus) {
    enforcerService.enforce({
        this.isCreator(sprocket)
    })
    sprocket .material = 'plastic'
    ((sprocket.save()) as com.security.Sprocket)
}
  • The first part, is the reference to the EnforcerService.

  • The second part, is the wrapping of original method, and the application of @CompileStatic, and @Transactional.

  • The third part is the original method, with the call to the enforcerService injected.