07 April 2008

Grails Acegi ACL Howto

Acegi ACLs are a daunting subject. However I have recently integrated Acegi ACL into the Grails Acegi Plugin, and with that made it much easier to implement fine-grained ACLs in your Grails projects.

Prerequisites
Please refer to the Resources section at the end of this article for downloads

There are primarily two use-cases for the current Acegi Plugin's ACL extensions:

  1. Protecting Method Invocations
  2. Protecting Domain object instances

1. Protecting Method Invocations

You can protect method invocations by requiring logged in user to be in certain roles(ROLE_xxx). This check ignores all method parameters, and will throw an exception if the current user is not in the required Role.
e.g. method signature:
   1:public ReportLog getReportLog(Integer reportId){
2: return Report.get(reportId).log
3:}
2. Protecting domain object instances

You can protect domain object instances (tuples in database) using a variety of fine-grained access controls. Currently Acegi supports three (3) distinct methods out of the box, two (2) of which are currently directly supported by the Grails plugin.

2.1. Applying ACLs to method parameters (CURRENTLY UNSUPPORTED!)

This ACL checks the first parameter of a method for a matching domain class to see if current user has ACL on that object instance (ACL_ENTRY).
e.g. method signature:
   1:public void saveReport(Report report){
2: report.save()
3:}
will result in a check being done on the Report parameter to saveReport()
to see if the current user has an ACL granting permission for the domain object
instance in the parameter.


2.2 Filtering collections using ACLs

You can filter the domain objects in a collection returned from a method to see if the user has appropriate ACLs on each object instance in the collection. Items in collection to which the current user does not have access to are automatically removed from the list. (AFTER_ACL_COLLECTION_READ)
e.g. method signature:
   1:public List getAllReports(){
2: return Report.list()
3:}
4:
2.2. Applying ACLs to method return value

You can check the domain object returned from a method invocation to see if the current user as an appropriate ACL granting permission to that domain object. (AFTER_ACL_READ)
e.g. method signature:
   1:public Report getReport(Integer id){
2: return Report.get(id)
3:}
4:

Protecting a Service Class
Let us now look at a service class which has ACL protection turned on:
   1:class ReportService{
2:
3: static acegiACL = {
4: Report{
5: getReportLog(['ROLE_USER', 'ROLE_ADMIN'])
6: getAllReports(['ROLE_USER', 'AFTER_ACL_COLLECTION_READ'])
7: getReport(['ROLE_USER', 'AFTER_ACL_READ'])
8: saveReport(['ACL_REPORT_WRITE']) //CURRENTLY UNSUPPORTED!
9: }
10: }
11:
12: boolean transactional = true
13:
14: def getReport(Integer reportId) {
15: return Report.get( reportId )
16: }
17:
18: def getAllReports(){
19: return Report.list()
20: }
21:
22: def getReportLog(Integer reportId){
23: return Report.get(reportId).log
24: }
25:
26: def saveReport(Report report){
27: report.save()
28: }
29:
30:}
Note that there is a static closure defined called 'acegiACL'. This naming is important, and each service class you wish to protect must have this closure definition.
NOTE: The 'acegiACL' closure is not currently supported on domain objects.

The 'acegiACL' closure can be explained as follows:
  • line 3 - "Report {... "
    • refers to the domain object we are protecting. This name is used when applying ACL controls for AFTER_ACL_READ and AFTER_ACL_COLLECTION_READ.
  • line 5 - getReportLog(['ROLE_USER', 'ROLE_ADMIN'])
    • Only allow users in the ROLE_USER and ROLE_ADMIN roles to call the getReportLog() method
  • line 6 - getAllReports(['ROLE_USER', 'AFTER_ACL_COLLECTION_READ'])
    • Only allow users in the ROLE_USER role to call the getAllReports() method.
    • Additionally filter all returned results checking that user has READ permission on the domain objects.
  • line 7 - getReport(['ROLE_USER', 'AFTER_ACL_READ'])
    • Only allow users in the ROLE_USER role to call this method.
    • Additionally check the returned domain instance to see if the current user has the READ permission on the object.
  • line 8 - saveReport(['ACL_REPORT_WRITE']) - (UNSUPPORTED!)
    • Check the custom permission to verify that the current user has WRITE permission on domain object instance passed into the method.
Future improvements
  • Allow fine-grained ACLs of the type ACL_REPORT_SAVE, by automatically creating instances of AclEntryVoter
  • Create a mechanism allowing users to easily hook up their own custom instances of AbstractAclVoter into the ACL mechanism
Acknowledgments: Thanks to Tsuyoshi Yamamoto San, and Haotian Sun for their excellent initial work on the Grails Acegi Plugin

Resources:
  • Acegi Security - http://www.acegisecurity.org/
  • Download example application demonstrating ACL integration here.
  • Download development version of Grails Acegi Plugin with ACL here.
    Update: [7May2008] - This plugin download is NOT the same as the 0.3 version of the plugin available for download from grails.codehaus.org!

2 comments:

umut said...

Thank you for all your efforts and nice tutorial.

I guess I am missing something though, I am getting the following error:
[17031] errors.GrailsExceptionResolver groovy.lang.MissingPropertyException: No such property: AclObjectIdentity for class: LookupStrategyService
org.codehaus.groovy.runtime.InvokerInvocationException: groovy.lang.MissingPropertyException: No such property: AclObjectIdentity for class: LookupStrategyService

Is there something else I need to do like changing some additional configuration files, etc.? When I check the database created, I do not see any table other than the regular requestmap and roles acegi plugin creates. How does acegi store access control lists?

Kind Regards,
Umut

Stephan February said...

@Umut:

You will have to run the command:

>grails CreateAclDomains

Please refer to the example application download for more details.

Cheers
Stephan