Thursday, October 29, 2009

Grails HowTo: Administer & ReUse Users & Roles in cross-(DB)schema apps w/security

The following design enables the central management of application Roles & Users, by a Grails application, for other Grails applications and implements User authentication via LDAP (authorization is managed by Roles).

The code that follows can not only be used to implement the central CRUD of AppGroups, Apps, Roles & Users but can be repeatedly re-used in each application that desires to take advantage of these AppGroups, Apps, Roles & Users.

To start, create a Grails app from the command-line:


grails create-app AppGroupUserRoleAdmin
cd AppGroupUserRoleAdmin

or Ctrl-Shift-N/Groovy/Grails Application in the NetBeans 6.7 IDE.

Install plugins (note: the jSecurity project has been adopted by Apache and renamed to first Ki and now Shiro; change jsecurity to apache.shiro if using the shiro Grails plugin i.e. grails install-plugin shiro and import org.apache.shiro.authc.AccountException):


grails install-plugin ldap
grails install-plugin jsecurity

In this scheme, Users (of certain Applications) will have Roles (specific to each Application) and Applications will be classified by their Application Group membership. e.g. joe is a User of the Budget application, the Budget application is a member of the Finance application group.

Create some classes to model the security domain i.e. Users, Roles, Applications & Application Groups :


grails create-domain-class whatever.AppUser
grails create-domain-class whatever.AppRole
grails create-domain-class whatever.App
grails create-domain-class whatever.AppGroup

Edit the resulting domain objects like so:


  • AppUser:

package whatever

class AppUser {
/* NOTE:
* Declare the id & version attributes as type: Integer
* or else the Grails default type (Long) will be used.
* Trying to persist Java Long values as JDBC types bigint
* will currently fail as our Ingres DB doesn't support that type (yet).
*/
Integer id
Integer version
String name // used by LDAP authentication
Integer idNumber // possibly used in SQL?
String toString() { name + ":" + idNumber + ":" + roles }
static hasMany = [roles:AppRole]
static constraints = {
name(nullable: false, blank: false, unique: true)
idNumber(min: 1, unique: true)
roles(nullable: false)
}
static mapping = {
table 'gr8_appuser'
roles joinTable: 'gr8_appuser_role'
}
}
  • AppRole


package whatever

class AppRole {
/* NOTE:
* Declare the id & version attributes as type: Integer
* or else the Grails default type (Long) will be used.
* Trying to persist Java Long values as JDBC types bigint
* will currently fail as our Ingres DB doesn't support that type (yet).
*/
Integer id
Integer version
String name
static belongsTo = [app:App]
String toString() { name + ":" + app }
static constraints = {
name(nullable: false, blank: false, unique: true)
}
static mapping = {
table 'gr8_approle'
}
}
  • App

package whatever
class App {
/* NOTE:
* Declare the id & version attributes as type: Integer
* or else the Grails default type (Long) will be used.
* Trying to persist Java Long values as JDBC types bigint
* will currently fail as our Ingres DB doesn't support that type (yet).
*/
Integer id
Integer version
String name
String entryUrl
static belongsTo = [appGroup:AppGroup]
static hasMany = [roles:AppRole]

String toString() { name + ":" + entryUrl }
static constraints = {
name(nullable: false, blank: false, unique: true)
entryUrl(url: true, nullable: false, blank: false)
}
static mapping = {
table 'gr8_app'
}
}
  • AppGroup

package whatever

class AppGroup {
/* NOTE:
* Declare the id & version attributes as type: Integer
* or else the Grails default type (Long) will be used.
* Trying to persist Java Long values as JDBC types bigint
* will currently fail as our Ingres DB doesn't support that type (yet).
*/
Integer id
Integer version
String name
String toString() { name }
static hasMany = [apps:App]
static constraints = {
name(nullable: false, blank: false, unique: true)
}
static mapping = {
table 'gr8_appgroup'
}
}

The above security-centric objects will be persisted to a DB schema from which we will grant select rights to other DB schemas; this will allow for their (read-only) re-use by schema-specific applications.

At this point you should be able to run the project; this will create the tables via GORM/Hibernate.

Modify the Grails' BootStrap class file to auto-insert some records into the DB so that we can login upon startup (Note: If you're re-using this code in an application other than the global one you may not want to auto-create Roles & Users upon application startup, in which case you can skip this step):


import org.codehaus.groovy.grails.commons.ConfigurationHolder as CH
import groovy.sql.Sql
import javax.sql.DataSource

class BootStrap {

def DataSource dataSource

def init = { servletContext ->
def sql = new Sql(dataSource)
def version = 0

// User
def int userId = 0
def Integer adminIdNumber = CH.config.adminUserIdNumber
def String userName = CH.config.adminUserName
sql.execute("insert into gr8_appuser (id, version, id_number, name) values (${userId}, ${version}, ${adminIdNumber}, ${userName})")
// App group

def int groupId = -2
def String groupName = CH.config.adminAppGroupName
sql.execute("insert into gr8_appgroup (id, version, name) values (${groupId}, ${version}, ${groupName})")

// App
def int appId = -3
def String appName = CH.config.applicationName
def String entryUrl = CH.config.entryUrl
sql.execute("insert into gr8_app (id, version, app_group_id, entry_url, name) values (${appId}, ${version}, ${groupId}, ${entryUrl}, ${appName})")

// Role
def int roleId = -4
def String roleName = CH.config.adminRoleDescr
sql.execute("insert into gr8_approle (id, version, app_id, name) values (${roleId}, ${version}, ${appId}, ${roleName})")

// User/Role r'ship
sql.execute("insert into gr8_appuser_role (app_user_roles_id, app_role_id) values (${userId}, ${roleId})")
}


def destroy = {
}
}


We'll put the ID of the Administrator of this new User/Role/App/Group application we're creating into Grails' config file conf\Config.groovy, this User will be able to do CRUD for all the applications we'll build in the future; just add add a snippet to the (v 1.1.1) environments group i.e. so it looks like this after you're done (only add the applicationName line if you're re-using this code in a subsequent application):


environments {
production {
grails.serverURL = "http://www.changeme.com"
}
development {
grails.serverURL = "http://localhost:8080/${appName}"applicationName = "${appName}"
adminAppGroupName = "Admin Applications"
adminRoleDescr = "Administrator"
adminUserIdNumber = 666
adminUserName = "safe"
entryUrl = "http://localhost/${appName}"

Adjust the User & URL to taste, the User will need to be authenticated by LDAP.

As mentioned in previous blog postings, now we need to create a (jSecurity) realm for the purposes of User authentication i.e. AuthRealm:


package whatever

import javax.naming.AuthenticationException
import javax.naming.Context
import javax.naming.NamingException
import javax.naming.directory.BasicAttribute
import javax.naming.directory.BasicAttributes
import javax.naming.directory.InitialDirContext
import org.jsecurity.authc.AccountException
import org.jsecurity.authc.CredentialsException
import org.jsecurity.authc.IncorrectCredentialsException
import org.jsecurity.authc.UnknownAccountException
import org.codehaus.groovy.grails.commons.ConfigurationHolder as CH

/**
* Simple realm that:
* - authenticates users against an LDAP server
* - authorizes users against a DB.
*/
class AuthRealm {
static authTokenClass = org.jsecurity.authc.UsernamePasswordToken
def grailsApplication
def authenticate(authToken) {
if (authToken.username && authToken.password) {
List matches = getEntriesByCommonName(authToken.username)
if (matches && matches.size() == 1) {
LdapUserEntity user = getEntry(matches)
if (user.authenticate("" + authToken.password)) {
return authToken.username
} else {
java.lang.Thread.sleep(5*1000) // Wait for LDAP to reflect ACCOUNT LOCKOUT
user = getEntry(getEntriesByCommonName(authToken.username))
if ("TRUE".equals(user.lockedByIntruder)) {
throw new AccountException("The account is locked")
} else {
throw new IncorrectCredentialsException("Invalid password for user '${authToken.username}'")
}
}
} else {
throw new UnknownAccountException("No account found for user [${authToken.username}]")
}
}
}


def private List getEntriesByCommonName(String id) {
return GldapoSchemaClassForUser.findAll( filter: "(cn=" + id + ")" )
}

def private LdapUserEntity getEntry(List entries) {
if (entries && entries.size() == 1) {
return entries[0]
}
}

def hasRole(principal, roleName) {
def user = JsecUser.findByName(principal, [fetch:[roles:'join']])
if (user) {

return user.roles.any{
it.name == roleName &&
it.app.name == CH.config.applicationName
}
} else {
return false
}

}

def hasAllRoles(principal, roles) {
def user = JsecUser.findByName(principal, [fetch:[roles:'join']])
if (user) {
return user.roles.all {
it.name == roleName &&
it.app.name == CH.config.applicationName
}
} else {
return false
}
}
}

Create a class that will model the LDAP properties of Users i.e. .\grails-app\utils\LdapUserEntity:

package whatever

import gldapo.schema.annotation.GldapoNamingAttribute
import gldapo.schema.annotation.GldapoSynonymFor
import gldapo.schema.annotation.GldapoSchemaFilter

@GldapoSchemaFilter("(objectclass=person)")
class LdapUserEntity {

@GldapoNamingAttribute
@GldapoSynonymFor("cn")
String name

@GldapoSynonymFor("mail")
String email

@GldapoSynonymFor("uid")
String username

@GldapoSynonymFor("fullname")
String fullName

//@GldapoSynonymFor("pwdFailureTime")
//String passwordFailureTime
// it's an operational attribute, not sure how/what Groovy type to map it to

@GldapoSynonymFor("passwordExpirationTime")
String passwordExpirationTime

@GldapoSynonymFor("loginIntruderResetTime")
String loginIntruderResetTime

@GldapoSynonymFor("loginIntruderAttempts")
String loginIntruderAttempts

//@GldapoSynonymFor("loginIntruderAddress")
//String loginIntruderAddress
// it's a binary attribute, not sure how/what Groovy type to map it to

@GldapoSynonymFor("loginIntruderGraceLimit")
String loginIntruderGraceLimit

@GldapoSynonymFor("loginIntruderGraceRemaining")
String loginIntruderGraceRemaining

@GldapoSynonymFor("loginIntruderLimit")
String loginIntruderLimit

@GldapoSynonymFor("lockedByIntruder")
String lockedByIntruder
}

Create a controller i.e. AuthController; only Admins can login to this app, subsequent uses of this pattern by user-facing applications would likely only allow a Role e.g. User to login:


package whatever

import org.jsecurity.authc.AuthenticationException
import org.jsecurity.authc.UsernamePasswordToken
import org.jsecurity.SecurityUtils

class AuthController {

def jsecSecurityManager

def index = { redirect(action: 'login', params: params) }

def login = {
return [ username: params.username,
rememberMe: (params.rememberMe != null),
targetUri: params.targetUri
]
}

def signIn = {
def authToken = new UsernamePasswordToken(params.username, params.password)
if (params.rememberMe) {
authToken.rememberMe = true
}
try {
def subject = jsecSecurityManager.login(authToken)
if (subject.authenticated) {
if (jsecSecurityManager.hasRole(subject.getPrincipals(), "Administrator")) {
session.user = subject.principal
} else {
session.user = null
throw new AuthenticationException("No account found")
}
}
else {
session.user = null
throw new AuthenticationException("No account found")
}
def targetUri = params.targetUri ?: "/"
log.info "Redirecting to '${targetUri}'."
redirect(uri: targetUri)
}
catch (AuthenticationException ex){
// Authentication failed, so display the appropriate message
// on the login page.
log.info "Authentication failure for user '${params.username}'."
if (message(code: "account.locked").contains(ex.getMessage())) {
flash.message = message(code: "account.locked")
} else if (message(code: "account.unknown").contains(ex.getMessage())) {
flash.message = message(code: "login.failed")
}else {
flash.message = message(code: "login.failed")
}
// Keep the username and "remember me" setting so that the
// user doesn't have to enter them again.
def m = [ username: params.username ]
if (params.rememberMe) {
m['rememberMe'] = true
}
// Remember the target URI too.
if (params.targetUri) {
m['targetUri'] = params.targetUri
}
// Now redirect back to the login page.
redirect(action: 'login', params: m)
}
}
def signOut = {
// Log the user out of the application.
SecurityUtils.subject?.logout()
// For now, redirect back to the home page.
redirect(uri: '/')
}
def unauthorized = {
render 'You do not have permission to access this page.'
}
}

Create a page to login from i.e. views\auth\login.gsp:


<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="layout" content="main" />
<title>Login</title>
</head>
<body>
<g:if test="${flash.message}">
<div class="message">${flash.message}</div>
</g:if>
<g:form action="signIn">
<input type="hidden" name="targetUri" value="${targetUri}" />
<table>
<tbody>
<tr>
<td>Username:</td>
<td><input type="text" name="username" value="${username}" /></td>
</tr>
<tr>
<td>Password:</td>
<td><input type="password" name="password" value="" /></td>
</tr>
<tr>
<td>Remember me?:</td>
<td><g:checkBox name="rememberMe" value="${rememberMe}" /></td>
</tr>
<tr>
<td />
<td><input type="submit" value="Sign in" /></td>
</tr>
</tbody>
</table>
</g:form>
</body>
</html>

Don't forget to add an LDAP section to conf\Config.groovy or authentication will fail:



ldap {
directories {
directory1 {
defaultDirectory = true
url = "ldap://ldap.host.org"
base = "ou=otherUnit,o=org"
userDn = "cn=adminID,ou=unit,o=org"
password = "password"
searchControls {
countLimit = 40
timeLimit = 600
searchScope = "subtree"
}
}
}
schemas = [
LdapUserEntity
]
}

Adjust the LDAP location & credentials to taste.

Create some controllers for the domain objects:


grails create-controller whatever.App
grails create-controller whatever.AppGroup
grails create-controller whatever.AppRole
grails create-controller whatever.AppUser

Edit each controller to take advantage of Grails' scaffolding e.g.


package whatever

class AppUserController {

def scaffold = AppUser


}

Create a filter (e.g. conf\SecurityFilters.groovy) to secure the applications URLs:


class SecurityFilters {

def filters = {
loginCheck(controller: "*", action: "*") {
before = {
if (!session.user && actionName != 'login' &&
actionName != 'signIn')
{
redirect(controller:'auth', action:'login')
return false
}
}
}
}

}

Run the app (e.g. grails run-app)

Login with LDAPish credentials

Create AppGroups, Apps, Roles & Users for the applications you intend to build.


Re-use the above setup (minus the last 4 controllers, since you'll be using those objects in a read-only state anyway and won't need to do CRUD on them) for the applications you build in future and they'll be LDAP-authenticated and the Roles & Users will be centrally managed.


TODO:



  • Introduce the finer-grained control allowed by Permissions

  • Illustrate the use of jSecurity's GSP tags in markup pages

  • Refactor SQL code in BootStrap into a Service class, see: http://grails.org/doc/1.1.x/guide/single.html#8.3 Dependency Injection and Services

Issue a SQL GRANT command to expose the DB tables to other applications i.e. other DB schemas e.g. :


grant select on gr8_appuser to public
grant select on gr8_appuser_role to public
grant select on gr8_approle to public
grant select on gr8_app to public
grant select on gr8_appgroup to public

No comments: