Three's a Crowd - securing a Grails application with Acegi and Crowd
March 4, 2008 2:54 PMOne of the cool things about working for Internal Systems is the opportunity to work on new systems. My most recent project is the development of my Fedex VII project into a fully-fledged application.
Where my Fedex VII project was a standard Java web application, I'm doing the real version using Grails with Acegi. However, I have to use our existing Crowd instance, to leverage its user base and its single sign-on capabilities. While there's some good documentation out there about the individual components, it's still a little bit of work to get Grails + Acegi + Crowd going together. This is how I did it.
Step 1: Grails & Acegi Installation
I downloaded and installed Grails v1.0.1 and Grails' Acegi Security Plugin v0.2, and set my GRAILS_HOME and PATH environment variables in .bash_login so I didn't drive myself batshit insane by having to set them for each new terminal window.
Step 2: Create Grails Application with Acegi Security
I created my application via the command line,
Kate:grails kellingburg$ grails create-app AcegiApp
installed the Acegi Plugin,
Kate:grails kellingburg$ cd AcegiApp/ Kate:AcegiApp kellingburg$ grails install-plugin PATH_TO_PLUGIN/grails-acegi-0.2.zip
created the domain classes that Acegi uses,
Kate:AcegiApp kellingburg$ grails create-auth-domains
and created a controller that only authorised users are able to access.
Kate:AcegiApp kellingburg$ grails create-controller AuthOnly
I then edited /conf/Bootstrap.groovy to add the Requestmap object that will secure the controller (note that the url is all in lowercase),
class BootStrap {
def init = { servletContext ->
new Requestmap(url:"/authonly/**",configAttribute:"ROLE_USER").save()
}
def destroy = {
}
}
put some dummy code in /controllers/AuthOnlyController.groovy,
class AuthOnlyController {
def index = {
render "Hello, World!"
}
}
and finally edited /web-app/index.gsp to use some of the Acegi taglibs:
<h3> <g:isLoggedIn>Hello <b><g:loggedInUserInfo field="username"/></b>!</g:isLoggedIn> <g:isNotLoggedIn>You are not logged in</g:isNotLoggedIn> </h3><g:ifAllGranted role="ROLE_ADMIN,ROLE_TEST">-- ROLE_ADMIN and ROLE_TEST granted<br/></g:ifAllGranted>
<g:ifAnyGranted role="ROLE_ADMIN,ROLE_TEST">-- ROLE_ADMIN or ROLE_TEST granted<br/></g:ifAnyGranted>
<g:ifNotGranted role="ROLE_TEST">-- ROLE_TEST not granted<br/></g:ifNotGranted>
<g:ifAnyGranted role="ROLE_USER">-- ROLE_USER granted<br/></g:ifAnyGranted>
I was now ready to run the application!
Kate:AcegiApp kellingburg$ grails run-app
Browsing to http://localhost:8080/AcegiApp/ showed the front page, and as expected, I was not logged in:

Clicking on AuthOnlyController brought up the login screen:

If this was a Grails + Acegi application, I could now begin to add users and roles to my application (by default, Acegi calls these Person objects and Authority objects). But I'm going to get my users from Crowd instead.
Step 3: Crowd Download
I grabbed a beta version of Crowd v1.3, but seeing as it was released earlier this week, you can download Crowd v1.3 from the Atlassian website.
Step 4: Crowd Installation & Configuration
I followed the Crowd v1.3 installation instructions to install Crowd locally. When my application is deployed, I will integrate with Atlassian's Crowd instance, but for development I wanted to set up my own. Because I wanted set up to be as painless as possible, I chose to use the database supplied with Crowd. I was able to quickly grab an evaluation license from the My Account section of the Atlassian website.
Installing and configuring Crowd was a cinch. My previous experiences with single-sign on products had left me very hesitant to get my feet wet with Crowd, but boy am I glad I did. It's hard to imagine how it could have been easier.
I configured one directory, one application, two users and two groups. The group names matched the Authorities that my AcegiApp will expect: ROLE_USER and ROLE_ADMIN.
To check that I'd set everything up correctly, Crowd provides an option under "Config Test" that verifies whether a user can authenticate with the application:

Step 5: Grails + Acegi + Crowd Integration
Crowd comes with centralised authentication and single sign-on connectors for Acegi, and some pretty darn good instructions. However, because this was a Grails app, I had to do things slightly differently.
crowd.properties
I copied crowd.properties from CROWD_INSTALL/client/conf to ACEGI_APP. It would be nice if Grails provided a /properties folder to hold properties files, but I needed to put the file in the root directory. I needed to edit this file as per the Crowd documentation:
application.name acegiapp application.password password application.login.url http://localhost:8080/AcegiApp/login/auth
applicationContext-CrowdClient.xml
I copied applicationContext-CrowdClient.xml from the CROWD/client/crowd-integration-client-1.3.jar to ACEGI_APP/conf/spring and renamed it resources.xml.
I added some beans to resources.xml, similar to but not the same as the Crowd documentation:
<bean id="crowdUserDetailsService" class="com.atlassian.crowd.integration.acegi.CrowdUserDetailsService">
<property name="securityServerClient" ref="securityServerClient"/>
</bean><bean id="crowdAuthenticationProvider" class="com.atlassian.crowd.integration.acegi.CrowdAuthenticationProvider">
<property name="userDetailsService" ref="crowdUserDetailsService"/>
<property name="httpAuthenticator" ref="httpAuthenticator"/>
<property name="securityServerClient" ref="securityServerClient"/>
</bean><bean id="authenticationProcessingFilter" class="com.atlassian.crowd.integration.acegi.CrowdAuthenticationProcessingFilter">
<property name="httpAuthenticator" ref="httpAuthenticator"/>
<property name="authenticationManager" ref="authenticationManager"/>
<property name="authenticationFailureUrl" value="/login/authfail?login_error=1"/>
<property name="defaultTargetUrl" value="/"/>
<property name="filterProcessesUrl" value="/j_acegi_security_check"/>
</bean><bean id="crowdLogoutHandler" class="com.atlassian.crowd.integration.acegi.CrowdLogoutHandler">
<property name="httpAuthenticator" ref="httpAuthenticator"/>
</bean><bean id="logoutFilter" class="org.acegisecurity.ui.logout.LogoutFilter">
<constructor-arg value="/"/>
<constructor-arg>
<list>
<ref bean="crowdLogoutHandler"/>
<bean class="org.acegisecurity.ui.logout.SecurityContextLogoutHandler"/>
</list>
</constructor-arg>
<property name="filterProcessesUrl" value="/j_acegi_logout"/>
</bean>
jar files
I copied the following jar files to the /lib directory. This step is where I really, really missed Maven 2 -- I spent a lot of time tracking down dependencies, particularly of jars not provided in CROWD/client/lib. (Note: this was a problem in the beta version of Crowd 1.3, the Crowd developers assure me they've since fixed this.)
activation-1.1.jar atlassian-core-3.8.jar commons-codec-1.3.jar commons-collections-3.2.jar commons-httpclient-3.0.jar crowd-integration-client-1.3.jar jaxen-1.1-beta-9.jar jdom-1.0.jar propertyset-1.3-21Nov03.jar stax-api-1.0.1.jar wsdl4j-1.6.1.jar wstx-asl-3.2.4.jar xfire-aegis-1.2.6.jar xfire-core-1.2.6.jar xwork-1.2.3.jar
I didn't copy all the files in CROWD/client/lib as Grails includes many of the dependencies by default.
AcegiGrailsPlugin.groovy
I then edited ACEGI_APP/plugins/acegi-0.2/AcegiGrailsPlugin.groovy -- which means I'll have to be careful if I ever upgrade the plugin.
At line 91, I removed the declarations of logoutFilter and authenticationProcessingFilter, and at line 179, I replaced daoAuthenticationProvider with crowdAuthenticationProvider:
authenticationManager(org.acegisecurity.providers.ProviderManager){
providers=[
ref("crowdAuthenticationProvider"),
ref("anonymousAuthenticationProvider"),
ref("rememberMeAuthenticationProvider")]
}
AuthorizeTagLib.groovy
I changed line 70 in ACEGI_APP/plugins/acegi-0.2/grails-app/taglib/AuthorizeTagLib.groovy so that it didn't refer to the domainClass field. This is because the authPrincipal is now a com.atlassian.crowd.integration.acegi.CrowdUserDetails object, and it does not have a domainClass field.
def loggedInUserInfo = {attrs,body->
def authPrincipal = SCH?.context?.authentication?.principal
if( authPrincipal!=null && authPrincipal!="anonymousUser"){
//out << authPrincipal?.domainClass?."${attrs.field}"
out << authPrincipal?."${attrs.field}"
}else{
out << body()
}
}
As with AcegiGrailsPlugin.groovy, I'll have to be careful if I ever upgrade the plugin.
Step 5: Grails + Acegi + Crowd
I started up my Grails applications, AcegiApp, went to the front page, clicked on AuthOnly and logged in as the first user I created. I'm now able to see the page:

And my front page now shows some information about the logged in user:

In Summary
Getting started with the individual components was pretty straight forward. I did a lot of trial-and-error to figure out how to integrate with Crowd, but the Crowd documentation was a great starting place. Some of that trial-and-error was due to *ahem* developer issues rather than Grails, Acegi or Crowd.
Two areas of uncoolness: transitive dependencies and plugin editing.
I really, really missed having Maven 2 support for Grails (currently the maven-grails-plugin does not support Grails v1.0.1) purely because of transitive dependency resolution.
I had to edit the two of the plugin files, which means I will have to be careful when upgrading the plugin. Plus, I ignore some of the fields in AcegiConfig.groovy. Ideally, integrating Crowd would require changes only to AcegiConfig.groovy and any fields set here would be referenced by the Crowd beans.
The biggest surprise from the whole endeavour - besides spending hours on an issue that arose because I typed a comma instead of a full stop - was how easy Crowd was. Installation and configuration were a breeze, and quick to boot. All Internal Systems applications will now include Crowd support from the get go - not because we have to, but because we want to.
Does this sound like fun to you? We are hiring in Internal Systems!



Copyright © 2009 Atlassian Pty Ltd.

8 Comment(s)
Kate,
you could you Grails ivy support to declare and resolve dependencies:
http://grails.org/Ivy+Integration
By Dmitriy Kopylenko at March 5, 2008 8:57 AM
Hello, I am having a problem when I run the app.
I am getting this error on save method on Bootstrap:
2008-03-06 00:08:16.780::WARN: Failed startup of context org.mortbay.jetty.webapp.WebAppContext@e5b3f{/AcegiApp,/Users/joaopaulo/Documents/workspace/AcegiApp/web-app}
org.codehaus.groovy.runtime.InvokerInvocationException: groovy.lang.MissingMethodException: No signature of method: Requestmap.save() is applicable for argument types: () values: {}
By Joao Paulo at March 5, 2008 7:22 PM
@Dmitriy: I hadn't thought of giving Ivy a go - it looks like something worth checking out though, thanks!
@Joao Paulo: Are you using Eclipse? I'm pretty sure I've seen this problem when I've used Eclipse and haven't checked a couple of settings (as per http://grails.org/Eclipse+IDE+Integration )
By Kate Ellingburg at March 5, 2008 9:32 PM
Perfect timing! I've just started working with Grails for a quick project and was about to start connecting it to Crowd with Acegi.
Cheers!
By Sam at March 7, 2008 7:44 AM
Great primer! I'm thinking this would be great to roll into the grails-acegi plugin, or even spawn a new Grails plugin to account for the Crowd integration.
Quick question: do you have any ideas how to allow Crowd groups to work as ACEGI roles without the "ROLE_" prefix?
Cheers,
By Graham Bakay at March 14, 2008 2:20 PM
@Sam: Good to hear!
@Graham: I was thinking that a Crowd plugin for Grails might be a good 20% time project :)
I have been able to get Crowd groups to work as Acegi roles without the ROLE_ prefix, but I haven't done much with it (the application I'm currently working on only requires authentication, not authorisation). But here's how I did it:
1. Created the group in Crowd ("ApplicationUser")
2. Assigned to one of my users
3. Edited index.gsp and added You have App User role!
Authenticating as the user in #2 shows the text, but authenicating as a different user doesn't. This is the extent of my investigation. I suspect that the ROLE_ prefix on Acegi roles is a matter of convention rather than a requirement, though I could easily be proven wrong.
By Kate Ellingburg at March 16, 2008 7:58 PM
Hey Kate, if you're thinking about making a Crowd/ACEGI/Grails plugin on you 20% time, I'd love to collaborate with you. I'm probably going to contact Tsuyoshi (the lead on the existing ACEGI plugin) and see if it should be seperate from the existing ACEGI plugin or if it can co-exist inside it. Please send me an email if you're interested!
By Graham Bakay at March 17, 2008 2:14 PM
I've donated some code to the Acegi plugin project that makes most of the editing you had to do unnecessary. I'd previously implemented CAS SSO with Acegi, so when I started using the Grails plugin I realized that the configuration was too restictive. So I refactored to allow a lot more user configurability such as which filters to use, logout handlers, etc. To do what you did in the latest version:
- remove your 'logoutFilter' bean and instead create the attribute 'logoutHandlerNames' in conf/SecurityConfig.groovy (used to be AcegiConfig.groovy), e.g.
logoutHandlerNames = ['crowdLogoutHandler', 'securityContextLogoutHandler']
Then instead of using the standard 'rememberMeServices' and 'securityContextLogoutHandler' it will use your beans - all you need to do is
define crowdLogoutHandler in resources.xml or resources.groovy as you did
- rename 'crowdUserDetailsService' to 'userDetailsService' - this will override the default bean with yours
- remove your redefinition of authenticationManager and define a 'providerNames' list like for logout handlers above, e.g.
providerNames = ['crowdAuthenticationProvider', 'anonymousAuthenticationProvider', 'rememberMeAuthenticationProvider']
and this will replace the default list providers with yours.
I tried to make the GrailsUser more flexible, but you'll still need to edit that one line in the taglib. I'll think about that some more to see how to make this easier.
You can download the latest build at http://plugins.grails.org/grails-acegi/trunk/grails-acegi-0.3-20080416-SNAPSHOT.zip - it's production-ready but I included 'SNAPSHOT' in the name to make sure v0.2.1 is considered the latest while we test the changes.
Email me if this is confusing :)
By Burt Beckwith at April 15, 2008 11:40 PM