Burt Beckwith's complete blog can be found at: http://burtbeckwith.com/blog/

Items:   1 to 5 of 24   Next »

Saturday, July 17, 2010

I released a new Grails plugin today, app-info. This plugin exposes a lot of internal information for a Grails application. Most of the code for this plugin dates back a long time and was originally mostly JSPs that I would copy into whatever application I was working on to give myself a view into what's going on. Over time as I started using Spring and Hibernate I added more pages to display that information, and it now also shows information about Grails too.

Click any of the images to see the full-size version in a new window.

Configuration

This plugin uses the Dynamic Controller plugin to modularize the controller actions as mixins. There are several of them and if there are some you don't want or need, you can exclude them. None are configured by default - that has to be done in your application's grails-app/conf/Config.groovy file. Here's the configuration from the sample application (download link at the end):

grails.plugins.dynamicController.mixins = [
   'com.burtbeckwith.grails.plugins.appinfo.IndexControllerMixin':       'com.burtbeckwith.appinfo_test.AdminManageController',
   'com.burtbeckwith.grails.plugins.appinfo.HibernateControllerMixin':   'com.burtbeckwith.appinfo_test.AdminManageController',
   'com.burtbeckwith.grails.plugins.appinfo.Log4jControllerMixin' :      'com.burtbeckwith.appinfo_test.AdminManageController',
   'com.burtbeckwith.grails.plugins.appinfo.SpringControllerMixin' :     'com.burtbeckwith.appinfo_test.AdminManageController',
   'com.burtbeckwith.grails.plugins.appinfo.MemoryControllerMixin' :     'com.burtbeckwith.appinfo_test.AdminManageController',
   'com.burtbeckwith.grails.plugins.appinfo.PropertiesControllerMixin' : 'com.burtbeckwith.appinfo_test.AdminManageController',
   'com.burtbeckwith.grails.plugins.appinfo.ScopesControllerMixin' :     'com.burtbeckwith.appinfo_test.AdminManageController'
]

With this configuration all of the URLs will start with http://localhost:8080/appname/adminManage. My preference is to re-map these in grails-app/conf/UrlMappings.groovy to http://localhost:8080/appname/admin/manage since I want the administration parts of the application under /admin/**. This is optional but if you want to do this see the configuration in the sample app.

Note: This plugin exposes a lot of information about your application so you're strongly recommended to use a security plugin and guard these URLs. The sample application uses the Spring Security Core plugin but you're free to restrict access however you like, and optionally not even include this in your production war.

Usage


The Attributes menu has three entries:

Application

Displays all application-scope attributes from the ServletContext

Request

Displays all request-scope attributes from the current HttpServletRequest. Since this is for the current request, it's mostly useful to see what's available in a typical request.

Session

Displays all session-scope attributes from the current HttpSession. Since this is for the current request, it's mostly useful to see what's available in a typical session.


The Properties menu has three entries:

Data Source

A read/write view of the DataSource bean. Depending on the DataSource implementation, changing an attribute will take effect immediately and reset the connection pool

Grails Properties

Read-only view of the Grails Configuration.

System Properties

Read/write view of system properties. You can alter current properties or add new ones.


The Info menu has five entries:

Controllers

All controllers, plus links to all actions

Logging

Has comboboxes for all loggers to change the log level at runtime. Also has a text field to register a new Logger and its level.

Also includes a reverse-engineered log4j.xml based on the in-memory Log4j configuration. This is an estimate, so it may not be 100% accurate. But if you're having logging configuration issues and are familiar with the log4j.xml format, this can be convenient for diagnosing how things are misconfigured.

Memory

Graphs describing memory usage. Also has an action to trigger explicit garbage collection.

Sessions

Displays all current sessions and session-scope variables with a link to invalidate the session. This is enabled by default; to disable set grails.plugins.appinfo.useContextListener = false in grails-app/conf/Config.groovy

Spring Beans

Spring bean information for all beans in the "main" context and the parent context


The Hibernate menu has five entries and several sub-actions

Overview

  • Properties
  • Mappings Info
  • Imports
  • Auxiliary Database Objects
  • Named Queries
  • Named SQL Queries
  • TypeDefs
  • Filter Definitions

Entity Graphs

ER-style graphs of all Hibernate classes with relationships.

Table Graphs

ER-style graphs of all tables with relationships.

Caching

2nd-level cache information for StandardQueryCache, UpdateTimestampsCache, and all domain class caches.

Also has links to clear the cache and display usage graphs

Statistics

  • General Hibernate statistics
  • Links for statistics for each domain class
  • Links for statistics for each collection
  • Links for statistics for cached queries

Combos

On each page there are three combo boxes. The Tables combo box lists all database tables and selecting one displays detailed information for that table

The Entities combo box lists all entities (domain classes, JPA-annotated Java classes, and hbm.xml-mapped classes) and selecting one displays detailed information for each one

The hbm.xml combo box lists all entities and selecting one displays the reverse-engineered hbm.xml that would have created the equivalent entity. This is useful if you're having GORM mapping issues and want to see what the Hibernate configuration is

Sample Project

You can download a preconfigured sample application here

The /admin/** URLs in the sample app are restricted to users with ROLE_ADMIN. There's one user configured in BootStrap.groovy with username 'admin' and password 'password' with ROLE_ADMIN, so you can use that to authenticate.


Wednesday, February 3, 2010

I saw a plaintive wail on Twitter about using GMail as the smtp server to send error emails using a Log4j SMTPAppender in Grails. It turned out to be a little tricky (and a bigger solution than 140 characters would allow) so I thought I'd describe the process here.

Most of the properties are configurable as appender attributes (e.g. server name, auth username, etc.) but two important ones aren't. SMTPAppender creates a Properties instance with System.getProperties() as the default values and adds smtp properties to that. But you need to specify the smtp port (it will default to 25 otherwise) and you need to tell it to send a STARTTLS command. Both are configurable via system properties:

System.setProperty 'mail.smtp.port', '587'
System.setProperty 'mail.smtp.starttls.enable', 'true'

and if you add those calls to Config.groovy before the appender is instantiated then it will have the values available when it configures its JavaMail Session:

import org.apache.log4j.Level
import org.apache.log4j.net.SMTPAppender

...

mail.error.server = 'smtp.gmail.com'
mail.error.port = 587
mail.error.username = 'your.email@gmail.com'
mail.error.password = 'yourpassword'
mail.error.to = 'to@yourapp.com'
mail.error.from = 'from@yourapp.com'
mail.error.subject = '[Application Error]'
mail.error.starttls = true
mail.error.debug = false

environments {
   production {
      grails.serverURL = "http://www.changeme.com"
   }
   development {
      grails.serverURL = "http://localhost:8080/${appName}"
   }
   test {
      grails.serverURL = "http://localhost:8080/${appName}"
   }
}

log4j = {

   System.setProperty 'mail.smtp.port', mail.error.port.toString()
   System.setProperty 'mail.smtp.starttls.enable', mail.error.starttls.toString()

   appenders {

      appender new SMTPAppender(name: 'smtp', to: mail.error.to, from: mail.error.from,
         subject: mail.error.subject, threshold: Level.ERROR,
         SMTPHost: mail.error.server, SMTPUsername: mail.error.username,
         SMTPDebug: mail.error.debug.toString(), SMTPPassword: mail.error.password,
         layout: pattern(conversionPattern:
            '%d{[ dd.MM.yyyy HH:mm:ss.SSS]} [%t] %n%-5p %n%c %n%C %n %x %n %m%n'))
   }

   error  'org.codehaus.groovy.grails.web.servlet'//  controllers
          'org.codehaus.groovy.grails.web.pages', //  GSP
          'org.codehaus.groovy.grails.web.sitemesh', //  layouts
          'org.codehaus.groovy.grails.web.mapping.filter', // URL mapping
          'org.codehaus.groovy.grails.web.mapping', // URL mapping
          'org.codehaus.groovy.grails.commons', // core / classloading
          'org.codehaus.groovy.grails.plugins', // plugins
          'org.codehaus.groovy.grails.orm.hibernate', // hibernate integration
          'org.springframework',
          'org.hibernate',
          'net.sf.ehcache.hibernate'
   warn   'org.mortbay.log'

   root {
      error 'stdout', 'smtp'
      additivity = true
   }
}

I've parameterized the properties to make them configurable for each environment or using an external configuration file.

Note that GMail has a limit of 500 emails per day, so if you generate a lot of errors in your app you could hit that limit.


Monday, January 25, 2010

The topic of delaying DataSource and SessionFactory creation until some point after startup has come up a few times on the Grails user mailing list so I thought I'd give it a shot. I got it working, but it's not pretty.

Grails (and Hibernate) will create up to three connections during initialization so the primary focus is to avoid those. In addition the DataSource will pre-instantiate connections, so we'll delay those as well.

The first connection required is for HibernateDialectDetectorFactoryBean, which uses connection metadata to figure out the dialect if it's not specified. This is a cool feature but problematic if you don't want early access to the database, and luckily the fix is simple: specify the dialect class in DataSource.groovy:

dataSource {
   pooled = ...
   driverClassName = ...
   username = ...
   password = ...
   dialect = org.hibernate.dialect.MySQLInnoDBDialect
}

substituting the appropriate class name for your database.

The second connection will be for SpringLobHandlerDetectorFactoryBean, which uses connection metadata to determine if you're using Oracle and if so to use an Oracle-specific LobHandler. The fix here is also simple: override the lobHandlerDetector bean in grails-app/conf/spring/resources.groovy with the correct version for your database. It's a little different if you're using Oracle or another database.

If you're not using Oracle, redefine the bean as

import org.springframework.jdbc.support.lob.DefaultLobHandler

beans = {
   lobHandlerDetector(DefaultLobHandler)
}

and if you are, define it as

import org.springframework.jdbc.support.nativejdbc.CommonsDbcpNativeJdbcExtractor
import org.springframework.jdbc.support.lob.OracleLobHandler

beans = {
   lobHandlerDetector(OracleLobHandler) {
      nativeJdbcExtractor = new CommonsDbcpNativeJdbcExtractor()
   }
}

and omit specifying nativeJdbcExtractor if you're not using pooled connections (e.g. if you're using JNDI).

The third connection is for Hibernate and is used to initialize the Configuration. This one is more work and requires some custom code. It's also somewhat brittle in that it requires a copy/paste of the sessionFactory bean definition from Grails source with some modifications, so it will probably require changes to work with future version of Grails.

Here's the override for Grails 1.2:

import org.codehaus.groovy.grails.orm.hibernate.events.PatchedDefaultFlushEventListener
import com.burtbeckwith.grails.delayds.DelayedSessionFactoryBean
...
sessionFactory(DelayedSessionFactoryBean) {
   def application = AH.application
   def ds = application.config.dataSource
   def hibConfig = application.config.hibernate
   dataSource = ref('dataSource')
   List hibConfigLocations = []
   if (application.classLoader.getResource('hibernate.cfg.xml')) {
      hibConfigLocations <<'classpath:hibernate.cfg.xml'
   }
   def explicitLocations = hibConfig?.config?.location
   if (explicitLocations) {
      if (explicitLocations instanceof Collection) {
         hibConfigLocations.addAll(explicitLocations.collect { it.toString() })
      }
      else {
         hibConfigLocations <<hibConfig.config.location.toString()
      }
   }
   configLocations = hibConfigLocations
   if (ds.configClass) {
      configClass = ds.configClass
   }
   hibernateProperties = ref('hibernateProperties')
   grailsApplication = ref('grailsApplication', true)
   lobHandler = ref('lobHandlerDetector')
   entityInterceptor = ref('entityInterceptor')
   eventListeners = ['flush':       new PatchedDefaultFlushEventListener(),
                     'pre-load':    ref('eventTriggeringInterceptor'),
                     'post-load':   ref('eventTriggeringInterceptor'),
                     'save':        ref('eventTriggeringInterceptor'),
                     'save-update': ref('eventTriggeringInterceptor'),
                     'post-insert': ref('eventTriggeringInterceptor'),
                     'pre-update':  ref('eventTriggeringInterceptor'),
                     'post-update': ref('eventTriggeringInterceptor'),
                     'pre-delete':  ref('eventTriggeringInterceptor'),
                     'post-delete': ref('eventTriggeringInterceptor')]
}

and here's the override for 1.1:

import com.burtbeckwith.grails.delayds.DelayedSessionFactoryBean
...
sessionFactory(DelayedSessionFactoryBean) {
   def application = AH.application
   def ds = application.config.dataSource
   dataSource = ref('dataSource')
   if (application.classLoader.getResource('hibernate.cfg.xml')) {
      configLocation = 'classpath:hibernate.cfg.xml'
   }
   if (ds.configClass) {
      configClass = ds.configClass
   }
   hibernateProperties = ref('hibernateProperties')
   grailsApplication = ref('grailsApplication', true)
   lobHandler = ref('lobHandlerDetector')
   eventListeners = ['pre-load':    ref('eventTriggeringInterceptor'),
                     'post-load':   ref('eventTriggeringInterceptor'),
                     'save':        ref('eventTriggeringInterceptor'),
                     'save-update': ref('eventTriggeringInterceptor'),
                     'post-insert': ref('eventTriggeringInterceptor'),
                     'pre-update':  ref('eventTriggeringInterceptor'),
                     'post-update': ref('eventTriggeringInterceptor'),
                     'pre-delete':  ref('eventTriggeringInterceptor'),
                     'post-delete': ref('eventTriggeringInterceptor')]
}

DelayedSessionFactoryBean extends ConfigurableLocalSessionFactoryBean to create a wrapper for the SessionFactory that lazily creates the real SessionFactory. Add this to src/groovy:

package com.burtbeckwith.grails.delayds

import java.lang.reflect.Field
import java.lang.reflect.InvocationHandler
import java.lang.reflect.Method
import java.lang.reflect.Proxy

import org.codehaus.groovy.grails.orm.hibernate.ConfigurableLocalSessionFactoryBean
import org.hibernate.SessionFactory
import org.springframework.util.ReflectionUtils

class DelayedSessionFactoryBean extends ConfigurableLocalSessionFactoryBean {

   private boolean _initialized
   private SessionFactory _realSessionFactory

   @Override
   void afterPropertiesSet() {
      // do nothing for now, lazy init on first access
   }

   @Override
   SessionFactory getObject() {

      def invoke = { proxy, Method method, Object[] args ->
         initialize()
         return method.invoke(_realSessionFactory, args)
      }

      return Proxy.newProxyInstance(SessionFactory.classLoader,
            [SessionFactory] as Class[], [invoke: invoke] as InvocationHandler)
   }

   private synchronized void initialize() {
      if (_initialized) {
         return
      }

      _realSessionFactory = wrapSessionFactoryIfNecessary(buildSessionFactory())

      Field field = ReflectionUtils.findField(getClass(), 'sessionFactory')
      field.accessible = true
      field.set(this, _realSessionFactory)

      afterSessionFactoryCreation()

      _initialized = true
   }
}

To delay DataSource creation, we'll use Spring's DelegatingDataSource and build the actual DataSource from the values in grails-app/conf/DataSource.groovy the first time getConnection() is called:

package com.burtbeckwith.grails.delayds

import java.sql.Connection
import java.sql.SQLException

import org.apache.commons.dbcp.BasicDataSource
import org.codehaus.groovy.grails.commons.ConfigurationHolder as CH
import org.springframework.jdbc.datasource.DelegatingDataSource

class DelayedDataSource extends DelegatingDataSource {

   private boolean _initialized

   @Override
   Connection getConnection() throws SQLException {
      initialize()
      return super.getConnection()
   }
   
   @Override
   void afterPropertiesSet() {
      // override to not check for targetDataSource since it's lazily created
   }

   private synchronized void initialize() {
      if (_initialized) {
         return
      }

      def config = CH.config.dataSource
      setTargetDataSource(new BasicDataSource(
            driverClassName: config.driverClassName, password: config.password,
            username: config.username, url: config.url))

      _initialized = true
   }
}

This also requires an override in grails-app/conf/spring/resources.groovy:

import com.burtbeckwith.grails.delayds.DelayedDataSource
...
beans = {
   ...
   dataSource(DelayedDataSource)
   ...
}

This creates a BasicDataSource which is what Grails will use by default, but of course feel free to change it to c3p0 or some other provider.


The net effect of using these classes and overridden Spring bean definitions is that both the DataSource and SessionFactory will be lazily initialized on first use. One option might be to have an initialization page that accepts configuration overrides for the database url, username, etc. to allow an admin to start the app and choose the appropriate settings.

The source files shown here are available at DelayedSessionFactoryBean and DelayedDataSource, and grab resources.groovy as a sample to merge into your own resources.groovy.


Sunday, December 27, 2009

It has taken way too long, but the Grails Spring Security plugin finally has ACL support. It's not officially available yet, but people have offered to beta test an early version of the plugin with ACLs, so you can download that here and report any issues back. Once it's stable I'll do an official release.

History

Stephan February did the first work adding ACL support to the plugin. Unfortunately at the time the plugin was based on Acegi 1.0.x and I had just converted it to use Spring Security 2.0. No one did the work to convert the ACL support to the new package layout and approach, so this wasn't used.

This is a frequently requested feature, so I created a feature request as a TODO item for myself. I found some time to work on this over the summer and created an initial GORM-based implementation (the standard Spring Security implementation uses JDBC). I was fortunate to be able to use this at a client project at InnoCentive which helped to flesh out the ideas and identify a few issues.

Around the same time, Phillip Merensky mentioned on the mailing list that he was working on an implementation. He wrote about his approach here and attached his version of the plugin to the JIRA issue. Phillip's work was very helpful; I've merged his version with mine for the current implementation.

Working with ACLs in Spring Security is complex but it will be easier to understand with a sample application.

Test Application

Create a test application

grails create-app acltest
cd acltest

Download the plugin with ACL support here and install it:

grails install-plugin /path/to/grails-acegi-0.5.2-ACL.zip

As with any application using the plugin, you need to run the create-auth-domains script, plus generate-manager if you want the generated GSPs and generate-registration if you want basic registration support:

grails create-auth-domains acltest.User acltest.Role acltest.Requestmap
grails generate-manager
grails generate-registration

The ACL support uses domain classes but to allow customizing the domain classes (e.g. to enable Hibernate 2nd-level caching) there's a script that copies the domain classes into your application:

grails create-acl-domains

The script takes no parameters since the package and names aren't configurable - the plugin code imports the domain classes.

Next, switch from using Requestmap entries in the database to using annotated controllers:

  • delete grails-app/domain/acltest/Requestmap.groovy
  • delete grails-app/controllers/RequestmapController.groovy
  • delete the grails-app/views/requestmap directory and its GSPs
  • delete Requestmap import from grails-app/controllers/RoleController.groovy
  • in grails-app/conf/SecurityConfig.groovy, disable requestmaps (useRequestMapDomainClass = false) and enable annotations (useControllerAnnotations = true), and remove the requestMapClass property:
    security {
       active = true

       loginUserDomainClass = 'acltest.User'
       authorityDomainClass = 'acltest.Role'

       useRequestMapDomainClass = false
       useControllerAnnotations = true
    }

To enable ACL processing, set the useAcl attribute to true:

security {
   active = true

   loginUserDomainClass = 'acltest.User'
   authorityDomainClass = 'acltest.Role'

   useRequestMapDomainClass = false
   useControllerAnnotations = true

   useAcl = true
}

We'll need a domain class to test with, so create a Report domain class:

grails create-domain-class acltest.Report

and add a name property for testing:

package acltest

class Report {
   String name
}

Working with ACLs

Probably the most important interface for ACLs is Permission. You can implement the interface yourself, but BasePermission has READ, WRITE, CREATE, DELETE, and ADMINISTRATION instances that should be sufficient for your needs.

The plugin provides a new service, AclUtilService, to grant and revoke permissions, and to check if permissions are granted. The service methods are:

  • void addPermission(object, recipient, Permission permission) grants the specified permission to the recipient (either the login name or an Authentication) for the specified instance
  • void addPermission(Class< ?> domainClass, long id, recipient, Permission permission) grants the specified permission to the recipient (either the login name or an Authentication) for the specified instance; use this overload to avoid loading the instance
  • void deletePermission(object, recipient, Permission permission) removes the grant of the specified permission from the recipient (either the login name or an Authentication) for the specified instance
  • void deletePermission(Class< ?> domainClass, long id, recipient, Permission permission) removes the grant of the specified permission from the recipient (either the login name or an Authentication) for the specified instance; use this overload to avoid loading the instance
  • boolean hasPermission(Authentication authentication, domainObject, Permission permission) checks if the authentication has a grant of the specified permission for the specified instance
  • boolean hasPermission(Authentication authentication, domainObject, Permission[] permissions) checks if the authentication has a grant of any of the specified permissions for the specified instance; the first one that is found is used, so the order of the array matters

Creating, editing, or deleting permissions requires an authenticated user. The default required role is ROLE_ADMIN for all actions, but this can be configured in SecurityConfig.groovy. Change the acl.authority.changeOwnership property to change who can call OwnershipAcl.setOwner(). Change the acl.authority.modifyAuditingDetails property to change who can call AuditableAcl.updateAuditing(). And change acl.authority.changeAclDetails to change who can call MutableAcl.deleteAce(), MutableAcl.insertAce(), MutableAcl.setEntriesInheriting(), MutableAcl.setParent(), or MutableAcl.updateAce().

You'll probably want to create an admin UI that uses AclUtilService and is aware of your secured domain classes and business rules.

Configuration

Configuring ACL support happens in two places; you configure Voters that have one or more associated permissions and a domain class (which can be an abstract base class), and you configure which service methods use which voters. Often there will be a 1-1 relationship between these but since they're separate, you can re-use the voters for multiple service methods. And you may not even need custom voters; if you only want to secure methods with roles, or if you only need return value checking, then you wouldn't configure any voters, but you'd still configure method restrictions.

There are two types of ACL checks; method return value and method parameter. The plugin creates two voters for return value checks, one for single values (AFTER_ACL_READ) and one for collections (AFTER_ACL_COLLECTION_READ). Each requires that the authenticated user have BasePermission.READ. An optimization would be to allow access to admins (who have been granted BasePermission.ADMINISTRATION); to configure this, redefine the beans in grails-app/conf/spring/resources.groovy:

beans = {
   afterAclCollectionRead(AclEntryAfterInvocationCollectionFilteringProvider,
        ref('aclService'),
        [BasePermission.READ, BasePermission.ADMINISTRATION])

   afterAclRead(AclEntryAfterInvocationProvider,
        ref('aclService'),
        [BasePermission.READ, BasePermission.ADMINISTRATION])
}

Voters for method parameter checks (the first parameter of the specified type or a subclass is checked) can be configured either in SecurityConfig.groovy or in domain class annotations. Putting the configuration in SecurityConfig.groovy keeps everything in one place, whereas the annotations let you put the declarations where they apply, so they're self-documenting. Use whichever approach you prefer.

To configure them in SecurityConfig.groovy, use the acl.voters property, e.g.

import org.springframework.security.acls.domain.BasePermission
import acltest.Report

security {
   ...
   useAcl = true

   acl.voters = [

      aclReportWriteVoter: [
           configAttribute: 'ACL_REPORT_WRITE',
           permissions: [BasePermission.ADMINISTRATION,
                         BasePermission.WRITE],
           domainObjectClass: Report],
   
        aclReportDeleteVoter: [
           configAttribute: 'ACL_REPORT_DELETE',
           permissions: [BasePermission.ADMINISTRATION,
                         BasePermission.DELETE],
           domainObjectClass: Report]
   ]
}

which creates a 'write' voter and a 'delete' voter. The equivalent annotations would be:

package acltest

import org.codehaus.groovy.grails.plugins.springsecurity.acl.AclVoter
import org.codehaus.groovy.grails.plugins.springsecurity.acl.AclVoters

@AclVoters([
   @AclVoter(name='aclReportWriteVoter',
             configAttribute='ACL_REPORT_WRITE',
             permissions=['ADMINISTRATION', 'WRITE']),
   @AclVoter(name='aclReportDeleteVoter',
             configAttribute='ACL_REPORT_DELETE',
             permissions=['ADMINISTRATION', 'DELETE'])
])
class Report {
   String name
}

Note that since you cannot use an annotation more than once, in a case like this where there can be multiple voter annotations for a domain class they need to be defined as attributes of a containing annotation (AclVoters). If you only have a single voter then you can annotate the class with that and omit the containing annotation.

The voter configuration should be fairly clear; there's a name parameter that's used as the Spring bean name (so it must be unique), a configAttribute parameter that's arbitrary but typically uses a naming convention where it starts with 'ACL_', and one or more permissions. The one limitation of annotations over the static configuration is that annotations cannot have Permissions as parameters, so Strings are used instead. This limits you to naming fields of the BasePermission class. If you have custom permission classes you'll need to use the static configuration.

Securing Service Methods

As with voters, there are two ways to define the access rules for service methods. You can define a static springSecurityACL property with configuration options, or annotate the class and/or individual methods.

Let's create a service to test ACLs:

grails create-service acltest.Report

and add some methods that work with Reports:

package acltest

class ReportService {

   boolean transactional = true

   Report getReport(long id) {
      Report.get(id)
   }

   Report createReport(params) {
      Report report = new Report(params)
      report.save()
      report
   }

   List getAllReports(params = [:]) { Report.list(params) }

   String getReportName(long id) { Report.get(id).name }

   Report updateReport(Report report, params) {
      report.properties = params
      if (!report.hasErrors()) {
         report.save()
      }
      report
   }

   void deleteReport(Report report) {
      report.delete()
   }
}

To configure the rules in one place, add a springSecurityACL property:

static springSecurityACL = [
   getReportName: ['ROLE_USER', 'ROLE_ADMIN'],
   getAllReports: ['ROLE_USER', 'AFTER_ACL_COLLECTION_READ'],
   getReport: ['ROLE_USER', 'AFTER_ACL_READ'],
   updateReport: ['ACL_REPORT_WRITE'],
   deleteReport: ['ACL_REPORT_DELETE']
]

and the equivalent annotated version would be:

package acltest

import org.codehaus.groovy.grails.plugins.springsecurity.Secured

class ReportService {

   boolean transactional = true

   @Secured(['ROLE_USER', 'AFTER_ACL_READ'])
   Report getReport(long id) {
      Report.get(id)
   }

   Report createReport(params) {
      Report report = new Report(params)
      report.save()
      report
   }

   @Secured(['ROLE_USER', 'AFTER_ACL_COLLECTION_READ'])
   List getAllReports(params = [:]) { Report.list(params) }

   @Secured(['ROLE_USER', 'ROLE_ADMIN'])
   String getReportName(long id) { Report.get(id).name }

   @Secured(['ACL_REPORT_WRITE'])
   Report updateReport(Report report, params) {
      report.properties = params
      if (!report.hasErrors()) {
         report.save()
      }
      report
   }

   @Secured(['ACL_REPORT_DELETE'])
   void deleteReport(Report report) {
      report.delete()
   }
}

The configuration specifies these rules:

  • getReportName requires that the authenticated user have either ROLE_USER or ROLE_ADMIN (but no ACL rules)
  • getAllReports requires ROLE_USER and will have elements removed from the returned List that the user doesn't have an ACL grant for (thanks to AFTER_ACL_COLLECTION_READ); the user must have one of the permissions defined in the afterAclCollectionRead bean (by default BasePermission.READ) for each element in the list; elements that don't have access granted will be removed
  • getReport requires ROLE_USER and will be denied (thanks to AFTER_ACL_READ) unless the user has one of the permissions defined in the afterAclRead bean (by default BasePermission.READ).
  • updateReport has no role restrictions but must satisfy the requirements of the aclReportWriteVoter voter (which has the ACL_REPORT_WRITE config attribute), i.e. BasePermission.ADMINISTRATION or BasePermission.WRITE
  • deleteReport has no role restrictions but must satisfy the requirements of the aclReportDeleteVoter voter (which has the ACL_REPORT_DELETE config attribute), i.e. BasePermission.ADMINISTRATION or BasePermission.DELETE
  • createReport has no restrictions

To test this out we'll need some users; create those and their grants in BootStrap.groovy:

import org.springframework.security.GrantedAuthority
import org.springframework.security.GrantedAuthorityImpl
import org.springframework.security.acls.domain.BasePermission
import org.springframework.security.context.SecurityContextHolder as SCH
import org.springframework.security.providers.UsernamePasswordAuthenticationToken

import acltest.Report
import acltest.Role
import acltest.User

class BootStrap {

   def aclUtilService
   def passwordEncoder
   def sessionFactory

   def init = { servletContext ->
      createUsers()
      createReports()
      createGrants()

      sessionFactory.currentSession.flush()
   }

   private void createUsers() {
      def adminRole = new Role(description: 'Admin', authority: 'ROLE_ADMIN').save()
      def admin = new User(username: 'admin', userRealName: 'admin',
            passwd: passwordEncoder.encodePassword('admin', null),
            enabled: true, email: 'admin@admin.com').save()
      adminRole.addToPeople admin

      def userRole = new Role(description: 'User', authority: 'ROLE_USER').save()
      def user1 = new User(username: 'user1', userRealName: 'user1',
            passwd: passwordEncoder.encodePassword('user1', null),
            enabled: true, email: 'user1@user.com').save()
      userRole.addToPeople user1

      def user2 = new User(username: 'user2', userRealName: 'user2',
            passwd: passwordEncoder.encodePassword('user2', null),
            enabled: true, email: 'user2@user.com').save()
      userRole.addToPeople user2
   }

   private void createReports() {
      (1..10).each { new Report(name: "report $it").save() }
   }

   private void createGrants() {

      loginAsAdmin()

      try {
         // user1 can see reports 1-4
         def user = User.findByUsername('user1')   
         (1..4).each {
            def report = Report.findByName("report $it")
            aclUtilService.addPermission(report,
                  user.username, BasePermission.READ)
         }
         // and can edit #3
         aclUtilService.addPermission(Report.findByName('report 3'),
               user.username, BasePermission.WRITE)
         // and edit and delete #4
         aclUtilService.addPermission(Report.findByName('report 4'),
               user.username, BasePermission.WRITE)
         aclUtilService.addPermission(Report.findByName('report 4'),
               user.username, BasePermission.DELETE)

         // user2 can see reports 5, 10
         user = User.findByUsername('user2')   
         [5, 10].each {
            def report = Report.findByName("report $it")
            aclUtilService.addPermission(report,
                  user.username, BasePermission.READ)
         }
      }
      finally {
         SCH.clearContext()
      }
   }

   // have to be authenticated as an admin to create ACLs
   private void loginAsAdmin() {
      SCH.context.authentication = new UsernamePasswordAuthenticationToken(
            'admin', 'password',
            [new GrantedAuthorityImpl('ROLE_ADMIN')] as GrantedAuthority[])
   }

   def destroy = {}
}

And to have a UI to test with, let's create a Report controller and GSPs:

grails generate-all acltest.Report

But to use the controller, it will have to be reworked to use ReportService. It's a good idea to put all create/edit/delete code in a transactional service, but in this case we need to move all database access to the service to ensure that appropriate access checks are made:

package acltest

import org.codehaus.groovy.grails.plugins.springsecurity.Secured

import org.springframework.dao.DataIntegrityViolationException

@Secured(['ROLE_ADMIN', 'ROLE_USER'])
class ReportController {

   static allowedMethods = [delete: 'POST', save: 'POST', update: 'POST']

   static defaultAction = 'list'

   def reportService

   def list = {
      params.max = Math.min(params.max ? params.max.toInteger() : 10, 100)

      [reportInstanceList: reportService.getAllReports(params),
       reportInstanceTotal: Report.count()]
   }

   def show = {
      def reportInstance = reportService.getReport(params.id?.toLong())
      if (!reportInstance) {
         flash.message = "Report not found with id $params.id"
         redirect action: list
         return
      }
      [reportInstance: reportInstance]
   }

   def delete = {
      def reportInstance = reportService.getReport(params.id?.toLong())
      if (!reportInstance) {
         flash.message = "Report not found with id $params.id"
         redirect action: list
         return
      }

      try {
         reportService.deleteReport(reportInstance)
         flash.message = "Report $params.id deleted"
         redirect action: list
      }
      catch (DataIntegrityViolationException e) {
         flash.message = "Report $params.id could not be deleted"
         redirect action: show, id: params.id
      }
   }

   def edit = {
      def reportInstance = reportService.getReport(params.id?.toLong())
      if (!reportInstance) {
         flash.message = "Report not found with id $params.id"
         redirect action: list
         return
      }

      [reportInstance: reportInstance]
   }

   def update = {
      def reportInstance = reportService.getReport(params.id?.toLong())
      if (!reportInstance) {
         flash.message = "Report not found with id $params.id"
         redirect action: list
         return
      }

      if (params.version) {
         long version = params.version.toLong()
         if (reportInstance.version> version) {
            reportInstance.errors.rejectValue('version',
               'report.optimistic.locking.failure',
               'Another user has updated this Report while you were editing.')
            render view:'edit',model: [reportInstance: reportInstance]
            return
         }
      }

      reportService.updateReport(reportInstance, params)
      if (reportInstance.hasErrors()) {
         render view: 'edit', model: [reportInstance: reportInstance]
         return
      }

      flash.message = "Report $params.id updated"
      redirect action: show, id: reportInstance.id
   }

   def create = {
      [reportInstance: new Report(params)]
   }

   def save = {

      def reportInstance = reportService.createReport(params)
      if (reportInstance.hasErrors()) {
         render view: 'create', model: [reportInstance: reportInstance]
         return
      }

      flash.message = "Report $reportInstance.id created"
      redirect action: show, id: reportInstance.id
   }
}

Note that the controller is annotated to require either ROLE_USER or ROLE_ADMIN. Since services have nothing to do with HTTP, when access is blocked you cannot be redirected to the login page as when you try to access a URL that requires an authentication. So you need to configure URLs with similar role requirements to give the user a chance to attempt a login before calling secured service methods.


Start the app:

grails run-app

and open http://localhost:8080/acltest/report/list

Login as user2/user2 and you should only see reports #5 and #10. Logout via http://localhost:8080/acltest/logout and open the list page again, this time logging in as user1/user1. Now you should be able to see instances #1-4.

Verify that you can view #3 directly by clicking the id or opening http://localhost:8080/acltest/report/show/3. Also verify that you can edit #3 but cannot delete it.

Verify that you can view #4 directly by clicking the id or opening http://localhost:8080/acltest/report/show/4 and that can edit and delete it.

Verify that you can't view #7 directly by opening http://localhost:8080/acltest/report/show/7.


You can download the full sample app here and the plugin with ACL support here.

There isn't much documentation available for Spring Security ACLs, but I found this blog post to be very informative and thorough.

If you have questions or issues with the code, please email the Grails User mailing list so others who might be having similar problems can partipate in the conversation.


Monday, December 21, 2009

I was looking at a non-Grails Spring Security application that used hierarchical roles and wondered what it'd take to get this working with the Grails plugin. Turns out it's pretty simple.

Non-hierarchical roles are checked by a RoleVoter but to use hierarchical roles you need a RoleHierarchyVoter. Replacing the roleVoter bean in resources.groovy is all it takes.

RoleHierarchyVoter needs an implementation of RoleHierarchy and the default implementation in Spring Security is RoleHierarchyImpl which parses a String defining the hierarchy. For example, this configuration defines the hierarchy ROLE_SUPERADMIN > ROLE_ADMIN > ROLE_USER:

import org.springframework.security.userdetails.hierarchicalroles.RoleHierarchyImpl
import org.springframework.security.vote.RoleHierarchyVoter

beans = {

   roleHierarchy(RoleHierarchyImpl) {
      hierarchy = '''
         ROLE_SUPERADMIN > ROLE_ADMIN
         ROLE_ADMIN > ROLE_USER
      '
''
   }

   roleVoter(RoleHierarchyVoter, ref('roleHierarchy'))
}

You can download a small demo app here that shows how it works. Unpack the app and run grails run-app, and then open http://localhost:8080/hierarchical/secure/. The app creates three users in BootStrap:

Username Password Role
user user ROLE_USER
admin admin ROLE_ADMIN
superadmin superadmin ROLE_SUPERADMIN

so you can login as each user to test the secured actions:

class SecureController {

   def index = {}

   @Secured(['ROLE_USER'])
   def user = {
   ...
   }

   @Secured(['ROLE_ADMIN'])
   def admin = {
   ...
   }

   @Secured(['ROLE_SUPERADMIN'])
   def superadmin = {
   ...
   }
}

Logout in between by navigating to http://localhost:8080/hierarchical/logout. Although only one role is defined for each action, as the super admin you can access all three, as the admin you can access admin and user, and as the user you can only access user.


I'll make this part of the plugin at some point to make configuration simpler, but for now it's not much work to do it explicitly.


Items:   1 to 5 of 24   Next »