Separating and securing Grails controllers

Ever wanted to have several Grails controllers automatically secured – just by name?

I had to make a subset of my Grails 1.2.2 application controllers only available to a certain group of people. A few controllers made actions on the application possible which only Administrators were allowed to do.

Acegi what?

So, I think everybody recognizes the ever so popular Acegi way (using Spring Security) of securing things with a single com.app.controller.HelloController in SecurityConfig.groovy:

security {
	active = true

	useRequestMapDomainClass = false
	requestMapString = """
		CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON
		PATTERN_TYPE_APACHE_ANT

		/hello/**=ROLE_USER
		/**=IS_AUTHENTICATED_ANONYMOUSLY
	"""
}

This way a login-screen will appear when a user tries to open the HelloController on /hello since ROLE_USER is required. Well, not really high-tech yet – a basic example you could find in the Acegi plugin’s documentation as well.

More…more controllers!

Let’s introduce several other controllers next to the com.app.controller.HelloController. Notice I myself made up the package name com.app.controller – just a habit to seperate Domain classes, Controllers and Services into com.app.domain, com.app.controller and com.app.services.

Anyway, see here a package structure with a few new controllers:

com.app.controller
 \ HelloController.groovy
 \ ImportController.groovy
 \ BackupController.groovy
com.app.domain
 \ User.groovy
 \ Role.groovy

Now, it isn’t surprising that ImportController and BackupController might be used for several system administration functions, such as importing of backing up data – not to be messed with by regular users! In SecurityConfig we would have to adjust the settings in order to only allow Administrators to do execute these kind of backend-controllers.

security {
	active = true

	useRequestMapDomainClass = false
	requestMapString = """
		CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON
		PATTERN_TYPE_APACHE_ANT

		/import/**=ROLE_ADMIN
		/backup/**=ROLE_ADMIN
		/hello/**=ROLE_USER
		/**=IS_AUTHENTICATED_ANONYMOUSLY
	"""
}

The problem I had in my development team, was that any developer might be developing some kind of backend or ‘administration’ controller – without adding the appropratiate changes in SecurityConfig. Next of all, when the list grows to…

/scrub/**=ROLE_ADMIN
/settings/remove**=ROLE_ADMIN
/settings/delete**=ROLE_ADMIN
/run/**=ROLE_ADMIN
/export2/**=ROLE_ADMIN
/another/**=ROLE_ADMIN
/schedule/**=ROLE_ADMIN
/export/**=ROLE_ADMIN
/import/**=ROLE_ADMIN
/backup/**=ROLE_ADMIN

…you’ll need something more centralized 🙂

I could either use annotations for each controller at the action or class-level, such as @Secured(['ROLE_ADMIN']), but then each developer had to remember to add these constructs to each backend-controller instead of adding a requestmap entry in SecurityConfig.

Depending on your needs annotations just might be your preferred way of doing this, but I choose to take a less intrusive (and in my opinion more fail-safe) way to separate these controllers from the rest. Here’s the summary:

  • Move the backend-controllers into a separate package
  • Have url mappings and SecurityConfig do the crunch work!

Seperate package

Move the backend controllers to a seperate subpackage e.g. com.app.controller.admin.

com.app.controller
 \ HelloController.groovy
com.app.controller.admin
 \ ImportController.groovy
 \ BackupController.groovy
com.app.domain
 \ User.groovy
 \ Role.groovy

Note that the controllers are still accessible on the original url’s – no matter their changed packages! One could still execute domain.com/import to access the ImportController.

Change url mapping

In UrlMappings.groovy we have to add a few bits – mainly I want the backend controllers to be accessible under the /admin url and disallow access on the original ones.

First we make a convenience method inside UrlMappings to locate and list controllers with a certain package name.

/**
 * Returns a list of controller names starting with specified package name.
 *
 * @param packageName The name the controller package should start with
 * @return the names of the controllers
 */
private static def findControllersInPackage(String packageName) {
	def controllers = []
	ApplicationHolder.application.controllerClasses.findAll{ c -> c.packageName.startsWith( packageName ) }.each { c ->
		controllers.add(c.logicalPropertyName)
	}
	return controllers
}

…and second we iterate over these controllers and make them work on our new url and disable them on the original url.

class UrlMappings {
	static mappings = {

		// collect controllers
		def secureControllers = findControllersInPackage("com.app.controller.admin")

		// disallow secured controllers on normal url
		"/$controller/$action?/$id?"{
			constraints {
				controller(validator: {
					   return !secureControllers.contains(it)
				})
			}
		}

		// allow secured controllers on secured url
		secureControllers.each { controllerName ->
			"/admin/${controllerName}/$action?/$id?" {
				controller = controllerName
			}

		}

		// errors
		"500"(view:'/error')
	}

	private static def findControllersInPackage(String packageName) {
		//...
	}
}

The way we collect the appropriate controllers allows me to even have controllers in sub sub subpackages under com.app.controller.admin. You’ll find that /import no longer works, but now you can find it under /admin/import – because of the url mappings disallow the original mappings, even generated or scaffolded GSP’s pages (with their createLink()‘s) keep working.

Secure url

Well, the only thing left to do, is shorten our SecurityConfig – securing everything under /admin will make things work nicely for us:

security {
	active = true

	useRequestMapDomainClass = false
	requestMapString = """
		CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON
		PATTERN_TYPE_APACHE_ANT

		/admin/**=ROLE_ADMIN
		/hello/**=ROLE_USER
		/**=IS_AUTHENTICATED_ANONYMOUSLY
	"""
}

Just instruct your fellow team members to put the appropriate controllers under the right package and they are instantaneously secured 🙂

One thought on “Separating and securing Grails controllers

Comments are closed.