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;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.FastDateFormat;
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.model.AuditAction;
import org.genesys.blocks.auditlog.model.AuditLog;
......@@ -79,14 +80,17 @@ public class AuditTrailInterceptor extends EmptyInterceptor implements Initializ
private static final Logger LOG = LoggerFactory.getLogger(AuditTrailInterceptor.class);
/** 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());
/** The ignored properties. */
private Set<String> ignoredProperties = new HashSet<>(DEFAULT_IGNORED_PROPERTIES);
/** 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. */
private Set<Class<?>> auditedClasses = new HashSet<>();
......@@ -138,6 +142,8 @@ public class AuditTrailInterceptor extends EmptyInterceptor implements Initializ
// make synchronized local caches
ignoredClasses = 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
return false;
}
final Set<String> entityIgnoredFields = ignoredClassFields.getOrDefault(entityClass, new HashSet<>());
final Set<String> entityIgnoredFields = ignoredClassFields.get(entityClass);
// Identify changed values
for (int i = 0; i < previousState.length; i++) {
......@@ -246,7 +252,8 @@ public class AuditTrailInterceptor extends EmptyInterceptor implements Initializ
final Object prev = previousState[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;
}
......@@ -256,11 +263,9 @@ public class AuditTrailInterceptor extends EmptyInterceptor implements Initializ
if (isPrimitiveType(types[i].getReturnedClass())) {
final String currentValue = formatValue(curr, types[i], entityClass, propertyName);
final String previousValue = formatValue(prev, types[i], entityClass, propertyName);
if (!StringUtils.equals(previousValue, currentValue)) {
// Notice cast to Long here!
recordChange(entity, (Long) id, propertyName, previousValue, currentValue, null);
}
} else if (isEntity(types[i].getReturnedClass())) {
final EntityId prevEntity = (EntityId) prev, currEntity = (EntityId) curr;
final String previousValue = prevEntity == null ? null : prevEntity.getId().toString();
......@@ -292,8 +297,14 @@ public class AuditTrailInterceptor extends EmptyInterceptor implements Initializ
if (someValue == 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)) {
TemporalType temporalType = TemporalType.TIMESTAMP;
......@@ -352,13 +363,13 @@ public class AuditTrailInterceptor extends EmptyInterceptor implements Initializ
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++) {
final String propertyName = propertyNames[i];
final Object state = states[i];
if (ignoredProperties.contains(propertyName) || entityIgnoredFields.contains(propertyName)) {
if (ignoredProperties.contains(propertyName) || (entityIgnoredFields != null && entityIgnoredFields.contains(propertyName))) {
continue;
}
......@@ -617,7 +628,6 @@ public class AuditTrailInterceptor extends EmptyInterceptor implements Initializ
/**
* Record change.
*
* @param logDate the log date
* @param entity the entity
* @param id the id
* @param propertyName the property name
......@@ -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,
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);
return;
}
TransactionAuditLog change = auditTrailService.auditLogEntry(AuditAction.UPDATE, entity, id, propertyName, previousState, currentState, referencedEntity);
if (auditLogStack.get().peek().remove(change)) {
LOG.trace("Replacing exising changelog {}", change);
LOG.trace("Replacing existing changelog {}", change);
} else {
LOG.trace("Adding new changelog {}", change);
}
......@@ -644,7 +655,6 @@ public class AuditTrailInterceptor extends EmptyInterceptor implements Initializ
/**
* Record delete.
*
* @param logDate the log date
* @param entity the entity
* @param id the id
* @param propertyName the property name
......@@ -652,7 +662,15 @@ public class AuditTrailInterceptor extends EmptyInterceptor implements Initializ
* @param referencedEntity the referenced entity
*/
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)) {
LOG.trace("Replacing exising changelog {}", delete);
} else {
......@@ -698,6 +716,12 @@ public class AuditTrailInterceptor extends EmptyInterceptor implements Initializ
ignoredEntityFields.add(field.getName());
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;
}
......
......@@ -15,7 +15,6 @@
*/
package org.genesys.blocks.auditlog.model;
import java.io.Serializable;
import java.util.Date;
import javax.persistence.Column;
......@@ -34,8 +33,11 @@ import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import javax.persistence.Transient;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.genesys.blocks.auditlog.annotations.NotAudited;
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.springframework.data.annotation.CreatedBy;
......@@ -47,10 +49,11 @@ import com.fasterxml.jackson.annotation.JsonProperty;
@Entity
@Table(name = "auditlog", indexes = { @Index(unique = false, columnList = "entityId, classPk, prop") })
@NotAudited
public class AuditLog implements Serializable {
public class AuditLog extends EmptyModel {
/** The Constant serialVersionUID. */
private static final long serialVersionUID = -2254427722756061411L;
public static final String FIELD_VALUE_NOT_AUDITED = "__FIELD_VALUE_NOT_AUDITED__";
/** The id. */
@Id
......@@ -60,6 +63,7 @@ public class AuditLog implements Serializable {
/** The created by. */
@CreatedBy
@JsonSerialize(converter = JsonSidConverter.class)
private Long createdBy;
/** The log date. */
......@@ -143,6 +147,7 @@ public class AuditLog implements Serializable {
*
* @return the id
*/
@Override
public Long getId() {
return id;
}
......
......@@ -15,20 +15,22 @@
*/
package org.genesys.blocks.auditlog.model.filters;
import java.util.List;
import java.util.Set;
import com.querydsl.core.BooleanBuilder;
import com.querydsl.core.types.Predicate;
import org.apache.commons.collections4.CollectionUtils;
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.model.filters.DateFilter;
import org.genesys.blocks.model.filters.EmptyModelFilter;
/**
* Search filter.
*/
public class AuditLogFilter {
public class AuditLogFilter extends EmptyModelFilter<AuditLogFilter, AuditLog> {
/** The classname. */
public String classname;
......@@ -53,27 +55,38 @@ public class AuditLogFilter {
*
* @return the predicate
*/
public Predicate buildQuery() {
final BooleanBuilder and = new BooleanBuilder();
if (classname != null) {
and.and(QAuditLog.auditLog.classPk.classname.eq(classname));
@Override
public List<Predicate> collectPredicates() {
return collectPredicates(QAuditLog.auditLog);
}
/**
* 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) {
and.and(QAuditLog.auditLog.entityId.eq(entityId));
predicates.add(auditLog.entityId.eq(entityId));
}
if (CollectionUtils.isNotEmpty(createdBy)) {
and.and(QAuditLog.auditLog.createdBy.in(createdBy));
predicates.add(auditLog.createdBy.in(createdBy));
}
if (CollectionUtils.isNotEmpty(action)) {
and.and(QAuditLog.auditLog.action.in(action));
predicates.add(auditLog.action.in(action));
}
if (CollectionUtils.isNotEmpty(propertyName)) {
and.and(QAuditLog.auditLog.propertyName.in(propertyName));
predicates.add(auditLog.propertyName.in(propertyName));
}
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 {
*/
@Override
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;
import javax.persistence.Table;
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.model.BasicModel;
......@@ -51,6 +52,10 @@ public class ExampleAuditedEntity extends BasicModel {
@Column(length = 20)
private String title;
@HideAuditValue
@Column(length = 200)
private String password;
@ManyToOne(optional = true)
private ExampleAuditedEntity reference;
......@@ -85,6 +90,24 @@ public class ExampleAuditedEntity extends BasicModel {
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.
*
......
......@@ -15,8 +15,8 @@
*/
package org.genesys.blocks.auditlog.service;
import static org.hamcrest.MatcherAssert.*;
import static org.hamcrest.Matchers.*;
import static org.hamcrest.MatcherAssert.assertThat;
import java.util.Arrays;
import java.util.List;
......@@ -68,7 +68,7 @@ public class AuditTrailServiceTest extends ServiceTest {
*/
@Test
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)));
AuditLog log = logs.get(0);
......@@ -85,7 +85,7 @@ public class AuditTrailServiceTest extends ServiceTest {
*/
@Test
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",
AuditLog.class)));
AuditLog log = logs.get(0);
......@@ -215,6 +215,27 @@ public class AuditTrailServiceTest extends ServiceTest {
assertThat(listAuditLogs(entity), hasSize(0));
}
/**
* Test update field with @HideAuditValue.
*/
@Test
public void testAuditingFieldWithHideAuditValue() {
ExampleAuditedEntity entity = new ExampleAuditedEntity();
entity.setName("Test 1");
entity.setPassword("pass");
entity = exampleAuditedEntityService.save(entity);
assertThat(listAuditLogs(entity), hasSize(0));
entity.setPassword("pass2");
entity = exampleAuditedEntityService.save(entity);
AuditLog lastLog = lastAuditLog(entity);
assertThat(lastLog, not(nullValue()));
assertThat(lastLog.getAction(), is(AuditAction.UPDATE));
assertThat(lastLog.getPropertyName(), is("password"));
assertThat(lastLog.getNewState(), equalTo(AuditLog.FIELD_VALUE_NOT_AUDITED));
assertThat(lastLog.getPreviousState(), equalTo(AuditLog.FIELD_VALUE_NOT_AUDITED));
}
/**
* Test referenced entity audit log.
*/
......
......@@ -224,7 +224,10 @@ public class DatabaseConfig {
@Bean
public Interceptor auditTrailInterceptor() {
// Has to stay here so the code in JPA Hibernate setup above can pick it up!
return new AuditTrailInterceptor();
AuditTrailInterceptor interceptor = new AuditTrailInterceptor();
// exclude 'password' field from a list of default ignored properties
interceptor.getIgnoredProperties().remove("password");
return interceptor;
}
/**
......
......@@ -20,11 +20,7 @@ import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.MappedSuperclass;
import javax.persistence.Transient;
import org.springframework.data.domain.Persistable;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonView;
/**
......@@ -60,45 +56,4 @@ public class BasicModel extends EmptyModel {
public void setId(final Long id) {
this.id = id;
}
/*
* (non-Javadoc)
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = (prime * result) + (id == null ? 0 : id.hashCode());
return result;
}
/*
* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(final Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final BasicModel other = (BasicModel) obj;
if (id == null || other.id == null) {
return false;
} else if (!id.equals(other.id)) {
return false;
}
return true;
}
@Override
public String toString() {
return super.toString() + " id=" + this.id;
}
}
......@@ -46,4 +46,44 @@ public abstract class EmptyModel implements EntityId, Serializable, Persistable<
return getId() == null || getId() < 0;
}
/*
* (non-Javadoc)
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = (prime * result) + (getId() == null ? 0 : getId().hashCode());
return result;
}
/*
* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(final Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final EmptyModel other = (EmptyModel) obj;
if (getId() == null || other.getId() == null) {
return false;
} else if (!getId().equals(other.getId())) {
return false;
}
return true;
}
@Override
public String toString() {
return super.toString() + " id=" + this.getId();
}
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment