- A (graphic/mathematic/ally beautiful) theory of everything (physical)
- Symmetry from revolutionaries
- GWT, as used in Google Wave
- WebDriver, also used by GWave developers
- A Groovy DSL for WebDriver
- GWave API
- Grails' plugin for GWave ((clients only?), with samples
- Programming with C Blocks on Apple Devices
- Mike Ash intros. closures in Obj-C
- Cocoa for Scientists (Part XXVII): Getting Closure with Objective-C
Friday, October 30, 2009
Wandering all over the road today
Thursday, October 29, 2009
Writing a Java applet in Grails
- Install the plugin:
grails install-plugin applet
- see the reference, if you like
- As per the reference, self-sign a certificate for yourself with these 2 command-line steps:
keytool -genkeypair -alias <alias> -storepass <password>
keytool -selfcert -alias <alias> -storepass <password>
- where <alias> & <password> are values that you choose
- this resulted in a .keystore file appearing in my home directory
- where <alias> & <password> are values that you choose
- Add this chunk to your conf/Config.groovy file:
-
plugins { applet { //groovyJar = 'C:/groovy/groovy-1.6.0/embeddable/groovy-all-1.6.0.jar' jars { '/testApplet.jar' { // leading '/' is optional **didn't work for me until I added it though** groovy = false; // set to false to skip including groovy-all-* in the jar classes= ['ca.upei.cs.avc.applet.*.class', 'lib/classes111.jar', 'lib/LabDataViewer.jar', 'lib/ResultsLib.jar'] sign { alias = 'alias' //your alias in keystore storepass = 'password'//alternatively, use a system property with -Dstorepass=password } pack200 = true } } } - Above, the classes value contains the Java class name of my applet (to be written) and some JARs it needs at compile or runtime. Any number of applets can be listed in the jars stanza. Notice, you can reference the standard Grails lib directory so you don't need to repeat yourself i.e. copy them somewhere special for the applet(s).
-
- My applet (i.e. src/java/whatever/applet/LabDataViewerApplet.java ) looks like this:
-
package whatever.applet; import javax.swing.JApplet; import java.sql.*; import oracle.jdbc.driver.OracleDriver; import upei.avc.ds.LabDataViewer; public class LabDataViewerApplet extends JApplet { int year = 1899; int labNumber = -1; private Connection con; public LabDataViewerApplet() {} public void init() { if (null != this.getParameter("labNumber")) { labNumber = Integer.parseInt(this.getParameter("labNumber")); } if (null != this.getParameter("year")) { year = Integer.parseInt(this.getParameter("year")); } try { jbInit(); } catch(Exception e) { e.printStackTrace(); } } private void jbInit() throws Exception { this.getContentPane().setLayout(null); DriverManager.registerDriver(new oracle.jdbc.driver.OracleDriver()); con = DriverManager.getConnection( "jdbc:oracle:thin:@host:port:database", "id","password"); LabDataViewer viewer = new LabDataViewer(labNumber, year, con); this.getContentPane().add(viewer, null); } } - This applet will get a JDBC Connection to an Oracle instance and instantiate & attach Robert Page's LabDataViewer (Java Swing) app to it's contentPane; the (applet) containing page will pass in parameters to the viewer to customize the data/view.
- You compile & package the applet by running this command:
grails package-applet
- This should result in the file testApplet.jar being placed in your web directory e.g.
C:\projx\grails_apps\testApplet>grails package-applet Welcome to Grails 1.1.1 - http://grails.org/ Licensed under Apache Standard License 2.0 Grails home is set to: C:\tools\grails\grails-1.1.1 Base Directory: C:\projx\grails_apps\testApplet Running script C:\Documents and Settings\default\.grails\1.1.1\projects\testApplet\plugins\applet-0.1\scripts\ PackageApplet.groovy Environment set to development [groovyc] Compiling 1 source file to C:\Documents and Settings\default\.grails\1.1.1\projects\testApplet\classes [echo] Building web-app//testApplet.jar with the following Autojar arguments: [-o, web-app//testApplet.jar, -c, C:\Documents and Settings\default\.grails\1.1.1\projects\testApplet\classes, ca.upei.cs.avc.applet.*.class, lib/classes111.jar, lib/LabDataViewer.jar, lib/ResultsLib.jar] 2009-10-20 16:00:33,299 [main] WARN root - Missing files: com/keyoti/rapidSpell/desktop/RapidSpellAsYouType.class org/jdesktop/layout/GroupLayout$Group.class org/jdesktop/layout/GroupLayout$ParallelGroup.class org/jdesktop/layout/GroupLayout$SequentialGroup.class org/jdesktop/layout/GroupLayout.class upei/avc/oracle/OraOCIConn.class [echo] pack200 is enabled, packing jar... [echo] Signing web-app//testApplet.jar as alias: alias [signjar] Signing JAR: C:\projx\grails_apps\testApplet\web-app\testApplet.jar to C:\projx\grails_apps\testAp plet\web-app\testApplet.jar as alias [signjar] Warning: [signjar] The signer certificate will expire within six months. [signjar] Enter Passphrase for keystore: C:\projx\grails_apps\testApplet>- Don't be concerned with the reported missing files above; in this case they were never used by the Swing application
- AutoJar (used by the plugin) uses reflection to find the class & jar files required by the applet
-
- The applet-containing webpage looks like this; any number of parameters can be passed into the applet (they're passed as Strings):
<html> <head> <script src="http://java.com/js/deployJava.js"></script> </head> <body> <script> var attributes = { name:'myApplet', codebase:'.', code:'whatever.applet.LabDataViewerApplet.class', archive:'testApplet.jar', width:500, height:500 } ; var parameters = {fontSize:16, labNumber: 6, year: 2006}; var version = '1.6' ; deployJava.runApplet(attributes, parameters, version); </script> </body> </html>- The resulting page appears blank (except for a 500x500 grey area I reserved for the applet) and the LabDataViewer launches in it's own window, closing the webpage kills the applet/application.
- Brought to you thanks to AutoJar, deployJava.js and the Grails' Applet plugin!
Some day...
...I'd like to get Single-Signon working with Grails & jSecurity but these links indicate it might be slow going:
- http://n2.nabble.com/SSO-with-Grails-JSecurity-Plugin-td1334030.html#a1334030
- http://www.nabble.com/RE%3A-SSO-with-Grails-JSecurity-Plugin-p20035703.html
- http://www.nabble.com/JSecurity----custom-SSO-implementation-ts20178301.html#a20178301
- http://www.nabble.com/SSO-with-Grails-JSecurity-Plugin-td19996414.html
- http://tramuntanal.wikidot.com/jsecurityplugin
- http://asrijaffar.blogspot.com/2008/08/grails-jsecurity-plugin.html
- http://skillsmatter.com/podcast/java-jee/exploring-the-power-of-jsecurity-in-grails
Grails application base: Nimble
Looks promising and not unlike what I've recently been hacking together.
Debugging Grails from NetBean v6.7
- Start Grails in debug mode from the command-line : grails-debug run-app
- In NetBeans
- Set a breakpoint
- Select Attach Debugger... from the Debug menu
- Type the value 5005 into the port field of the ensuing dialog
- Press OK
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
Using 2 Grails plugins to authenticate your applications' users
Persuant to my last blog post (Talking to LDAP from Grails) here is a follow-on post detailing the ridiculously easy process of using Grails' jSecurity & LDAP plugins to authenticate your application users. This example assumes your application will hand-off user authentication to LDAP and will handle authorization via filesystem or (more likely) database storage i.e. only store user IDs in your application's datastore, no passwords (yay!).
To begin:
- create a grails app (e.g. grails create-app myApp
- cd into the app's directory (e.g. cd myApp)
- install the plugins e.g.
- grails install-plugin ldap
- grails install-plugin jsecurity
- install a jSecurity LDAP realm e.g.
- grails create-ldap-realm
- install a jSecurity authenticating controller (i.e. the C in MVC, a servlet) e.g.
- grails create-auth-controller
- configure the LDAP plugin as per my previous post
As a result of the above steps you will now have the following additional/ or modified files in your Grails app's project tree:
- grails-app
- conf
- Config.groovy
- controllers
- AuthController.groovy
- realms
- JsecLdapRealm.groovy
- utils
- GldapoSchemaClassForUser.groovy
- views
- JsecLdapRealm.groovy
Now, the jSecurity script create-ldap-realm created the file JsecLdapRealm.groovy.
The line:
static authTokenClass = org.jsecurity.authc.UsernamePasswordTokensignals jSecurity that this realm will participate in authenticating users; in fact, the authenticate() method is where this work is implemented (surprise!).
By default the authenticate() method will have the typical Java/LDAP boilerplate code within it i.e.
def authenticate(authToken) {
log.info "Attempting to authenticate ${authToken.username} in LDAP realm..."
def username = authToken.username
def password = new String(authToken.password)
// Get LDAP config for application. Use defaults when no config
// is provided.
def appConfig = grailsApplication.config
def ldapUrls = appConfig.ldap.server.url ?: [ "ldap://localhost:389/" ]
def searchBase = appConfig.ldap.search.base ?: ""
def searchUser = appConfig.ldap.search.user ?: ""
def searchPass = appConfig.ldap.search.pass ?: ""
def usernameAttribute = appConfig.ldap.username.attribute ?: "uid"
def skipAuthc = appConfig.ldap.skip.authentication ?: false
def skipCredChk = appConfig.ldap.skip.credentialsCheck ?: false
def allowEmptyPass = appConfig.ldap.allowEmptyPasswords != [:] ? appConfig.ldap.allowEmptyPasswords : true
// Skip authentication ?
if (skipAuthc) {
log.info "Skipping authentication in development mode."
return username
}
// Null username is invalid
if (username == null) {
throw new AccountException("Null usernames are not allowed by this realm.")
}
// Empty username is invalid
if (username == "") {
throw new AccountException("Empty usernames are not allowed by this realm.")
}
// Allow empty passwords ?
if (!allowEmptyPass) {
// Null password is invalid
if (password == null) {
throw new CredentialsException("Null password are not allowed by this realm.")
}
// empty password is invalid
if (password == "") {
throw new CredentialsException("Empty passwords are not allowed by this realm.")
}
}
// Accept strings and GStrings for convenience, but convert to
// a list.
if (ldapUrls && !(ldapUrls instanceof Collection)) {
ldapUrls = [ ldapUrls ]
}
// Set up the configuration for the LDAP search we are about
// to do.
def env = new Hashtable()
env[Context.INITIAL_CONTEXT_FACTORY] = "com.sun.jndi.ldap.LdapCtxFactory"
if (searchUser) {
// Non-anonymous access for the search.
env[Context.SECURITY_AUTHENTICATION] = "simple"
env[Context.SECURITY_PRINCIPAL] = searchUser
env[Context.SECURITY_CREDENTIALS] = searchPass
}
// Find an LDAP server that we can connect to.
def ctx
def urlUsed = ldapUrls.find { url ->
log.info "Trying LDAP server ${url} ..."
env[Context.PROVIDER_URL] = url
// If an exception occurs, log it.
try {
ctx = new InitialDirContext(env)
return true
}
catch (NamingException e) {
log.error "Could not connect to ${url}: ${e}"
return false
}
}
if (!urlUsed) {
def msg = 'No LDAP server available.'
log.error msg
throw new org.jsecurity.authc.AuthenticationException(msg)
}
// Look up the DN for the LDAP entry that has a 'uid' value
// matching the given username.
def matchAttrs = new BasicAttributes(true)
matchAttrs.put(new BasicAttribute(usernameAttribute, username))
def result = ctx.search(searchBase, matchAttrs)
if (!result.hasMore()) {
throw new UnknownAccountException("No account found for user [${username}]")
}
// Skip credentials check ?
if (skipCredChk) {
log.info "Skipping credentials check in development mode."
return username
}
// Now connect to the LDAP server again, but this time use
// authentication with the principal associated with the given
// username.
def searchResult = result.next()
env[Context.SECURITY_AUTHENTICATION] = "simple"
env[Context.SECURITY_PRINCIPAL] = searchResult.nameInNamespace
env[Context.SECURITY_CREDENTIALS] = password
try {
new InitialDirContext(env)
return username
}
catch (AuthenticationException ex) {
log.info "Invalid password"
throw new IncorrectCredentialsException("Invalid password for user '${username}'")
}
}
Thankfully, because we're using GroovyLDAPObjects, this can be reduced to:
def authenticate(authToken) {
if (authToken.username && authToken.password) {
List matches = getEntriesByCommonName(authToken.username)
if (matches && matches.size() == 1) {
GldapoSchemaClassForUser 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 GldapoSchemaClassForUser getEntry(List entries) {
if (entries && entries.size() == 1) {
return entries[0]
}
}
What the above code is doing is:
- if you passed me an ID & password then
- lookup the ID in LDAP
- iff there is 1 matching entry in LDAP then
- get that entry in the form of my GLDAPO Schema object
- try authenticating to LDAP as that user
- if authenticated then
- return the user name and we're done
- else
- wait 5 seconds
- check LDAP for the same user
- if the account is now locked out because of bad passwords then
- throw an account-locked exception
- else
- throw a login-failure exception
- if the account is now locked out because of bad passwords then
- else throw an account-not-found exception
- conf
Much briefer and easier to grok 8 months after you wrote it...
The only modification I made to the controller was to distinguish between a password failure and an account lockout so as to inform the user appropriately:
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 {
flash.message = message(code: "login.failed")
}
and I modified the grails-app/i18n/jsecurity.properties message bundle to hold the message-to-the-user
account.locked = The account is locked, please try again later
All that remains to be done is to authorize the user e.g. do a DB check for the user ID, etc.
Finally, implement your view & business logic (only!) using jSecurity to restrict the views & business logic services to users with the appropriate roles and/or permissions. Did I blog that yet?