Grails MongoDB Dirty Checking Fails With Spring Security

238 views Asked by At

I'm using Grails 3.3.2 with the mongoDB plugin (v6.1.4) and Spring Security Core plugin (v3.2.0).

I have the following UserPasswordEncoderListenerwith the following persistenceEvent method:

 @Override
protected void onPersistenceEvent(AbstractPersistenceEvent event) {
    if (event.entityObject instanceof User) {
        User u = (event.entityObject as User)
        if (u.password && (event.eventType == EventType.PreInsert || (event.eventType == EventType.PreUpdate && u.hasChanged('password')))) {
            event.getEntityAccess().setProperty("password", encodePassword(u.password))
        }
    }
}

The problem is the hasChanged call always return true every time I save a user object that has NO updates causing an already encoded password to be re-encoded and thereby breaking authentication.

One workaround will be to do it the old way and just retrieve the original password from the db and compare them before encoding but I'm wondering why hasChanged is falsely returning true.

I tested that hasChanged behaves correctly elsewhere by running the following in the groovy console:

def user = User.findByEmail("[email protected]")
println "Result: "+ user.hasChanged('password')

The result is Result: false. Why does it NOT work in the persistence listener class?

FYI: I have the following bean defined in resources.groovy:

userPasswordEncoderListener(UserPasswordEncoderListener,ref('mongoDatastore'))
2

There are 2 answers

0
ddelponte On BEST ANSWER

Have you tried using isDirty() rather than hasChanged() in your listener?

For example:

package com.mycompany.myapp

import grails.plugin.springsecurity.SpringSecurityService
import org.grails.datastore.mapping.engine.event.AbstractPersistenceEvent
import org.grails.datastore.mapping.engine.event.PreInsertEvent
import org.grails.datastore.mapping.engine.event.PreUpdateEvent
import org.springframework.beans.factory.annotation.Autowired
import grails.events.annotation.gorm.Listener
import groovy.transform.CompileStatic

@CompileStatic
class UserPasswordEncoderListener {

    @Autowired
    SpringSecurityService springSecurityService

    @Listener(User)
    void onPreInsertEvent(PreInsertEvent event) {
        encodePasswordForEvent(event)
    }

    @Listener(User)
    void onPreUpdateEvent(PreUpdateEvent event) {
        encodePasswordForEvent(event)
    }

    private void encodePasswordForEvent(AbstractPersistenceEvent event) {
        if (event.entityObject instanceof User) {
            User u = event.entityObject as User
            if (u.password && ((event instanceof  PreInsertEvent) || (event instanceof PreUpdateEvent && u.isDirty('password')))) {
                event.getEntityAccess().setProperty('password', encodePassword(u.password))
            }
        }
    }

    private String encodePassword(String password) {
        springSecurityService?.passwordEncoder ? springSecurityService.encodePassword(password) : password
    }
}

More info is available at: https://grails-plugins.github.io/grails-spring-security-core/3.2.x/index.html#tutorials

0
Emmanuel John On

Temp workaround for now:

@Override
protected void onPersistenceEvent(AbstractPersistenceEvent event) {
    if (event.entityObject instanceof User) {
        User u = (event.entityObject as User)
        if (u.password && (event.eventType == EventType.PreInsert || (event.eventType == EventType.PreUpdate && u.hasChanged('password')))) {
            if(event.eventType == EventType.PreUpdate){ //Temp workaround until hasChanged behaves correctly
                def originalUser = User.get(u?.id)
                if(originalUser.password != u.password){
                    event.getEntityAccess().setProperty("password", encodePassword(u.password))
                }
            }else {
                event.getEntityAccess().setProperty("password", encodePassword(u.password))
            }

        }
    }
}

Left an issue on the github repo (https://github.com/grails-plugins/grails-spring-security-core/issues/539). Will update with feedback.