Commit 76dc5b97 authored by Matija Obreza's avatar Matija Obreza
Browse files

Merge branch 'gg-ce-196' into 'main'

Introduced @HideAuditValue

See merge request genesys-pgr/application-blocks!93
parents 13a16c4d c6e93718
/*
* Copyright 2021 Global Crop Diversity Trust
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.genesys.blocks.auditlog.annotations;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Add this annotation to persisted fields to prevent recording of 'previousState'
* and 'newState' values.
*
* @author Maxym Borodenko
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.FIELD, ElementType.TYPE })
public @interface HideAuditValue {
}
...@@ -43,6 +43,7 @@ import javax.persistence.TemporalType; ...@@ -43,6 +43,7 @@ import javax.persistence.TemporalType;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.FastDateFormat; import org.apache.commons.lang3.time.FastDateFormat;
import org.genesys.blocks.auditlog.annotations.Audited; import org.genesys.blocks.auditlog.annotations.Audited;
import org.genesys.blocks.auditlog.annotations.HideAuditValue;
import org.genesys.blocks.auditlog.annotations.NotAudited; import org.genesys.blocks.auditlog.annotations.NotAudited;
import org.genesys.blocks.auditlog.model.AuditAction; import org.genesys.blocks.auditlog.model.AuditAction;
import org.genesys.blocks.auditlog.model.AuditLog; import org.genesys.blocks.auditlog.model.AuditLog;
...@@ -79,14 +80,17 @@ public class AuditTrailInterceptor extends EmptyInterceptor implements Initializ ...@@ -79,14 +80,17 @@ public class AuditTrailInterceptor extends EmptyInterceptor implements Initializ
private static final Logger LOG = LoggerFactory.getLogger(AuditTrailInterceptor.class); private static final Logger LOG = LoggerFactory.getLogger(AuditTrailInterceptor.class);
/** The Constant DEFAULT_IGNORED_PROPERTIES. */ /** The Constant DEFAULT_IGNORED_PROPERTIES. */
private static final Set<String> DEFAULT_IGNORED_PROPERTIES = Stream.of("serialVersionUID", "id", "password", "createdDate", "lastModifiedDate", "version", "lastModifiedBy") private static final Set<String> DEFAULT_IGNORED_PROPERTIES = Stream.of("serialVersionUID", "id", "createdDate", "lastModifiedDate", "version", "lastModifiedBy")
.collect(Collectors.toSet()); .collect(Collectors.toSet());
/** The ignored properties. */ /** The ignored properties. */
private Set<String> ignoredProperties = new HashSet<>(DEFAULT_IGNORED_PROPERTIES); private Set<String> ignoredProperties = new HashSet<>(DEFAULT_IGNORED_PROPERTIES);
/** The ignored properties of audited entities. */ /** The ignored properties of audited entities. */
private final Map<Class<?>, Set<String>> ignoredClassFields = new HashMap<>(); private final Map<Class<?>, Set<String>> ignoredClassFields;
/** The secured properties of audited entities. */
private final Map<Class<?>, Set<String>> securedClassFields;
/** The audited classes. */ /** The audited classes. */
private Set<Class<?>> auditedClasses = new HashSet<>(); private Set<Class<?>> auditedClasses = new HashSet<>();
...@@ -138,6 +142,8 @@ public class AuditTrailInterceptor extends EmptyInterceptor implements Initializ ...@@ -138,6 +142,8 @@ public class AuditTrailInterceptor extends EmptyInterceptor implements Initializ
// make synchronized local caches // make synchronized local caches
ignoredClasses = Collections.synchronizedSet(new HashSet<>()); ignoredClasses = Collections.synchronizedSet(new HashSet<>());
includedClasses = Collections.synchronizedSet(new HashSet<>()); includedClasses = Collections.synchronizedSet(new HashSet<>());
ignoredClassFields = Collections.synchronizedMap(new HashMap<>());
securedClassFields = Collections.synchronizedMap(new HashMap<>());
} }
/* /*
...@@ -238,7 +244,7 @@ public class AuditTrailInterceptor extends EmptyInterceptor implements Initializ ...@@ -238,7 +244,7 @@ public class AuditTrailInterceptor extends EmptyInterceptor implements Initializ
return false; return false;
} }
final Set<String> entityIgnoredFields = ignoredClassFields.getOrDefault(entityClass, new HashSet<>()); final Set<String> entityIgnoredFields = ignoredClassFields.get(entityClass);
// Identify changed values // Identify changed values
for (int i = 0; i < previousState.length; i++) { for (int i = 0; i < previousState.length; i++) {
...@@ -246,7 +252,8 @@ public class AuditTrailInterceptor extends EmptyInterceptor implements Initializ ...@@ -246,7 +252,8 @@ public class AuditTrailInterceptor extends EmptyInterceptor implements Initializ
final Object prev = previousState[i]; final Object prev = previousState[i];
final Object curr = currentState[i]; final Object curr = currentState[i];
if (ignoredProperties.contains(propertyName) || entityIgnoredFields.contains(propertyName)) { if (ignoredProperties.contains(propertyName) || (entityIgnoredFields != null && entityIgnoredFields.contains(propertyName))) {
LOG.trace("{} property in {} is not audited.", propertyName, entityClass.getSimpleName());
continue; continue;
} }
...@@ -256,11 +263,9 @@ public class AuditTrailInterceptor extends EmptyInterceptor implements Initializ ...@@ -256,11 +263,9 @@ public class AuditTrailInterceptor extends EmptyInterceptor implements Initializ
if (isPrimitiveType(types[i].getReturnedClass())) { if (isPrimitiveType(types[i].getReturnedClass())) {
final String currentValue = formatValue(curr, types[i], entityClass, propertyName); final String currentValue = formatValue(curr, types[i], entityClass, propertyName);
final String previousValue = formatValue(prev, types[i], entityClass, propertyName); final String previousValue = formatValue(prev, types[i], entityClass, propertyName);
// Notice cast to Long here!
recordChange(entity, (Long) id, propertyName, previousValue, currentValue, null);
if (!StringUtils.equals(previousValue, currentValue)) {
// Notice cast to Long here!
recordChange(entity, (Long) id, propertyName, previousValue, currentValue, null);
}
} else if (isEntity(types[i].getReturnedClass())) { } else if (isEntity(types[i].getReturnedClass())) {
final EntityId prevEntity = (EntityId) prev, currEntity = (EntityId) curr; final EntityId prevEntity = (EntityId) prev, currEntity = (EntityId) curr;
final String previousValue = prevEntity == null ? null : prevEntity.getId().toString(); final String previousValue = prevEntity == null ? null : prevEntity.getId().toString();
...@@ -292,8 +297,14 @@ public class AuditTrailInterceptor extends EmptyInterceptor implements Initializ ...@@ -292,8 +297,14 @@ public class AuditTrailInterceptor extends EmptyInterceptor implements Initializ
if (someValue == null) { if (someValue == null) {
return null; return null;
} }
final Class<?> returnedClass = type.getReturnedClass();
// Check if field should be masked
final Set<String> securedFields = securedClassFields.get(entityClass);
if (securedFields != null && securedFields.contains(propertyName)) {
return AuditLog.FIELD_VALUE_NOT_AUDITED;
}
final Class<?> returnedClass = type.getReturnedClass();
if (Date.class.equals(returnedClass) || Calendar.class.equals(returnedClass)) { if (Date.class.equals(returnedClass) || Calendar.class.equals(returnedClass)) {
TemporalType temporalType = TemporalType.TIMESTAMP; TemporalType temporalType = TemporalType.TIMESTAMP;
...@@ -352,13 +363,13 @@ public class AuditTrailInterceptor extends EmptyInterceptor implements Initializ ...@@ -352,13 +363,13 @@ public class AuditTrailInterceptor extends EmptyInterceptor implements Initializ
return; return;
} }
final Set<String> entityIgnoredFields = ignoredClassFields.getOrDefault(entityClass, new HashSet<>()); final Set<String> entityIgnoredFields = ignoredClassFields.get(entityClass);
for (int i = 0; i < states.length; i++) { for (int i = 0; i < states.length; i++) {
final String propertyName = propertyNames[i]; final String propertyName = propertyNames[i];
final Object state = states[i]; final Object state = states[i];
if (ignoredProperties.contains(propertyName) || entityIgnoredFields.contains(propertyName)) { if (ignoredProperties.contains(propertyName) || (entityIgnoredFields != null && entityIgnoredFields.contains(propertyName))) {
continue; continue;
} }
...@@ -617,7 +628,6 @@ public class AuditTrailInterceptor extends EmptyInterceptor implements Initializ ...@@ -617,7 +628,6 @@ public class AuditTrailInterceptor extends EmptyInterceptor implements Initializ
/** /**
* Record change. * Record change.
* *
* @param logDate the log date
* @param entity the entity * @param entity the entity
* @param id the id * @param id the id
* @param propertyName the property name * @param propertyName the property name
...@@ -628,13 +638,14 @@ public class AuditTrailInterceptor extends EmptyInterceptor implements Initializ ...@@ -628,13 +638,14 @@ public class AuditTrailInterceptor extends EmptyInterceptor implements Initializ
private void recordChange(final Object entity, final Long id, final String propertyName, final String previousState, final String currentState, private void recordChange(final Object entity, final Long id, final String propertyName, final String previousState, final String currentState,
final Class<?> referencedEntity) { final Class<?> referencedEntity) {
if (StringUtils.equals(previousState, currentState)) { if (StringUtils.equals(previousState, currentState) && !StringUtils.equals(currentState, AuditLog.FIELD_VALUE_NOT_AUDITED)) {
LOG.trace("No state change {}.{} {}=={}", entity.getClass(), id, previousState, currentState); LOG.trace("No state change {}.{} {}=={}", entity.getClass(), id, previousState, currentState);
return; return;
} }
TransactionAuditLog change = auditTrailService.auditLogEntry(AuditAction.UPDATE, entity, id, propertyName, previousState, currentState, referencedEntity); TransactionAuditLog change = auditTrailService.auditLogEntry(AuditAction.UPDATE, entity, id, propertyName, previousState, currentState, referencedEntity);
if (auditLogStack.get().peek().remove(change)) { if (auditLogStack.get().peek().remove(change)) {
LOG.trace("Replacing exising changelog {}", change); LOG.trace("Replacing existing changelog {}", change);
} else { } else {
LOG.trace("Adding new changelog {}", change); LOG.trace("Adding new changelog {}", change);
} }
...@@ -644,7 +655,6 @@ public class AuditTrailInterceptor extends EmptyInterceptor implements Initializ ...@@ -644,7 +655,6 @@ public class AuditTrailInterceptor extends EmptyInterceptor implements Initializ
/** /**
* Record delete. * Record delete.
* *
* @param logDate the log date
* @param entity the entity * @param entity the entity
* @param id the id * @param id the id
* @param propertyName the property name * @param propertyName the property name
...@@ -652,7 +662,15 @@ public class AuditTrailInterceptor extends EmptyInterceptor implements Initializ ...@@ -652,7 +662,15 @@ public class AuditTrailInterceptor extends EmptyInterceptor implements Initializ
* @param referencedEntity the referenced entity * @param referencedEntity the referenced entity
*/ */
private void recordDelete(final Object entity, final Long id, final String propertyName, final String state, final Class<?> referencedEntity) { private void recordDelete(final Object entity, final Long id, final String propertyName, final String state, final Class<?> referencedEntity) {
TransactionAuditLog delete = auditTrailService.auditLogEntry(AuditAction.DELETE, entity, id, propertyName, state, null, referencedEntity); String stateToLog = state;
// Check fields masked with @HideAuditValue
if (stateToLog != null) {
final Set<String> securedFields = securedClassFields.get(entity.getClass());
stateToLog = securedFields != null && securedFields.contains(propertyName) ? AuditLog.FIELD_VALUE_NOT_AUDITED : state;
}
TransactionAuditLog delete = auditTrailService.auditLogEntry(AuditAction.DELETE, entity, id, propertyName, stateToLog, null, referencedEntity);
if (auditLogStack.get().peek().remove(delete)) { if (auditLogStack.get().peek().remove(delete)) {
LOG.trace("Replacing exising changelog {}", delete); LOG.trace("Replacing exising changelog {}", delete);
} else { } else {
...@@ -698,6 +716,12 @@ public class AuditTrailInterceptor extends EmptyInterceptor implements Initializ ...@@ -698,6 +716,12 @@ public class AuditTrailInterceptor extends EmptyInterceptor implements Initializ
ignoredEntityFields.add(field.getName()); ignoredEntityFields.add(field.getName());
ignoredClassFields.put(entityClass, ignoredEntityFields); ignoredClassFields.put(entityClass, ignoredEntityFields);
} }
if (field.getAnnotation(HideAuditValue.class) != null) {
Set<String> securedFields = securedClassFields.getOrDefault(entityClass, new HashSet<>());
LOG.trace("Previous and a new value of {} property of {} class is excluded from persisting", field.getName(), entityClass);
securedFields.add(field.getName());
securedClassFields.put(entityClass, securedFields);
}
}); });
return true; return true;
} }
......
...@@ -15,7 +15,6 @@ ...@@ -15,7 +15,6 @@
*/ */
package org.genesys.blocks.auditlog.model; package org.genesys.blocks.auditlog.model;
import java.io.Serializable;
import java.util.Date; import java.util.Date;
import javax.persistence.Column; import javax.persistence.Column;
...@@ -34,8 +33,11 @@ import javax.persistence.Temporal; ...@@ -34,8 +33,11 @@ import javax.persistence.Temporal;
import javax.persistence.TemporalType; import javax.persistence.TemporalType;
import javax.persistence.Transient; import javax.persistence.Transient;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.genesys.blocks.auditlog.annotations.NotAudited; import org.genesys.blocks.auditlog.annotations.NotAudited;
import org.genesys.blocks.model.ClassPK; import org.genesys.blocks.model.ClassPK;
import org.genesys.blocks.model.EmptyModel;
import org.genesys.blocks.util.JsonSidConverter;
import org.hibernate.annotations.Type; import org.hibernate.annotations.Type;
import org.springframework.data.annotation.CreatedBy; import org.springframework.data.annotation.CreatedBy;
...@@ -47,10 +49,11 @@ import com.fasterxml.jackson.annotation.JsonProperty; ...@@ -47,10 +49,11 @@ import com.fasterxml.jackson.annotation.JsonProperty;
@Entity @Entity
@Table(name = "auditlog", indexes = { @Index(unique = false, columnList = "entityId, classPk, prop") }) @Table(name = "auditlog", indexes = { @Index(unique = false, columnList = "entityId, classPk, prop") })
@NotAudited @NotAudited
public class AuditLog implements Serializable { public class AuditLog extends EmptyModel {
/** The Constant serialVersionUID. */ /** The Constant serialVersionUID. */
private static final long serialVersionUID = -2254427722756061411L; private static final long serialVersionUID = -2254427722756061411L;
public static final String FIELD_VALUE_NOT_AUDITED = "__FIELD_VALUE_NOT_AUDITED__";
/** The id. */ /** The id. */
@Id @Id
...@@ -60,6 +63,7 @@ public class AuditLog implements Serializable { ...@@ -60,6 +63,7 @@ public class AuditLog implements Serializable {
/** The created by. */ /** The created by. */
@CreatedBy @CreatedBy
@JsonSerialize(converter = JsonSidConverter.class)
private Long createdBy; private Long createdBy;
/** The log date. */ /** The log date. */
...@@ -143,6 +147,7 @@ public class AuditLog implements Serializable { ...@@ -143,6 +147,7 @@ public class AuditLog implements Serializable {
* *
* @return the id * @return the id
*/ */
@Override
public Long getId() { public Long getId() {
return id; return id;
} }
......
...@@ -15,20 +15,22 @@ ...@@ -15,20 +15,22 @@
*/ */
package org.genesys.blocks.auditlog.model.filters; package org.genesys.blocks.auditlog.model.filters;
import java.util.List;
import java.util.Set; import java.util.Set;
import com.querydsl.core.BooleanBuilder;
import com.querydsl.core.types.Predicate; import com.querydsl.core.types.Predicate;
import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.CollectionUtils;
import org.genesys.blocks.auditlog.model.AuditAction; import org.genesys.blocks.auditlog.model.AuditAction;
import org.genesys.blocks.auditlog.model.AuditLog;
import org.genesys.blocks.auditlog.model.QAuditLog; import org.genesys.blocks.auditlog.model.QAuditLog;
import org.genesys.blocks.model.filters.DateFilter; import org.genesys.blocks.model.filters.DateFilter;
import org.genesys.blocks.model.filters.EmptyModelFilter;
/** /**
* Search filter. * Search filter.
*/ */
public class AuditLogFilter { public class AuditLogFilter extends EmptyModelFilter<AuditLogFilter, AuditLog> {
/** The classname. */ /** The classname. */
public String classname; public String classname;
...@@ -53,27 +55,38 @@ public class AuditLogFilter { ...@@ -53,27 +55,38 @@ public class AuditLogFilter {
* *
* @return the predicate * @return the predicate
*/ */
public Predicate buildQuery() { @Override
final BooleanBuilder and = new BooleanBuilder(); public List<Predicate> collectPredicates() {
if (classname != null) { return collectPredicates(QAuditLog.auditLog);
and.and(QAuditLog.auditLog.classPk.classname.eq(classname)); }
/**
* Builds the query.
*
* @param auditLog the auditLog
* @return the predicate
*/
public List<Predicate> collectPredicates(QAuditLog auditLog) {
final List<Predicate> predicates = super.collectPredicates(auditLog);
if (classname != null) {
predicates.add(auditLog.classPk.classname.eq(classname));
} }
if (entityId != null) { if (entityId != null) {
and.and(QAuditLog.auditLog.entityId.eq(entityId)); predicates.add(auditLog.entityId.eq(entityId));
} }
if (CollectionUtils.isNotEmpty(createdBy)) { if (CollectionUtils.isNotEmpty(createdBy)) {
and.and(QAuditLog.auditLog.createdBy.in(createdBy)); predicates.add(auditLog.createdBy.in(createdBy));
} }
if (CollectionUtils.isNotEmpty(action)) { if (CollectionUtils.isNotEmpty(action)) {
and.and(QAuditLog.auditLog.action.in(action)); predicates.add(auditLog.action.in(action));
} }
if (CollectionUtils.isNotEmpty(propertyName)) { if (CollectionUtils.isNotEmpty(propertyName)) {
and.and(QAuditLog.auditLog.propertyName.in(propertyName)); predicates.add(auditLog.propertyName.in(propertyName));
} }
if (logDate != null) { if (logDate != null) {
and.and(logDate.buildQuery(QAuditLog.auditLog.logDate)); predicates.add(logDate.buildQuery(auditLog.logDate));
} }
return and; return predicates;
} }
} }
...@@ -92,7 +92,7 @@ public class AuditLogRepositoryCustomImpl implements AuditLogCustomRepository { ...@@ -92,7 +92,7 @@ public class AuditLogRepositoryCustomImpl implements AuditLogCustomRepository {
*/ */
@Override @Override
public Page<AuditLog> listAuditLogs(final AuditLogFilter filters, final Pageable page) { public Page<AuditLog> listAuditLogs(final AuditLogFilter filters, final Pageable page) {
return repository.findAll(filters.buildQuery(), page); return repository.findAll(filters.buildPredicate(), page);
} }
/* /*
......
...@@ -31,6 +31,7 @@ import javax.persistence.ManyToOne; ...@@ -31,6 +31,7 @@ import javax.persistence.ManyToOne;
import javax.persistence.Table; import javax.persistence.Table;
import org.genesys.blocks.auditlog.annotations.Audited; import org.genesys.blocks.auditlog.annotations.Audited;
import org.genesys.blocks.auditlog.annotations.HideAuditValue;
import org.genesys.blocks.auditlog.annotations.NotAudited; import org.genesys.blocks.auditlog.annotations.NotAudited;
import org.genesys.blocks.model.BasicModel; import org.genesys.blocks.model.BasicModel;
...@@ -51,6 +52,10 @@ public class ExampleAuditedEntity extends BasicModel { ...@@ -51,6 +52,10 @@ public class ExampleAuditedEntity extends BasicModel {
@Column(length = 20) @Column(length = 20)
private String title; private String title;
@HideAuditValue
@Column(length = 200)
private String password;
@ManyToOne(optional = true) @ManyToOne(optional = true)
private ExampleAuditedEntity reference; private ExampleAuditedEntity reference;
...@@ -85,6 +90,24 @@ public class ExampleAuditedEntity extends BasicModel { ...@@ -85,6 +90,24 @@ public class ExampleAuditedEntity extends BasicModel {
this.title = title; this.title = title;
} }
/**
* Gets the password.
*
* @return the password
*/
public String getPassword() {
return password;
}
/**
* Sets the password.
*
* @param password the new password
*/
public void setPassword(String password) {
this.password = password;
}
/** /**
* Gets the name. * Gets the name.
* *
......
...@@ -15,8 +15,8 @@ ...@@ -15,8 +15,8 @@
*/ */
package org.genesys.blocks.auditlog.service; package org.genesys.blocks.auditlog.service;
import static org.hamcrest.MatcherAssert.*;
import static org.hamcrest.Matchers.*; import static org.hamcrest.Matchers.*;
import static org.hamcrest.MatcherAssert.assertThat;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
...@@ -68,7 +68,7 @@ public class AuditTrailServiceTest extends ServiceTest { ...@@ -68,7 +68,7 @@ public class AuditTrailServiceTest extends ServiceTest {
*/ */
@Test @Test
public void save1() { public void save1() {
final Long entity = new Long(42); final Long entity = Long.valueOf(42);
final List<AuditLog> logs = auditTrailService.addAuditLogs(Sets.newHashSet(auditTrailService.auditLogEntry(AuditAction.UPDATE, entity, 1, "a", null, "new", null))); final List<AuditLog> logs = auditTrailService.addAuditLogs(Sets.newHashSet(auditTrailService.auditLogEntry(AuditAction.UPDATE, entity, 1, "a", null, "new", null)));
AuditLog log = logs.get(0); AuditLog log = logs.get(0);
...@@ -85,7 +85,7 @@ public class AuditTrailServiceTest extends ServiceTest { ...@@ -85,7 +85,7 @@ public class AuditTrailServiceTest extends ServiceTest {
*/ */
@Test @Test
public void saveRefEnt() { public void saveRefEnt() {
final Long entity = new Long(42); final Long entity = Long.valueOf(42);
final List<AuditLog> logs = auditTrailService.addAuditLogs(Sets.newHashSet(auditTrailService.auditLogEntry(AuditAction.UPDATE, entity, 1, "a", null, "new", final List<AuditLog> logs = auditTrailService.addAuditLogs(Sets.newHashSet(auditTrailService.auditLogEntry(AuditAction.UPDATE, entity, 1, "a", null, "new",
AuditLog.class))); AuditLog.class)));
AuditLog log = logs.get(0); AuditLog log = logs.get(0);
...@@ -215,6 +215,27 @@ public class AuditTrailServiceTest extends ServiceTest { ...@@ -215,6 +215,27 @@ public class AuditTrailServiceTest extends ServiceTest {
assertThat(listAuditLogs(entity), hasSize(0)); assertThat(listAuditLogs(entity), hasSize(0));
}