Skip to content

Commit 4eda005

Browse files
Allow whitelist of properties to be used instead of a ignore list
- Backport from #138 to master for 2.x plugin version
1 parent edc10fa commit 4eda005

File tree

4 files changed

+196
-19
lines changed

4 files changed

+196
-19
lines changed

audit-logging/src/main/groovy/grails/plugins/orm/auditable/AuditLogListener.groovy

Lines changed: 55 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
package grails.plugins.orm.auditable
2020

2121
import com.fasterxml.jackson.annotation.JsonIgnore
22+
import grails.compiler.GrailsCompileStatic
2223
import grails.core.GrailsDomainClass
2324
import grails.util.GrailsClassUtils
2425
import groovy.util.logging.Commons
@@ -32,7 +33,6 @@ import org.springframework.context.ApplicationEvent
3233
import org.springframework.web.context.request.RequestContextHolder
3334

3435
import static grails.plugins.orm.auditable.AuditLogListenerUtil.*
35-
3636
/**
3737
* Grails interceptor for logging saves, updates, deletes and acting on
3838
* individual properties changes and delegating calls back to the Domain Class
@@ -86,6 +86,7 @@ class AuditLogListener extends AbstractPersistenceEventListener {
8686
}
8787

8888
@Override
89+
@GrailsCompileStatic
8990
protected void onPersistenceEvent(AbstractPersistenceEvent event) {
9091
// GPAUDITLOGGING-64: Even we register AuditLogListeners per datasource, at least up to Grails 2.4.2 events for other datasources
9192
// get triggered in all other listeners.
@@ -113,6 +114,7 @@ class AuditLogListener extends AbstractPersistenceEventListener {
113114
}
114115
}
115116

117+
@GrailsCompileStatic
116118
void stamp(AbstractPersistenceEvent event) {
117119
def entity = event.entityObject
118120
def actor = getActor()
@@ -127,8 +129,8 @@ class AuditLogListener extends AbstractPersistenceEventListener {
127129
stampLastUpdatedBy(entity,actor,event)
128130
}
129131
}
130-
131-
void stampTimestamp(AbstractPersistenceEvent event,def entity){
132+
133+
void stampTimestamp(AbstractPersistenceEvent event, def entity){
132134
String dateCreatedProperty = GrailsClassUtils.getStaticPropertyValue(entity.class,'_dateCreatedStampableProperty')
133135
Class<?> dateCreatedType = GrailsClassUtils.getPropertyType(entity.class,dateCreatedProperty)
134136
def timestamp = timestampProvider.createTimestamp(dateCreatedType)
@@ -281,6 +283,30 @@ class AuditLogListener extends AbstractPersistenceEventListener {
281283
return ignore
282284
}
283285

286+
287+
/**
288+
* Get the list of auditable properties. This is used to override
289+
* the default behaviour of auditing all properties except those in the
290+
* ignore list.
291+
*
292+
* static auditable = [auditableProperties: ['dateCreated','lastUpdated','myField']]
293+
*
294+
*
295+
*/
296+
List auditableProperties(domain) {
297+
298+
Map auditableMap = getAuditableMap(domain)
299+
if (auditableMap?.containsKey('auditableProperties')) {
300+
log.debug "Found auditableProperty list on this entity ${domain.class.name}"
301+
def list = auditableMap['auditableProperties']
302+
if (list instanceof List) {
303+
return list
304+
}
305+
}
306+
307+
null
308+
}
309+
284310
/**
285311
* The default properties to mask list is: ['password']
286312
* if you want to provide your own mask list, specify in the DomainClass:
@@ -523,14 +549,11 @@ class AuditLogListener extends AbstractPersistenceEventListener {
523549
* to provide a list of fields for onChange to ignore.
524550
*/
525551
protected boolean significantChange(domain, Map oldMap, Map newMap) {
552+
def auditableProperties = auditableProperties(domain)
526553
def ignore = ignoreList(domain)
527-
ignore?.each { String key ->
528-
oldMap.remove(key)
529-
newMap.remove(key)
530-
}
531554
boolean changed = false
532555
oldMap.each { String k, Object v ->
533-
if (v != newMap[k]) {
556+
if (isPropertyAuditable(k, auditableProperties, ignore) && v != newMap[k]) {
534557
changed = true
535558
}
536559
}
@@ -571,12 +594,13 @@ class AuditLogListener extends AbstractPersistenceEventListener {
571594
newMap?.remove('version')
572595
oldMap?.remove('version')
573596

597+
List auditableProperties = auditableProperties(domain)
574598
List ignoreList = ignoreList(domain)
575599

576600
if (newMap && oldMap) {
577601
log.trace "There are new and old values to log"
578602
newMap.each { String key, val ->
579-
if (key in ignoreList) {
603+
if (!isPropertyAuditable(key, auditableProperties, ignoreList)) {
580604
return
581605
}
582606
if (val != oldMap[key]) {
@@ -598,7 +622,7 @@ class AuditLogListener extends AbstractPersistenceEventListener {
598622
if (newMap && verbose && !AuditLogListenerThreadLocal.auditLogNonVerbose) {
599623
log.trace "there are new values and logging is verbose ... "
600624
newMap.each { String key, val ->
601-
if (key in ignoreList) {
625+
if (!isPropertyAuditable(key, auditableProperties, ignoreList)) {
602626
return
603627
}
604628
def audit = getAuditLogDomainInstance(
@@ -618,7 +642,7 @@ class AuditLogListener extends AbstractPersistenceEventListener {
618642
if (oldMap && verbose && !AuditLogListenerThreadLocal.auditLogNonVerbose) {
619643
log.trace "there is only an old map of values available and logging is set to verbose... "
620644
oldMap.each { String key, val ->
621-
if (key in ignoreList) {
645+
if (!isPropertyAuditable(key, auditableProperties, ignoreList)) {
622646
return
623647
}
624648
def audit = getAuditLogDomainInstance(
@@ -822,4 +846,24 @@ class AuditLogListener extends AbstractPersistenceEventListener {
822846
AuditLogListenerThreadLocal.clearAuditLogDisabled()
823847
}
824848
}
849+
850+
/**
851+
* Returns a boolean indicating if the given property should be audited or ignored.
852+
* If there is an auditableProperties list the property is auditable if it is in that
853+
* list. Otherwise the property is auditable if is is not in the ignoreList.
854+
* @param fieldName The field name
855+
* @param auditableProperties List of auditable Properties
856+
* @param ignoreList The ignoreList
857+
* @return true if property is auditable
858+
*/
859+
boolean isPropertyAuditable(def fieldName, List auditableProperties, List ignoreList) {
860+
if (auditableProperties) {
861+
return fieldName in auditableProperties
862+
} else if (ignoreList) {
863+
return !(fieldName in ignoreList)
864+
}
865+
866+
true
867+
}
868+
825869
}

audit-test/src/integration-test/groovy/test/AuditDeleteSpec.groovy

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,20 @@ package test
2020

2121
import grails.plugins.orm.auditable.AuditLoggingConfigUtils
2222
import grails.test.mixin.integration.Integration
23-
import grails.transaction.*
24-
import spock.lang.*
23+
import grails.transaction.Rollback
24+
import spock.lang.Shared
25+
import spock.lang.Specification
2526

2627
@Integration
2728
@Rollback
2829
class AuditDeleteSpec extends Specification {
2930

30-
def defaultIgnoreList = ['id'] + AuditLoggingConfigUtils.auditConfig.defaultIgnore?.asImmutable() ?: []
31+
@Shared
32+
def defaultIgnoreList
33+
34+
void setup() {
35+
defaultIgnoreList = ['id'] + AuditLoggingConfigUtils.auditConfig.defaultIgnore?.asImmutable() ?: []
36+
}
3137

3238
void setupData() {
3339
Author.auditable = true
@@ -187,5 +193,44 @@ class AuditDeleteSpec extends Specification {
187193
}
188194
}
189195

196+
void "Test auditableProperties"() {
197+
given:
198+
setupData()
199+
Author.auditable = [auditableProperties: ['famous', 'age', 'dateCreated']]
200+
def author = Author.findByName("Aaron")
201+
202+
when:
203+
author.delete(flush: true, failOnError: true)
204+
205+
then: "only properties in auditableProperties are logged"
206+
def events = AuditTrail.findAllByClassName('test.Author')
207+
208+
events.size() == 3
209+
['famous', 'age', 'dateCreated'].each { name ->
210+
assert events.find {it.propertyName == name}, "${name} was not logged"
211+
}
212+
}
213+
214+
void "Test auditableProperties overrides ignore list"() {
215+
given:
216+
setupData()
217+
Author.auditable = [
218+
auditableProperties: ['famous', 'age', 'dateCreated'],
219+
ignore: ['famous', 'age']
220+
]
221+
def author = Author.findByName("Aaron")
222+
223+
when:
224+
author.delete(flush: true, failOnError: true)
225+
226+
then: "only properties in auditableProperties are logged"
227+
def events = AuditTrail.findAllByClassName('test.Author')
228+
229+
events.size() == 3
230+
['famous', 'age', 'dateCreated'].each { name ->
231+
assert events.find {it.propertyName == name}, "${name} was not logged"
232+
}
233+
}
234+
190235
}
191236

audit-test/src/integration-test/groovy/test/AuditInsertSpec.groovy

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,20 +20,22 @@ package test
2020

2121
import grails.plugins.orm.auditable.AuditLogListener
2222
import grails.plugins.orm.auditable.AuditLoggingConfigUtils
23+
import grails.test.mixin.integration.Integration
24+
import grails.transaction.Rollback
2325
import org.springframework.util.StringUtils
26+
import spock.lang.Shared
27+
import spock.lang.Specification
2428
import spock.lang.Unroll
2529

26-
import grails.test.mixin.integration.Integration
27-
import grails.transaction.*
28-
import spock.lang.*
29-
3030
@Integration
3131
@Rollback
3232
class AuditInsertSpec extends Specification {
3333

34-
def defaultIgnoreList = ['id'] + AuditLoggingConfigUtils.auditConfig.defaultIgnore?.asImmutable() ?: []
34+
@Shared
35+
def defaultIgnoreList
3536

3637
void setup() {
38+
defaultIgnoreList = ['id'] + AuditLoggingConfigUtils.auditConfig.defaultIgnore?.asImmutable() ?: []
3739
Author.auditable = true
3840
}
3941

@@ -298,4 +300,42 @@ class AuditInsertSpec extends Specification {
298300
}
299301
}
300302

303+
void "Test auditableProperties"() {
304+
given:
305+
Author.auditable = [auditableProperties: ['name', 'age', 'dateCreated']]
306+
def author = new Author(name: "Aaron", age: 50, famous: true, ssn: '123-981-0001')
307+
308+
when:
309+
author.save(flush: true, failOnError: true)
310+
311+
then: "only properties in auditableProperties are logged"
312+
def events = AuditTrail.findAllByClassName('test.Author')
313+
314+
events.size() == 3
315+
['name', 'age', 'dateCreated'].each { name ->
316+
assert events.find {it.propertyName == name}, "${name} was not logged"
317+
}
318+
}
319+
320+
void "Test auditableProperties overrides ignore list"() {
321+
given:
322+
Author.auditable = [
323+
auditableProperties: ['name', 'age', 'dateCreated'],
324+
ignore: ['name', 'age']
325+
]
326+
def author = new Author(name: "Aaron", age: 50, famous: true, ssn: '123-981-0001')
327+
328+
when:
329+
author.save(flush: true, failOnError: true)
330+
331+
then: "only properties in auditableProperties are logged"
332+
def events = AuditTrail.findAllByClassName('test.Author')
333+
334+
events.size() == 3
335+
['name', 'age', 'dateCreated'].each { name ->
336+
assert events.find {it.propertyName == name}, "${name} was not logged"
337+
}
338+
}
339+
340+
301341
}

audit-test/src/integration-test/groovy/test/AuditUpdateSpec.groovy

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import spock.lang.*
2525
@Integration
2626
@Rollback
2727
class AuditUpdateSpec extends Specification {
28+
2829
void setupData() {
2930
Author.auditable = true
3031

@@ -230,6 +231,53 @@ class AuditUpdateSpec extends Specification {
230231
first.persistedObjectVersion == author.version - 1
231232
}
232233

234+
void "Test auditableProperties"() {
235+
given:
236+
setupData()
237+
Author.auditable = [auditableProperties: ['name', 'famous', 'lastUpdated']]
238+
def author = Author.findByName("Aaron")
239+
240+
when:
241+
author.age = 50
242+
author.famous = false
243+
author.name = 'Bob'
244+
author.save(flush: true, failOnError: true)
245+
246+
then: "only properties in auditableProperties are logged"
247+
def events = AuditTrail.findAllByClassName('test.Author')
248+
249+
events.size() == 3
250+
251+
['name', 'famous', 'lastUpdated'].each { name ->
252+
assert events.find {it.propertyName == name}, "${name} was not logged"
253+
}
254+
}
255+
256+
void "Test auditableProperties overrides ignore list"() {
257+
given:
258+
setupData()
259+
Author.auditable = [
260+
auditableProperties: ['name', 'famous', 'lastUpdated'],
261+
ignore: ['name', 'famous']
262+
]
263+
def author = Author.findByName("Aaron")
264+
265+
when:
266+
author.age = 50
267+
author.famous = false
268+
author.name = 'Bob'
269+
author.save(flush: true, failOnError: true)
270+
271+
then: "only properties in auditableProperties are logged"
272+
def events = AuditTrail.findAllByClassName('test.Author')
273+
274+
events.size() == 3
275+
276+
['name', 'famous', 'lastUpdated'].each { name ->
277+
assert events.find {it.propertyName == name}, "${name} was not logged"
278+
}
279+
}
280+
233281
void "Test handler is called"() {
234282
given:
235283
setupData()

0 commit comments

Comments
 (0)