简体   繁体   中英

grails 2.5 how to validate domain object password with service inside validator

We have a simple operator object, which uses spring security to encode the password thusly:

class Operator
   transient springSecurityService
   def siteService

   String username
   Site   site
   String password

   def beforeInsert() {
        encodePassword()
   }

   def beforeUpdate() {
       if (isDirty('password')) {
           encodePassword()
       }
    }
   protected void encodePassword() {
       password = springSecurityService?.passwordEncoder ? springSecurityService.encodePassword(password) : password
   }

Now we want to validate that the password matches a regexp which is defined at runtime. Operators can be created via the UI (ie standard CRUD forms). We have a different regexp for each site. The fact that the password gets overwritten with an ecoded one, and we should not test that, makes it more challenging.

Attempt 1: do the validation in the encodePassword():

def beforeInsert() {
  protected void encodePassword() {
    String regexp = siteService.getInheritedValue(site, "operatorPasswordRegexp")

     if (!password.matches(regexp) {
         thrown new RuntimeException "invalid password format"
     }

    password = springSecurityService?.passwordEncoder ? springSecurityService.encodePassword(password) : password
  }
}

This partially works, in that it stops passwords which don't match a regexp found at runtime being created. The problem is it throws an exception, which generates a 500 error page, when we want the operator edit form to highlight the password field with a nice friendly validation message.

Attempt two, using a custom validator

static constraints = {
password    password: true, blank:false, validator: { val, obj ->
        if (obj.isDirty(val)) {
           return true
        }
        String regexp = obj.siteService.getInheritedValue(obj.operates, "operatorPasswordRegexp")
        if (regexp != null && regexp != "") {
            return val.matches(regexp)
        }
        return true
    }

This appears to work, but the save always fails silently. It took me some time to realise why - when you do this:

operator.password="valid1"
opertor.save(failonError:true)  

No errors are thrown. Even if you remove failonError, and check the return value,its always null (no errors). BUT IT DOES NOT SAVE THE OPERATOR.

The problem is that the beforeInsert is updating the password to an encoded version which does not pass the validator of course (and isnt supposed to), the validator says no at this point, and the save silently fails. Ie the validiator is being called twice for a single save.

The question is, how to I get the beforeInsert() code to NOT call the validator, or the validator to ignore being called from beforeInsert?

You can achieve your task using both approaches.

1: Do the validation in the encodePassword() : Instead of throwing the exception, add an error to instance. I think your encodePassword() function is in your same domain, so get errors object associated to it using this.errors . ex:

this.errors.rejectValue("password", "user.password.pattern.error")

There are different rejectValue methods, this one accepts the field name and the message code defined in your message.properties file.

2: Custom Validator :

isDirty() is not a static method, call it using the obj provided in the custom validator. isDirty() accepts the property name to be checked for dirtiness not its value.

obj.isDirty(PropertyName)

constraints is a static block, it would not be able to directly access your service. You need to inject your service using static context.

static SiteService siteService;

I would recommend to do it using custom validator.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM