Archive for the Category 'grails'

Grails App-Info plugin

Saturday, July 17th, 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.

Using GMail with a Log4j SMTP Appender in Grails

Wednesday, February 03rd, 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.

Delayed SessionFactory Creation in Grails

Monday, January 25th, 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.

ACLs in the Grails Spring Security Plugin

Sunday, December 27th, 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.

Hierarchical Roles in the Grails Spring Security Plugin

Monday, December 21st, 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.

Clustering Grails

Saturday, December 19th, 2009

I did a talk on Grails clustering at the Groovy & Grails eXchange 2009 in London last week and wanted to put up the slides and the sample application and clustering scripts. This was the third time I've given a version of this talk (first at a Boston Grails meetup and again at SpringOne 2GX) so I'm way overdue getting this up.

I created a simple Grails app and installed the Spring Security plugin (to test web cluster login failover and to have domain classes for Hibernate 2nd level caching) and the Quartz plugin to demonstrate support for multiple servers. The basic non-clustered version is here so you can compare it to the version that has the required changes here.

In no particular order, here are the relevant project files:

  • There's a simple Quartz job in grails-app/jobs/ClusterTestJob that prints which instance it's running on to stdout every two seconds.
  • grails-app/conf/QuartzConfig.groovy specifies jdbcStore=true to have the plugin configure job storage in the database instead of in-memory.
  • Externalized configuration is enabled in grails-app/conf/Config.groovy using
    grails.config.locations = [
         "classpath:${appName}-config.groovy",
         "file:./${appName}-config.groovy"]

    which allows local dev environment configuration using a file name clustered-config.groovy in the root directory of the project, or prod configuration (e.g. to keep production database passwords out of source control) using a clustered-config.groovy in the classpath. Tomcat's lib directory is in its classpath, so that's a convenient place to put the file.

  • In addition note that the Log4j file loggers are configured using ServerAwareFileAppender. Specifying the location of log files is tricky. If you leave the path out and make it relative, then files get created relative to the directory Tomcat is started from, which might not always be the same. But if you hard-code the path, it won't work cross-platform. So this class figures out if you're running in Tomcat or Jetty and if you're running in dev mode, and is also cluster-aware. Depending on how you're running it knows where the logs directory is and puts logs there.
  • grails-app/conf/hibernate/hibernate.cfg.xml loads grails-app/conf/hibernate/Quartz.mysql.innodb.hbm.xml which has DDL for creating the tables needed for Quartz. Use the appropriate DDL for your database from one of the files in the Quartz plugin's src/templates/sql directory.
  • Role.groovy has been customized to support the Hibernate 2nd-level cache:
    - implements Serializable
    - read-only cache configured in mapping (along with disabling optimistic locking since it's not needed)
    - custom list() and count() methods that optionally use the cache or bypass it
  • User.groovy is also customized for the Hibernate 2nd-level cache:
    - implements Serializable
    - read-write cache configured in mapping
  • grails-app/conf/spring/resources.groovy has the fix for GRAILSPLUGINS-1207
  • src/java/ehcache.xml has caches configured for the domain classes. It's not distributed though since it'll be used in the development environment.
  • scripts/_Events.groovy has code to delete unused jars (unrelated to clustering, just a good idea) and delete the version of ehcache.jar that comes with Grails since the app uses a newer version. It also replaces the non-distributed ehcache.xml with the distributed version from cluster_resources, and copies quartz.properties in case you need to customize those beyond the defaults.
  • grails-app/conf/BootStrap.groovy has code to create the admin role and an admin user to test login

The files from Tomcat and the cluster scripts are available here. Untar them somewhere on the server where you want to create a cluster:

tar xfz cluster.tar.gz
cd cluster

You'll see five scripts:

  • cleanup.sh
  • createCluster.sh
  • createInstance.sh
  • deploy.sh
  • run.sh

These are combinations of bash scripts and an Ant build file (clusterTasks.xml). All of the scripts need a cluster root directory. You can either specify it for each script invocation (run the script without parameters to see the usage) or you can set the CR environment variable

export CR=/usr/local/tomcatcluster

The first script to run is createCluster.sh:

./createCluster.sh

This will create the directory structure and copy the Tomcat jars and other shared files.

There's an empty clustered-config.groovy that you can use to customize the production deployment. One common use for this is to externalize database passwords. This can be done with JNDI, but I prefer a self-contained war file that doesn't require container-specific configuration. This is copied to $CR/shared/lib when you run createCluster.sh since it's shared by all instances.

Next you need to run createInstance.sh once for each cluster node on this server. The syntax is

createInstance.sh [server number] [instance number] [cluster root dir]

and you need to ensure that the server number and instance number combination is unique throughout the cluster. This is simple if you number each server and choose a unique instance number for each instance on that server, e.g.

./createInstance.sh 1 1
./createInstance.sh 1 2
./createInstance.sh 1 3

on server one,

./createInstance.sh 2 1
./createInstance.sh 2 2
./createInstance.sh 2 3

on server two, etc. So go ahead and create at least two nodes on this server:

./createInstance.sh 1 1
./createInstance.sh 1 2

Some files to note are $CR/shared/conf/server.xml and $CR/instance_X/conf/catalina.properties. Most values in server.xml have been replaced with ${} properties whose values are specified in catalina.properties. These property values are retrieved via System.getProperty() but Tomcat reads catalina.properties and sets system properties from there. Using this approach we can have one parameterized server.xml and allow per-instance customization of ports, etc. Another config file is $CR/instance_X/bin/setenv.sh which configures CATALINA_OPTS (and optionally other environment variables). Edit this file to change the heap or permgen memory for each instance.

Having created the instances, now you need to deploy a war. Download the test project and unpack it:

tar xfz clustered.tar.gz
cd clustered

and run grails compile to trigger installation of the project's plugins. As mentioned in the presentation, there's a bug in the Quartz plugin that causes a problem in clusters since it tries to re-register jobs, so edit plugins/quartz-0.4.1/QuartzGrailsPlugin.groovy and change

if(scheduler.getTrigger(trigger.name, trigger.group)) {
   scheduler.rescheduleJob(trigger.name, trigger.group, ctx.getBean("${key}Trigger"))
} else {
   scheduler.scheduleJob(ctx.getBean("${key}Trigger"))
}

to

if(scheduler.getTrigger(trigger.triggerAttributes.name,
                        trigger.triggerAttributes.group)) {
   scheduler.rescheduleJob(trigger.triggerAttributes.name,
                           trigger.triggerAttributes.group,
                           ctx.getBean("${key}Trigger"))
} else {
   scheduler.scheduleJob(ctx.getBean("${key}Trigger"))
}

Now you can build the war:

grails clean && grails war

and use deploy.sh to deploy it to the cluster:

./deploy.sh /path/to/clustered-0.1.war

The name of the war isn't important since deploy.sh deploys as the root context (in $CR/shared/webapps/ROOT). Note that you only deploy a war once per server since it's shared by all instances on that box.

Next create the database:

$ mysql -u root -p
Enter password:
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 8
Server version: 5.1.39-log MySQL Community Server (GPL)

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> create database clustered;
Query OK, 1 row affected (0.00 sec)

mysql> grant all on clustered.* to clustered@localhost identified by 'clustered';
Query OK, 0 rows affected (0.00 sec)

The project has a modified version of the SchemaExport script that fixes a bug reading hbm.xml files, so run

grails schema-export

and choose option [2] to use the project version instead of the Grails version (this bug is fixed in Grails 1.2). This will create a file called ddl.sql that you can use to create the tables. The generated script is designed to be used by Hibernate's schema export tool which ignores errors trying to drop foreign keys on non-existent tables but this will fail from the commandline, so edit the file and remove all of the DROP TABLE ... and alter table ... drop foreign key ... statements and run

mysql -u clustered -pclustered -D clustered < ddl.sql

Now that we've deployed the war and created the database, we can now start the instances using run.sh. The syntax is

run.sh [start|stop|run] [instance number]

so you would run

run.sh start 1

to start instance #1. Once it has successfully started you can start the other instances:

run.sh start 2

You should see output indicating that the instances are discovering one another.

The server logs are in a few different directories. $CR/instance_X/logs will contain that instance's catalina.out and localhost_access_log, and the two rolling Log4j logs configured in Config.groovy, X_clusterdemo.log and X_sql.log, prefixed with the instance number.

$CR/shared/logs contains the remaining Tomcat logs; admin, catalina, host-manager, localhost, and manager all prefixed with the instance number. instance_X.pid files are here also, containing the process id for each instance to help with shutting down individual instances.


There is no load balancing configured yet; this can be configured using hardware load balancers, Apache, etc. $CR/shared/conf/server.xml configures the jvmRoute attribute of the <Engine> element (worker${cluster.server.number}_${cluster.instance.number}) for use with mod_jk.

So for now to test the app we'll go to specific instances. Open http://localhost:8091/admin/ and since that controller is secured, it'll prompt you to login. Use the username and password configured in BootStrap.groovy (admin/p4ssw0rd) and you should be allowed to view the page.

To test session replication, kill this instance. Find the pid for instance 1 in $CR/shared/logs/instance_1.pid and run kill -9 to ensure there's no orderly cleanup:

kill -9 `cat $CR/shared/logs/instance_1.pid`

You'll see messages in the logs for instance 2 indicating that instance 1 has disappeared:
org.apache.catalina.tribes.group.interceptors.TcpFailureDetector memberDisappeared. You'll also see that if Quartz wasn't running in instance 2 it now is.

Open http://localhost:8092/admin/ and you should still be logged in since your Authentication is stored in the HTTP session and was replicated.


To shutdown an instance, use run.sh with the stop argument:

run.sh stop 1

You can use cleanup.sh to delete log files and temp files in the temp and work directories. This isn't required but it's convenient.


Some further reading:

Links and files:

Groovy & Grails eXchange 2009

Saturday, December 19th, 2009

I got back from the Groovy & Grails eXchange 2009 a few of days ago and wanted to get some thoughts down before the whole thing becomes a blur. It was quite a week - the conference was two days but I brought my wife and extended the trip into a small vacation. We did a bunch of tourist stuff including the Tower of London, the British Museum, Stonehenge, the cathedral in Salisbury, shopping at Oxford Street and Portobello Market, saw a play, etc. Wendy Devolder and her husband Nick graciously offered to meet us Saturday at Portobello Market since they live nearby. Portobello Market is huge but thanks to Wendy and Nick we saw a bunch of stuff we would have otherwise missed, including the amazing Mutate Britain: One Foot in the Grove street art and sculpture show.

There were even other attendees from Boston. My colleague Eugenia Harris (we work together at InnoCentive) was also there for the week so the three of us did a lot of the touristy stuff together, and four guys from Cantina Consulting were also there.

The talks were excellent, but the best thing about these conferences is the social interactions. I met a bunch of Groovy/Grails/Griffon folks in New Orleans at SpringOne 2GX and this was another opportunity to meet even more. I got to meet and talk to several Grails contributors and users including Peter Ledbrook, Marc Palmer, Tomas Lin, Robert Fletcher, Glenn Saqui, Luis Arias, Jakob Külzer, Stefan Armbruster, and Jeremy "The Human Grails Search Engine" Flowers.

The conference was very well run by Russ Miles and Wendy Devolder's crew at Skills Matter. The space was a little overbooked and would have been better with closer to 100 attendees but they were aware of that and are still getting used to the new location that they recently moved to. Other than that everything was smooth sailing, and they created a conducive environment for a bunch of Grails and Groovy geeks.

One particularly cool aspect was the speaker's dinner on Tuesday at the Slaughtered Lamb. It was fun talking to (and drinking with) Wendy, Nick, Peter, Tomas, and the rest of the folks there. Not having met people there in person, it was great to make some early introductions before the conference started.

The talks that I found most interesting involved stuff that I rarely use. I don't do much complex configuration management but I have in the past, so Russel Winder's Gant talk and Hans Dockter's Gradle talk were great to get a sense for the current state of the art in build tools. I also don't do much UI work, but I thought Tomas Lin's Flex talk and Sébastien Blanc's iWebkit talk were both pretty cool. They definitely make creating slick UIs look easy.

The highlight of the conference for me though wasn't actually part of the conference - Graeme Rocher and Peter Ledbrook's GGUG on Wednesday night on Grails internals. It was a rare opportunity to see a presentation from the two people who have contributed most significantly to Grails. We had a half hour break after a full day at the conference and they spoke and took questions for an hour and a half. It made for a long day, but the audience clearly would have stayed for hours more.

So all in all it was a blast. London was a lot of fun, and Skills Matter put on a great conference. We could use a Skills Matter here in Boston :)


You can download the PDF slides and watch the video here. I wrote up a post here showing how to use the sample apps from the talk.

SpringOne 2GX Sample Apps – Spring Security LDAP Login

Tuesday, December 01st, 2009

This is the second in a series of posts making the demo applications that I used for my SpringOne 2GX presentations available. I'll describe here how to create a Grails application using the Spring Security plugin that authenticates users from LDAP. This is based on topics from the Demystifying Spring Security in Grails talk (you can download the presentation here) but wasn't shown there since I ran out of time.

Also refer to the plugin documentation for other tutorials here.


To create an application that authenticates users from LDAP, run

grails create-app springone2gx_ldap
cd springone2gx_ldap

To make classpath management simpler in Eclipse/STS I create a grails-app/conf/BuildConfig.groovy (in Grails 1.1 apps; in 1.2 this is done for you) with the line

grails.project.plugins.dir='plugins'

to keep plugins in the project root like in 1.0.x but this is optional.

Next install the plugin:

grails install-plugin acegi

Run the create-auth-domains script to generate the person, authority, and request map domain classes and also grails-app/conf/SecurityConfig.groovy:

grails create-auth-domains com.burtbeckwith.springone2gx.User com.burtbeckwith.springone2gx.Role com.burtbeckwith.springone2gx.Requestmap

The other two scripts that the plugin provides are optional and create CRUD pages (generate-manager) and basic user registration (generate-registration). It's a good idea to run generate-manager; run generate-registration if it's useful to you.

grails generate-manager

As with the previous post we'll use annotated controllers, so we'll need to configure that, and we can delete the request map class and CRUD pages. The plugin scripts currently asssume you'll be using request maps, so we have to run generate-manager and generate-registration before deleting these.

  • delete grails-app/domain/com/burtbeckwith/springone2gx/Requestmap.groovy
  • delete grails-app/controller/RequestmapController.groovy
  • delete the grails-app/views/requestmap directory and its GSPs
  • remove the com.burtbeckwith.springone2gx.Requestmap import from grails-app/controller/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 = "com.burtbeckwith.springone2gx.User"
       authorityDomainClass = "com.burtbeckwith.springone2gx.Role"

       useRequestMapDomainClass = false
       useControllerAnnotations = true
    }

In Eclipse or STS the steps to configure the classpath are:

  • add PLUGIN_DIR/src/groovy as a source folder
  • add PLUGIN_DIR/src/java as a source folder
  • add PLUGIN_DIR/grails-app/services as a source folder
  • add these jars from PLUGIN_DIR/lib
    • facebook-java-api-2.0.4.jar
    • jcifs-1.2.25.jar
    • spring-ldap-1.2.1.jar
    • spring-ldap-tiger-1.2.1.jar
    • spring-security-core-2.0.4.jar
    • spring-security-core-tiger-2.0.4.jar
    • spring-security-ntlm-2.0.4.jar
    • spring-security-openid-2.0.4.jar

Having done all that, let's create a secured controller to test annotations:

grails create-controller secure

and add the import for the annotation, and annotate at the class level that you must be an admin to access this controller:

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

@Secured(['ROLE_ADMIN'])
class SecureController {

   def index = {
      redirect action: foo
   }

   def foo = {
      render 'OK'
   }

   def bar = {
      render 'also OK'
   }
}

The controller has a redirect from the default action and a second action so we can test that all methods inherit the class-level annotation.

Next lets configure LDAP. To make this a self-contained demo, we'll use the excellent LDAP server plugin but obviously you'll need to configure the application to connect to your LDAP server.

Install the plugin by running

grails install-plugin ldap-server

and add the necessary LDAP configuration option to grails-app/conf/SecurityConfig.groovy (at a minimum useLdap = true)

security {
   ...
   useLdap = true
   ldapServer = 'ldap://localhost:10389'
   ldapManagerDn = 'uid=admin,ou=system'
   ldapManagerPassword = 'secret'
   ldapSearchBase = 'dc=d1,dc=example,dc=com'
   ldapSearchFilter = '(uid={0})'
   ldapGroupSearchBase = 'ou=groups,dc=d1,dc=example,dc=com'
   ldapGroupSearchFilter = 'uniquemember={0}'
   ldapUsePassword = false
}

The LDAP plugin requires one or more configured LDAP servers in grails-app/conf/Config.groovy and we'll need just one:

ldapServers {
   d1 {
      base = 'dc=d1,dc=example,dc=com'
      port = 10389
      indexed = ['objectClass', 'uid', 'mail', 'userPassword', 'description']
   }
}

The plugin will auto-load .ldif data files with user information, so put these records in grails-app/ldap-servers/d1/data/users.ldif:

dn: ou=groups,dc=d1,dc=example,dc=com
objectclass: organizationalUnit
objectclass: top
ou: groups

dn: cn=USER,ou=groups,dc=d1,dc=example,dc=com
objectclass: groupOfUniqueNames
cn: USER
objectclass: top
uniqueMember: cn=person1,dc=d1,dc=example,dc=com
uniqueMember: cn=person2,dc=d1,dc=example,dc=com
uniqueMember: cn=person3,dc=d1,dc=example,dc=com

dn: cn=ADMIN,ou=groups,dc=d1,dc=example,dc=com
objectclass: groupOfUniqueNames
objectclass: top
cn: ADMIN
uniqueMember: cn=person2,dc=d1,dc=example,dc=com

dn: cn=person1,dc=d1,dc=example,dc=com
objectClass: uidObject
objectClass: person
objectClass: top
objectClass: organizationalPerson
uid: person1
userPassword: {SHA}44rSFJQ9qtHWTBAvrsKd5K/p2j0=
cn: person1
sn: jones

dn: cn=person2,dc=d1,dc=example,dc=com
objectClass: uidObject
objectClass: person
objectClass: top
objectClass: organizationalPerson
uid: person2
userPassword: {SHA}KqYKj/f81HPTIeAUav2eJt85UUc=
cn: person2
sn: jones

dn: cn=person3,dc=d1,dc=example,dc=com
objectClass: uidObject
objectClass: person
objectClass: top
objectClass: organizationalPerson
uid: person3
userPassword: {SHA}ERnP037iRzV+A0oI2ETuol9v0g8=
cn: person3
sn: jones

Spring Security will by default convert LDAP groups ('groupOfUniqueNames') to roles, prefixing the group names with ROLE_, so this data creates three users; person1, person2, and person3 (with passwords 'password1', 'password2', and 'password3' respectively), all with ROLE_USER and person2 with ROLE_ADMIN.

Since LDAP is only managing authentication details we need local data in the database; create the corresponding entries in BootStrap:

import com.burtbeckwith.springone2gx.User

class BootStrap {

   def init = { servletContext ->
      new User(username: 'person1', enabled: true).save()
      new User(username: 'person2', enabled: true).save()
      new User(username: 'person3', enabled: true).save(flush: true)
   }

   def destroy = {}
}

Since LDAP is handling authentication, we can (partially) remove password-related fields from User.groovy along with other unused fields:

class User {

   static hasMany = [authorities: Role]
   static belongsTo = Role

   String username
   String passwd = 'notused'
   boolean enabled

   static constraints = {
      username blank: false, unique: true
   }
}

We need to leave in the passwd property since GrailsDaoImpl expects it but its value isn't important so we'll just hard-code it in the domain class. A custom subclass GrailsDaoImpl or a new implementation of UserDetailsService would remove this requirement.

Start the app using

grails run-app

and open http://localhost:8080/springone2gx_ldap/secure/ in a browser and it should prompt you to login. If you login as person1 or person3 you'll be denied access since those users only have ROLE_USER but person2 has ROLE_ADMIN and will be allowed.

After successful login it'll redirect to http://localhost:8080/springone2gx_ldap/secure/foo - verify that http://localhost:8080/springone2gx_ldap/secure/bar is also secured by going to that in your browser.


You can download a finished application based on this discussion here

SpringOne 2GX Sample Apps – Spring Security Form Login

Tuesday, December 01st, 2009

This is the first in a series of posts making the demo applications that I used for my SpringOne 2GX presentations available. I'll describe here how to create a standard Grails application using the Spring Security plugin that authenticates users from a database. This was used in the Demystifying Spring Security in Grails talk (you can download the presentation here)

Also refer to the plugin documentation for other tutorials here.


To create a standard application that loads users from a database, run

grails create-app springone2gx
cd springone2gx

To make classpath management simpler in Eclipse/STS I create a grails-app/conf/BuildConfig.groovy (in Grails 1.1 apps; in 1.2 this is done for you) with the line

grails.project.plugins.dir='plugins'

to keep plugins in the project root like in 1.0.x but this is optional.

Next install the plugin:

grails install-plugin acegi

Run the create-auth-domains script to generate the person, authority, and request map domain classes and also grails-app/conf/SecurityConfig.groovy:

grails create-auth-domains com.burtbeckwith.springone2gx.User com.burtbeckwith.springone2gx.Role com.burtbeckwith.springone2gx.Requestmap

The other two scripts that the plugin provides are optional and create CRUD pages (generate-manager) and basic user registration (generate-registration). It's a good idea to run generate-manager; run generate-registration if it's useful to you.

grails generate-manager

There are three methods for securing URLs in the plugin and for this demo we'll use controller annotations, so we'll need to configure that. We can delete the request map class, controller, and CRUD pages since they won't be needed. The plugin scripts currently assume you'll be using request maps, so we have to run generate-manager and generate-registration before deleting these.

  • delete grails-app/domain/com/burtbeckwith/springone2gx/Requestmap.groovy
  • delete grails-app/controller/RequestmapController.groovy
  • delete the grails-app/views/requestmap directory and its GSPs
  • remove the com.burtbeckwith.springone2gx.Requestmap import from grails-app/controller/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 = "com.burtbeckwith.springone2gx.User"
       authorityDomainClass = "com.burtbeckwith.springone2gx.Role"

       useRequestMapDomainClass = false
       useControllerAnnotations = true
    }


The current plugin is monolithic - it has support for several authentication mechanisms (OpenID, Facebook, etc.) If you're not using these you can delete that code and associated jar files. This is completely optional, and you can always add them back by extracting them from the plugin zip.

If you're not using OpenID, here are the steps to remove that code:

  • delete the org.codehaus.groovy.grails.plugins.springsecurity.openid package: PLUGIN_DIR/src/groovy/org/codehaus/groovy/grails/plugins/springsecurity/openid
  • delete these jars
    • openid4java-0.9.2.jar
    • spring-security-openid-2.0.4.jar
    • xmlsec-1.3.0.jar
    • htmlparser-1.6.jar
    • commons-httpclient-3.0.1.jar
    • openxri-client.jar
    • openxri-syntax.jar
  • in LoginController.groovy
    • delete def openIDConsumer
    • delete def openIDAuthenticationProcessingFilter
    • delete the openIdAuthenticate() action
    • remove the config.useOpenId part in the auth() action

If you're not using Facebook, here are the steps to remove that code:

  • delete the org.codehaus.groovy.grails.plugins.springsecurity.facebook package: PLUGIN_DIR/src/java/org/codehaus/groovy/grails/plugins/springsecurity/facebook
  • delete these jars
    • facebook-java-api-2.0.4.jar
    • json-20070829.jar
  • in LoginController.groovy
    • remove the config.useFacebook part in the auth() action

If you're not using Kerberos, here are the steps to remove that code:

  • delete the org.codehaus.groovy.grails.plugins.springsecurity.kerberos package: PLUGIN_DIR/src/groovy/org/codehaus/groovy/grails/plugins/springsecurity/kerberos

If you're not using LDAP, here are the steps to remove that code:

  • delete the org.codehaus.groovy.grails.plugins.springsecurity.ldap package: PLUGIN_DIR/src/groovy/org/codehaus/groovy/grails/plugins/springsecurity/ldap and PLUGIN_DIR/src/java/org/codehaus/groovy/grails/plugins/springsecurity/ldap
  • delete these jars
    • spring-ldap-1.2.1.jar
    • spring-ldap-tiger-1.2.1.jar
  • in LoginController.groovy
    • remove the config.useFacebook part in the auth() action

If you're not using CAS, here are the steps to remove that code:

  • delete these jars

    • cas-client-core-3.1.1.jar
    • spring-security-cas-client-2.0.4.jar

Even if you don't use NTLM it's best to leave it in - the jars and code aren't large and it's more complicated to remove than just deleting.

In Eclipse or STS the steps to configure the classpath are:

  • add PLUGIN_DIR/src/groovy as a source folder
  • add PLUGIN_DIR/src/java as a source folder
  • add PLUGIN_DIR/grails-app/services as a source folder
  • add PLUGIN_DIR/lib/spring-security-core-2.0.4.jar
  • add PLUGIN_DIR/lib/spring-security-core-tiger-2.0.4.jar
  • add PLUGIN_DIR/lib/spring-security-ntlm-2.0.4.jar
  • add PLUGIN_DIR/lib/jcifs-1.2.25.jar

Having done all that, let's create a secured controller to test annotations:

grails create-controller secure

and add the import for the annotation, and annotate at the class level that you must be an admin to access this controller:

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

@Secured(['ROLE_ADMIN'])
class SecureController {

   def index = {
      redirect action: foo
   }

   def foo = {
      render 'OK'
   }

   def bar = {
      render 'also OK'
   }
}

The controller has a redirect from the default action and a second action so we can test that all methods inherit the class-level annotation.

Hitting this controller will require a login, so lets add code to BootStrap to create a user:

import com.burtbeckwith.springone2gx.Role
import com.burtbeckwith.springone2gx.User

class BootStrap {

   def passwordEncoder

   def init = { servletContext ->

      def adminRole = new Role(description: 'Admin role',
                  authority: 'ROLE_ADMIN').save()

      String password = passwordEncoder.encodePassword('p4ssw0rd', null)
      def me = new User(username: 'admin',
                  passwd: password, enabled: true).save()
      adminRole.addToPeople(me)
      adminRole.save(flush: true)
   }

   def destroy = {}
}

Start the app using

grails run-app

and open http://localhost:8080/springone2gx/secure/ in a browser and it should prompt you to login - use the username and password from the user created in BootStrap.

After successful login it'll redirect to http://localhost:8080/springone2gx/secure/foo - verify that http://localhost:8080/springone2gx/secure/bar is also secured by going to that in your browser.


The 'secure' controller blocks access, but anyone can access the role or user controller - open http://localhost:8080/springone2gx/user/list to verify that. So let's finish locking down the app.

Add the same annotation to UserController and RoleController that you added to SecureController:

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

@Secured(['ROLE_ADMIN'])
class UserController {
...
}

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

@Secured(['ROLE_ADMIN'])
class RoleController {
...
}

Up to now we've assumed that all access should be allowed unless it's explicitly blocked. This is appropriate for many applications, but you might want to take the pessimistic approach of denying access unless it's allowed. That's simple to do.

Set controllerAnnotationsRejectIfNoRule to true in grails-app/conf/SecurityConfig.groovy, and since we can't annotate CSS/JavaScript/images/etc., we'll need to allow access to those using the controllerAnnotationStaticRules property:

security {

   active = true

   loginUserDomainClass = 'com.burtbeckwith.springone2gx.User'
   authorityDomainClass = 'com.burtbeckwith.springone2gx.Role'

   useRequestMapDomainClass = false
   useControllerAnnotations = true

   controllerAnnotationsRejectIfNoRule = true
   controllerAnnotationStaticRules = [
      '/**/js/**': ['IS_AUTHENTICATED_ANONYMOUSLY'],
      '/**/css/**': ['IS_AUTHENTICATED_ANONYMOUSLY'],
      '/**/images/**': ['IS_AUTHENTICATED_ANONYMOUSLY'],
      '/*': ['IS_AUTHENTICATED_ANONYMOUSLY']
   ]
}

Users won't be able to log in now, since you're now allowing access to LoginController. That's easy to do: add an annotation:

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

@Secured(['IS_AUTHENTICATED_ANONYMOUSLY'])
class LoginController {
...
}

IS_AUTHENTICATED_ANONYMOUSLY means that any access is allowed - "anonymous" logins as well as real logins. Do the same for LogoutController, and CaptchaController and RegisterController if you're using the registration features of the plugin.

Using this approach, new controllers will be inaccessible until you annotate them. If you need the pessimistic approach, this is preferred to the alternative that controllers accidentally allow full access because you forgot to lock them down.


You can download a finished application based on this discussion here

Fixing the User/Role Many-to-Many in the Grails Spring Security Plugin

Sunday, October 25th, 2009

The User/Role many-to-many relationship in the Grails Spring Security plugin is modeled using the standard GORM mapping approach, i.e. using hasMany and belongsTo. As I pointed out here this is a performance concern when you have a large number of users, since granting a new user a popular role (e.g. ROLE_USER) will cause all other users with that role to be loaded from the database.

To fix this in the current plugin would be a breaking change, but I'm planning on creating a new plugin that will use Spring Security 3 once it's released, so I thought I'd write up some notes on how to fix the many-to-many mapping for current users. It's only a few steps.

The first is to map the join table, so you'll need to create a UserRole domain class (I'm assuming that your person class is named User and your authority class is named Role - translate as appropriate):

import org.apache.commons.lang.builder.HashCodeBuilder

class UserRole implements Serializable {

   User user
   Role role

   boolean equals(other) {
      if (!(other instanceof UserRole)) {
         return false
      }

      return other.user.id == user.id && other.role.id == role.id
   }

   int hashCode() {
      return new HashCodeBuilder().append(user.id).append(role.id).toHashCode()
   }

   static UserRole create(User user, Role role, boolean flush = false) {
      new UserRole(user: user, role: role).save(flush: flush, insert: true)
   }

   static boolean remove(User user, Role role, boolean flush = false) {
      UserRole userRole = UserRole.findByUserAndRole(user, role)
      return userRole ? userRole.delete(flush: flush) : false
   }

   static void removeAll(User user) {
      executeUpdate("DELETE FROM UserRole WHERE user=:user", [user: user])
   }

   static mapping = {
      id composite: ['role', 'user']
      version false
      table 'role_people'
   }
}

Some notes on this class:

  • it has to implement Serializable since it's a Hibernate composite primary key class
  • the mapping block settings ensure that the table DDL is the same as that for the autogenerated join table, so you won't need to update your database
  • the hashCode and equals methods are just suggestions; feel free to re-implement

Next remove static hasMany = [people: User] from Role and static hasMany = [authorities: Role] and static belongsTo = Role from User.

While we don't want to map the Role's User collection, we still need convenient access to the User's roles, so next add a utility method to User to mimic what we removed when deleting the hasMany. While we're here let's add a hasRole method:

Set<Role> getAuthorities() {
   UserRole.findAllByUser(this).collect { it.role } as Set
}

boolean hasRole(Role role) {
   UserRole.countByUserAndRole(this, role)> 0
}

If you're using the plugin-generated CRUD pages (created via grails generate-manager) you'll want to remove the User listings from views/role/show.gsp:

<tr class="prop">
   <td valign="top" class="name">People:</td>
   <td valign="top" class="value">${authority.people}</td>
</tr>

and views/role/edit.gsp:

<tr class="prop">
  <td valign="top" class="name"><label for="people">People:</label></td>
  <td valign="top" class="value ${hasErrors(bean:authority,field:'people','errors')}">
  <ul>
  <g :each var="p" in="${authority.people?}">
     <li>${p}</li>
  </g>
  </ul>
  </td>
</tr>

Then in RegisterController.groovy change

role.addToPeople(person)

to

UserRole.create(person, role)

and finally in UserController.groovy, change (in two places)

Role.findAll().each { it.removeFromPeople(person) }

to

UserRole.removeAll(person)

and

Role.findByAuthority(key).addToPeople(person)

to

UserRole.create(person, Role.findByAuthority(key))


And that's it. You shouldn't need to make any database changes, since the new code will map to the existing tables just like the old code. If you've used the addToPeople and removeFromPeople dynamic many-to-many methods elsewhere in your code you'll need to convert those to use the UserRole helper methods, but otherwise the impact should be fairly minor.