diff --git a/gradle/liquibase.gradle b/gradle/liquibase.gradle index d33475c19..c4df6b69c 100644 --- a/gradle/liquibase.gradle +++ b/gradle/liquibase.gradle @@ -41,6 +41,10 @@ task liquibaseDiffChangelog(dependsOn: compileJava, type: JavaExec) { args "--changeLogFile=src/main/resources/config/liquibase/changelog/" + buildTimestamp() +"_changelog.xml" args "--referenceUrl=hibernate:spring:org.radarcns.management.domain?dialect=org.hibernate.dialect.PostgreSQLDialect&hibernate.physical_naming_strategy=org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy&hibernate.implicit_naming_strategy=org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy" + // comment out line above and uncomment lines below if you want to compare against another postgres instance + //args "--referenceUrl=jdbc:postgresql://localhost:5433/managementportal" + //args "--referenceUsername=postgres" + //args "--referencePassword=" args "--username=postgres" args "--password=" args "--url=jdbc:postgresql://localhost:5432/managementportal" diff --git a/src/main/java/org/radarcns/management/config/audit/AuditReaderConfiguration.java b/src/main/java/org/radarcns/management/config/audit/AuditReaderConfiguration.java new file mode 100644 index 000000000..24b9603db --- /dev/null +++ b/src/main/java/org/radarcns/management/config/audit/AuditReaderConfiguration.java @@ -0,0 +1,21 @@ +package org.radarcns.management.config.audit; + +import org.hibernate.envers.AuditReader; +import org.hibernate.envers.AuditReaderFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import javax.persistence.EntityManager; + +@Configuration +public class AuditReaderConfiguration { + + @Autowired + private EntityManager entityManager; + + @Bean + public AuditReader auditReader() { + return AuditReaderFactory.get(entityManager); + } +} diff --git a/src/main/java/org/radarcns/management/domain/AbstractAuditingEntity.java b/src/main/java/org/radarcns/management/domain/AbstractAuditingEntity.java deleted file mode 100644 index ace46c678..000000000 --- a/src/main/java/org/radarcns/management/domain/AbstractAuditingEntity.java +++ /dev/null @@ -1,81 +0,0 @@ -package org.radarcns.management.domain; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import java.io.Serializable; -import java.time.ZonedDateTime; -import javax.persistence.Column; -import javax.persistence.EntityListeners; -import javax.persistence.MappedSuperclass; -import org.hibernate.envers.Audited; -import org.radarcns.management.domain.support.EventPublisherEntityListener; -import org.radarcns.management.domain.support.LogPublisherEntityListener; -import org.springframework.data.annotation.CreatedBy; -import org.springframework.data.annotation.CreatedDate; -import org.springframework.data.annotation.LastModifiedBy; -import org.springframework.data.annotation.LastModifiedDate; -import org.springframework.data.jpa.domain.support.AuditingEntityListener; - -/** - * Base abstract class for entities which will hold definitions for created, last modified by and - * created, last modified by date. - */ -@MappedSuperclass -@Audited -@EntityListeners({AuditingEntityListener.class, EventPublisherEntityListener.class, - LogPublisherEntityListener.class}) -public abstract class AbstractAuditingEntity implements Serializable { - - private static final long serialVersionUID = 1L; - - @CreatedBy - @Column(name = "created_by", nullable = false, length = 50, updatable = false) - @JsonIgnore - private String createdBy; - - @CreatedDate - @Column(name = "created_date", nullable = false) - @JsonIgnore - private ZonedDateTime createdDate = ZonedDateTime.now(); - - @LastModifiedBy - @Column(name = "last_modified_by", length = 50) - @JsonIgnore - private String lastModifiedBy; - - @LastModifiedDate - @Column(name = "last_modified_date") - @JsonIgnore - private ZonedDateTime lastModifiedDate = ZonedDateTime.now(); - - public String getCreatedBy() { - return createdBy; - } - - public void setCreatedBy(String createdBy) { - this.createdBy = createdBy; - } - - public ZonedDateTime getCreatedDate() { - return createdDate; - } - - public void setCreatedDate(ZonedDateTime createdDate) { - this.createdDate = createdDate; - } - - public String getLastModifiedBy() { - return lastModifiedBy; - } - - public void setLastModifiedBy(String lastModifiedBy) { - this.lastModifiedBy = lastModifiedBy; - } - - public ZonedDateTime getLastModifiedDate() { - return lastModifiedDate; - } - - public void setLastModifiedDate(ZonedDateTime lastModifiedDate) { - this.lastModifiedDate = lastModifiedDate; - } -} diff --git a/src/main/java/org/radarcns/management/domain/AbstractEntity.java b/src/main/java/org/radarcns/management/domain/AbstractEntity.java new file mode 100644 index 000000000..f48955de3 --- /dev/null +++ b/src/main/java/org/radarcns/management/domain/AbstractEntity.java @@ -0,0 +1,59 @@ +package org.radarcns.management.domain; + +import org.radarcns.management.domain.support.AbstractEntityListener; + +import java.io.Serializable; +import java.time.ZonedDateTime; + +/** + * Base abstract class for entities which will hold definitions for created, last modified by and + * created, last modified by date. These will be populated by {@link AbstractEntityListener} on + * the {@code PostLoad} trigger. Since this class is not an Entity or a MappedSuperClass, we need + * to define the entitylistener on each of the subclasses. + */ +public abstract class AbstractEntity implements Serializable { + + private static final long serialVersionUID = 1L; + + private String createdBy; + + private ZonedDateTime createdDate; + + private String lastModifiedBy; + + private ZonedDateTime lastModifiedDate; + + public String getCreatedBy() { + return createdBy; + } + + public void setCreatedBy(String createdBy) { + this.createdBy = createdBy; + } + + public ZonedDateTime getCreatedDate() { + return createdDate; + } + + public void setCreatedDate(ZonedDateTime createdDate) { + this.createdDate = createdDate; + } + + public String getLastModifiedBy() { + return lastModifiedBy; + } + + public void setLastModifiedBy(String lastModifiedBy) { + this.lastModifiedBy = lastModifiedBy; + } + + public ZonedDateTime getLastModifiedDate() { + return lastModifiedDate; + } + + public void setLastModifiedDate(ZonedDateTime lastModifiedDate) { + this.lastModifiedDate = lastModifiedDate; + } + + public abstract Long getId(); +} diff --git a/src/main/java/org/radarcns/management/domain/PersistentAuditEvent.java b/src/main/java/org/radarcns/management/domain/PersistentAuditEvent.java index cf6f6ac6a..4c4b50b72 100644 --- a/src/main/java/org/radarcns/management/domain/PersistentAuditEvent.java +++ b/src/main/java/org/radarcns/management/domain/PersistentAuditEvent.java @@ -31,7 +31,7 @@ public class PersistentAuditEvent implements Serializable { @Id @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator") - @SequenceGenerator(name = "sequenceGenerator", initialValue = 1, allocationSize = 1) + @SequenceGenerator(name = "sequenceGenerator", initialValue = 1000) @Column(name = "event_id") private Long id; diff --git a/src/main/java/org/radarcns/management/domain/Project.java b/src/main/java/org/radarcns/management/domain/Project.java index 4e5f6110b..9d46f09ef 100644 --- a/src/main/java/org/radarcns/management/domain/Project.java +++ b/src/main/java/org/radarcns/management/domain/Project.java @@ -42,13 +42,13 @@ @Audited @Table(name = "project") @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) -public class Project extends AbstractAuditingEntity implements Serializable { +public class Project extends AbstractEntity implements Serializable { private static final long serialVersionUID = 1L; @Id @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator") - @SequenceGenerator(name = "sequenceGenerator", initialValue = 1, allocationSize = 1) + @SequenceGenerator(name = "sequenceGenerator", initialValue = 1000) private Long id; @NotNull diff --git a/src/main/java/org/radarcns/management/domain/Role.java b/src/main/java/org/radarcns/management/domain/Role.java index 7b97182cb..ff0a5a6ef 100644 --- a/src/main/java/org/radarcns/management/domain/Role.java +++ b/src/main/java/org/radarcns/management/domain/Role.java @@ -12,6 +12,7 @@ import org.hibernate.annotations.Cache; import org.hibernate.annotations.CacheConcurrencyStrategy; import org.hibernate.envers.Audited; +import org.hibernate.envers.RelationTargetAuditMode; import javax.persistence.Entity; import javax.persistence.GeneratedValue; @@ -34,18 +35,19 @@ @Audited @Table(name = "radar_role") @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) -public class Role extends AbstractAuditingEntity implements Serializable { +public class Role extends AbstractEntity implements Serializable { private static final long serialVersionUID = 1L; @Id @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator") - @SequenceGenerator(name = "sequenceGenerator", initialValue = 1, allocationSize = 1) + @SequenceGenerator(name = "sequenceGenerator", initialValue = 1000) private Long id; @ManyToMany(mappedBy = "roles") @JsonIgnore @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) + @Audited(targetAuditMode = RelationTargetAuditMode.NOT_AUDITED) private Set users = new HashSet<>(); @ManyToOne diff --git a/src/main/java/org/radarcns/management/domain/Source.java b/src/main/java/org/radarcns/management/domain/Source.java index 1e8bb319a..2b546551d 100644 --- a/src/main/java/org/radarcns/management/domain/Source.java +++ b/src/main/java/org/radarcns/management/domain/Source.java @@ -39,13 +39,13 @@ @Audited @Table(name = "radar_source") @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) -public class Source extends AbstractAuditingEntity implements Serializable { +public class Source extends AbstractEntity implements Serializable { private static final long serialVersionUID = 1L; @Id @GeneratedValue(strategy = GenerationType.AUTO, generator = "sequenceGenerator") - @SequenceGenerator(name = "sequenceGenerator", initialValue = 1, allocationSize = 1) + @SequenceGenerator(name = "sequenceGenerator", initialValue = 1000) private Long id; @NotNull diff --git a/src/main/java/org/radarcns/management/domain/SourceData.java b/src/main/java/org/radarcns/management/domain/SourceData.java index c750e2122..4541a096d 100644 --- a/src/main/java/org/radarcns/management/domain/SourceData.java +++ b/src/main/java/org/radarcns/management/domain/SourceData.java @@ -31,13 +31,13 @@ @Audited @Table(name = "source_data") @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) -public class SourceData extends AbstractAuditingEntity implements Serializable { +public class SourceData extends AbstractEntity implements Serializable { private static final long serialVersionUID = 1L; @Id @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator") - @SequenceGenerator(name = "sequenceGenerator", initialValue = 1, allocationSize = 1) + @SequenceGenerator(name = "sequenceGenerator", initialValue = 1000) private Long id; //SourceData type e.g. ACCELEROMETER, TEMPERATURE. diff --git a/src/main/java/org/radarcns/management/domain/SourceType.java b/src/main/java/org/radarcns/management/domain/SourceType.java index 24aa5955f..1673373d1 100644 --- a/src/main/java/org/radarcns/management/domain/SourceType.java +++ b/src/main/java/org/radarcns/management/domain/SourceType.java @@ -35,13 +35,13 @@ @Audited @Table(name = "source_type") @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) -public class SourceType extends AbstractAuditingEntity implements Serializable { +public class SourceType extends AbstractEntity implements Serializable { private static final long serialVersionUID = 1L; @Id @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator") - @SequenceGenerator(name = "sequenceGenerator", initialValue = 1, allocationSize = 1) + @SequenceGenerator(name = "sequenceGenerator", initialValue = 1000) private Long id; @NotNull diff --git a/src/main/java/org/radarcns/management/domain/Subject.java b/src/main/java/org/radarcns/management/domain/Subject.java index 16c2c5c31..df890852a 100644 --- a/src/main/java/org/radarcns/management/domain/Subject.java +++ b/src/main/java/org/radarcns/management/domain/Subject.java @@ -36,13 +36,13 @@ @Audited @Table(name = "subject") @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) -public class Subject extends AbstractAuditingEntity implements Serializable { +public class Subject extends AbstractEntity implements Serializable { private static final long serialVersionUID = 1L; @Id @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator") - @SequenceGenerator(name = "sequenceGenerator", initialValue = 1, allocationSize = 1) + @SequenceGenerator(name = "sequenceGenerator", initialValue = 1000) private Long id; @Column(name = "external_link") diff --git a/src/main/java/org/radarcns/management/domain/User.java b/src/main/java/org/radarcns/management/domain/User.java index 4984ccf2d..f4c78a605 100644 --- a/src/main/java/org/radarcns/management/domain/User.java +++ b/src/main/java/org/radarcns/management/domain/User.java @@ -9,9 +9,11 @@ import org.hibernate.envers.Audited; import org.hibernate.validator.constraints.Email; import org.radarcns.auth.config.Constants; +import org.radarcns.management.domain.support.AbstractEntityListener; import javax.persistence.Column; import javax.persistence.Entity; +import javax.persistence.EntityListeners; import javax.persistence.FetchType; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; @@ -38,13 +40,14 @@ @Audited @Table(name = "radar_user") @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) -public class User extends AbstractAuditingEntity implements Serializable { +@EntityListeners({AbstractEntityListener.class}) +public class User extends AbstractEntity implements Serializable { private static final long serialVersionUID = 1L; @Id @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator") - @SequenceGenerator(name = "sequenceGenerator", initialValue = 1, allocationSize = 1) + @SequenceGenerator(name = "sequenceGenerator", initialValue = 1000) private Long id; @NotNull diff --git a/src/main/java/org/radarcns/management/domain/audit/CustomRevisionEntity.java b/src/main/java/org/radarcns/management/domain/audit/CustomRevisionEntity.java index 04a8fc988..14845fe61 100644 --- a/src/main/java/org/radarcns/management/domain/audit/CustomRevisionEntity.java +++ b/src/main/java/org/radarcns/management/domain/audit/CustomRevisionEntity.java @@ -1,18 +1,83 @@ package org.radarcns.management.domain.audit; -import javax.persistence.Entity; -import javax.persistence.Table; -import org.hibernate.envers.DefaultRevisionEntity; import org.hibernate.envers.RevisionEntity; +import org.hibernate.envers.RevisionNumber; +import org.hibernate.envers.RevisionTimestamp; import org.radarcns.management.config.audit.CustomRevisionListener; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.SequenceGenerator; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; +import java.io.Serializable; +import java.text.DateFormat; +import java.util.Date; +import java.util.Objects; + @Entity @RevisionEntity(CustomRevisionListener.class) @Table(name = "_revisions_info") -public class CustomRevisionEntity extends DefaultRevisionEntity { +public class CustomRevisionEntity implements Serializable { + private static final long serialVersionUID = 8530213963961662300L; + + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "revisionGenerator") + @SequenceGenerator(name = "revisionGenerator", initialValue = 2, allocationSize = 50, + sequenceName = "sequence_revision") + @RevisionNumber + private long id; + + @RevisionTimestamp + private Date timestamp; private String auditor; + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + @Temporal(TemporalType.TIMESTAMP) + public Date getTimestamp() { + return timestamp; + } + + public void setTimestamp(Date timestamp) { + this.timestamp = timestamp; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof CustomRevisionEntity)) { + return false; + } + CustomRevisionEntity that = (CustomRevisionEntity) o; + return id == that.id && Objects.equals(timestamp, that.timestamp) && Objects + .equals(auditor, that.auditor); + } + + @Override + public int hashCode() { + return Objects.hash(id, timestamp, auditor); + } + + @Override + public String toString() { + return "CustomRevisionEntity(id = " + id + + ", revisionDate = " + DateFormat.getDateTimeInstance().format( timestamp ) + ", " + + "auditor = " + auditor + ")"; + } + public String getAuditor() { return auditor; } diff --git a/src/main/java/org/radarcns/management/domain/support/AbstractEntityListener.java b/src/main/java/org/radarcns/management/domain/support/AbstractEntityListener.java new file mode 100644 index 000000000..038c4d4b1 --- /dev/null +++ b/src/main/java/org/radarcns/management/domain/support/AbstractEntityListener.java @@ -0,0 +1,109 @@ +package org.radarcns.management.domain.support; + +import org.hibernate.envers.AuditReader; +import org.hibernate.envers.AuditReaderFactory; +import org.radarcns.management.domain.AbstractEntity; +import org.radarcns.management.domain.audit.CustomRevisionEntity; +import org.radarcns.management.security.SpringSecurityAuditorAware; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.persistence.EntityManager; +import javax.persistence.PostLoad; +import javax.persistence.PostPersist; +import javax.persistence.PostRemove; +import javax.persistence.PostUpdate; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +/** + * Entity listener that contains all listeners that should fire on AbstractEntity's. This + * listener will publish entity events to log, to have a uniform, centralized logging of entity + * operations. Also, it will populate the created_by, created_at, last_modified_by and + * last_modified_at fields using Envers audits when an entity is loaded. + */ +@Component +public class AbstractEntityListener { + + public static final String ENTITY_CREATED = "ENTITY_CREATED"; + public static final String ENTITY_UPDATED = "ENTITY_UPDATED"; + public static final String ENTITY_REMOVED = "ENTITY_REMOVED"; + + private final Logger logger = LoggerFactory.getLogger(AbstractEntityListener.class); + private static final String TEMPLATE = "[{}] by {}: entityClass={}, entity={}"; + + @Autowired + private EntityManager em; + + @Autowired + private SpringSecurityAuditorAware springSecurityAuditorAware; + + /** + * Event listener to log a persist event. + * + * @param entity the entity that is persisted + */ + @PostPersist + public void publishPersistEvent(AbstractEntity entity) { + AutowireHelper.autowire(this, springSecurityAuditorAware); + logger.info(TEMPLATE, ENTITY_CREATED, springSecurityAuditorAware.getCurrentAuditor(), + entity.getClass().getName(), entity.toString()); + } + + /** + * Event listener to log an update event. + * + * @param entity the entity that is updated + */ + @PostUpdate + public void publishUpdateEvent(AbstractEntity entity) { + AutowireHelper.autowire(this, springSecurityAuditorAware); + logger.info(TEMPLATE, ENTITY_UPDATED, springSecurityAuditorAware.getCurrentAuditor(), + entity.getClass().getName(), entity.toString()); + } + + /** + * Event listener to log a remove event. + * + * @param entity the entity that is removed + */ + @PostRemove + public void publishRemoveEvent(AbstractEntity entity) { + AutowireHelper.autowire(this, springSecurityAuditorAware); + logger.info(TEMPLATE, ENTITY_REMOVED, springSecurityAuditorAware.getCurrentAuditor(), + entity.getClass().getName(), entity.toString()); + } + + /** + * Event listener to populate audit metadata. + * + * @param entity the entity that was loaded + */ + @PostLoad + public void populateAuditMetaData(AbstractEntity entity) { + AutowireHelper.autowire(this, em); + AuditReader auditReader = AuditReaderFactory.get(em); + + List revisions = auditReader.getRevisions(entity.getClass(), entity.getId()); + Number first = Collections.min(revisions, Comparator.comparingLong(Number::longValue)); + Number last = Collections.max(revisions, Comparator.comparingLong(Number::longValue)); + + CustomRevisionEntity firstRevision = auditReader.findRevision(CustomRevisionEntity.class, + first); + CustomRevisionEntity lastRevision = auditReader.findRevision(CustomRevisionEntity.class, + last); + + entity.setCreatedDate(ZonedDateTime.ofInstant(firstRevision.getTimestamp().toInstant(), + ZoneOffset.UTC)); + entity.setCreatedBy(firstRevision.getAuditor()); + entity.setLastModifiedDate(ZonedDateTime.ofInstant(lastRevision.getTimestamp().toInstant(), + ZoneOffset.UTC)); + entity.setLastModifiedBy(lastRevision.getAuditor()); + logger.info("Populated audit data for entity {}", entity.toString()); + } +} diff --git a/src/main/java/org/radarcns/management/domain/support/EventPublisherEntityListener.java b/src/main/java/org/radarcns/management/domain/support/EventPublisherEntityListener.java deleted file mode 100644 index e92b08066..000000000 --- a/src/main/java/org/radarcns/management/domain/support/EventPublisherEntityListener.java +++ /dev/null @@ -1,97 +0,0 @@ -package org.radarcns.management.domain.support; - -import java.time.ZoneId; -import java.time.ZonedDateTime; -import java.util.HashMap; -import java.util.Map; -import javax.persistence.PostPersist; -import javax.persistence.PostRemove; -import javax.persistence.PostUpdate; -import org.radarcns.management.domain.AbstractAuditingEntity; -import org.radarcns.management.security.SpringSecurityAuditorAware; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.actuate.audit.AuditEvent; -import org.springframework.boot.actuate.audit.AuditEventRepository; -import org.springframework.data.auditing.CurrentDateTimeProvider; -import org.springframework.data.auditing.DateTimeProvider; -import org.springframework.stereotype.Component; - -/** - * EntityListener that publishes audit events to the ApplicationEventPublisher so we also have - * separate audit logs for these events instead of only having the latest modified at and modified - * by information. We can not autowire Spring beans into JPA classes, so we need to make use of an - * AutowireHelper - * (source: - * https://guylabs.ch/2014/02/22/autowiring-pring-beans-in-hibernate-jpa-entity-listeners/). - */ -@Component -public class EventPublisherEntityListener { - - public static final String ENTITY_CREATED = "ENTITY_CREATED"; - public static final String ENTITY_UPDATED = "ENTITY_UPDATED"; - public static final String ENTITY_REMOVED = "ENTITY_REMOVED"; - - @Autowired - private AuditEventRepository auditEventRepository; - - private final DateTimeProvider dateTimeProvider = CurrentDateTimeProvider.INSTANCE; - - @Autowired - private SpringSecurityAuditorAware springSecurityAuditorAware; - - /** - * Event listener to publish a persist event to the audit event repository. - * - * @param entity the entity that is persisted - */ - @PostPersist - public void publishPersistEvent(AbstractAuditingEntity entity) { - AutowireHelper.autowire(this, auditEventRepository); - AuditEvent event = new AuditEvent(entity.getCreatedBy(), ENTITY_CREATED, - createData(entity)); - auditEventRepository.add(event); - } - - /** - * Event listener to publish an update event to the audit event repository. - * - * @param entity the entity that is updated - */ - @PostUpdate - public void publishUpdateEvent(AbstractAuditingEntity entity) { - AutowireHelper.autowire(this, auditEventRepository); - AuditEvent event = new AuditEvent(entity.getLastModifiedBy(), ENTITY_UPDATED, - createData(entity)); - auditEventRepository.add(event); - } - - /** - * Event listener to publish a remove event to the audit event repository. - * - * @param entity the entity that is removed - */ - @PostRemove - public void publishRemoveEvent(AbstractAuditingEntity entity) { - AutowireHelper.autowire(this, auditEventRepository); - AutowireHelper.autowire(this.springSecurityAuditorAware); - AuditEvent event = new AuditEvent(springSecurityAuditorAware.getCurrentAuditor(), - ENTITY_REMOVED, createData(entity)); - auditEventRepository.add(event); - } - - /** - * Get a standard description of the entity (class and toString() result) as well as the date - * when the operation took place. - * - * @param entity The entity for which to generate the description - * @return a Map containing the description of the entity and date the operation took place - */ - private Map createData(AbstractAuditingEntity entity) { - Map data = new HashMap<>(); - data.put("entityClass", entity.getClass().getName()); - data.put("entity", entity.toString()); - data.put("date", ZonedDateTime.ofInstant(dateTimeProvider.getNow().toInstant(), - ZoneId.systemDefault())); - return data; - } -} diff --git a/src/main/java/org/radarcns/management/domain/support/LogPublisherEntityListener.java b/src/main/java/org/radarcns/management/domain/support/LogPublisherEntityListener.java deleted file mode 100644 index ef5847337..000000000 --- a/src/main/java/org/radarcns/management/domain/support/LogPublisherEntityListener.java +++ /dev/null @@ -1,55 +0,0 @@ -package org.radarcns.management.domain.support; - -import javax.persistence.PostPersist; -import javax.persistence.PostRemove; -import javax.persistence.PostUpdate; -import org.radarcns.management.domain.AbstractAuditingEntity; -import org.radarcns.management.security.SecurityUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.stereotype.Component; - -/** - * Entity listener that publishes entity events to log, to have a uniform, centralized logging of - * entity operations. - */ -@Component -public class LogPublisherEntityListener { - - private final Logger logger = LoggerFactory.getLogger(LogPublisherEntityListener.class); - private static final String TEMPLATE = "[{}] by {}: entityClass={}, entity={}"; - - /** - * Event listener to log a persist event. - * - * @param entity the entity that is persisted - */ - @PostPersist - public void publishPersistEvent(AbstractAuditingEntity entity) { - logger.info(TEMPLATE, EventPublisherEntityListener.ENTITY_CREATED, - entity.getCreatedBy(), entity.getClass().getName(), entity.toString()); - } - - /** - * Event listener to log an update event. - * - * @param entity the entity that is updated - */ - @PostUpdate - public void publishUpdateEvent(AbstractAuditingEntity entity) { - logger.info(TEMPLATE, EventPublisherEntityListener.ENTITY_UPDATED, - entity.getLastModifiedBy(), entity.getClass().getName(), entity.toString()); - } - - /** - * Event listener to log a remove event. - * - * @param entity the entity that is removed - */ - @PostRemove - public void publishRemoveEvent(AbstractAuditingEntity entity) { - logger.info(TEMPLATE, EventPublisherEntityListener.ENTITY_REMOVED, - SecurityUtils.getCurrentUserLogin(), entity.getClass().getName(), - entity.toString()); - } -} diff --git a/src/main/java/org/radarcns/management/repository/CustomRevisionEntityRepository.java b/src/main/java/org/radarcns/management/repository/CustomRevisionEntityRepository.java new file mode 100644 index 000000000..9461633d5 --- /dev/null +++ b/src/main/java/org/radarcns/management/repository/CustomRevisionEntityRepository.java @@ -0,0 +1,7 @@ +package org.radarcns.management.repository; + +import org.radarcns.management.domain.audit.CustomRevisionEntity; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface CustomRevisionEntityRepository extends JpaRepository { +} diff --git a/src/main/java/org/radarcns/management/repository/UserRepository.java b/src/main/java/org/radarcns/management/repository/UserRepository.java index 541ce652b..026ec039a 100644 --- a/src/main/java/org/radarcns/management/repository/UserRepository.java +++ b/src/main/java/org/radarcns/management/repository/UserRepository.java @@ -1,24 +1,26 @@ package org.radarcns.management.repository; -import java.time.ZonedDateTime; -import java.util.List; -import java.util.Optional; import org.radarcns.management.domain.User; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.history.RevisionRepository; import org.springframework.data.repository.query.Param; +import java.util.List; +import java.util.Optional; + /** * Spring Data JPA repository for the User entity. */ -public interface UserRepository extends JpaRepository { +public interface UserRepository extends JpaRepository, + RevisionRepository { Optional findOneByActivationKey(String activationKey); - List findAllByActivatedIsFalseAndCreatedDateBefore(ZonedDateTime dateTime); + List findAllByActivated(boolean activated); Optional findOneByResetKey(String resetKey); diff --git a/src/main/java/org/radarcns/management/service/UserService.java b/src/main/java/org/radarcns/management/service/UserService.java index 4cceabd38..36aa01622 100644 --- a/src/main/java/org/radarcns/management/service/UserService.java +++ b/src/main/java/org/radarcns/management/service/UserService.java @@ -310,13 +310,19 @@ public User getUserWithAuthorities() { */ @Scheduled(cron = "0 0 1 * * ?") public void removeNotActivatedUsers() { - ZonedDateTime now = ZonedDateTime.now(); - List users = userRepository - .findAllByActivatedIsFalseAndCreatedDateBefore(now.minusDays(3)); - for (User user : users) { - log.debug("Deleting not activated user {}", user.getLogin()); - userRepository.delete(user); - } + log.info("Scheduled scan for expired user accounts starting now"); + ZonedDateTime cutoff = ZonedDateTime.now().minusDays(3); + // get all non-activated users + userRepository.findAllByActivated(false).stream() + .forEach(user -> { + log.info("User {} created at {}", user.getLogin(), user.getCreatedDate()); + }); + userRepository.findAllByActivated(false).stream() + .filter(user -> user.getCreatedDate().isBefore(cutoff)) + .forEach(user -> { + log.info("Deleting not activated user after 3 days: {}", user.getLogin()); + userRepository.delete(user); + }); } public Page findAllByProjectNameAndAuthority(Pageable pageable, String projectName, diff --git a/src/main/resources/config/application.yml b/src/main/resources/config/application.yml index 0658d0fdb..436baea6b 100644 --- a/src/main/resources/config/application.yml +++ b/src/main/resources/config/application.yml @@ -60,6 +60,12 @@ spring: naming: physical-strategy: org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy implicit-strategy: org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy + properties: + org.hibernate.envers: + store_data_at_delete: true + audit_strategy: org.hibernate.envers.strategy.ValidityAuditStrategy + audit_strategy_validity_store_revend_timestamp: true + global_with_modified_flag: true messages: basename: i18n/messages mvc: diff --git a/src/main/resources/config/liquibase/changelog/20180308162645_enable_envers_revisions.xml b/src/main/resources/config/liquibase/changelog/20180308162645_enable_envers_revisions.xml deleted file mode 100644 index f2c56dde9..000000000 --- a/src/main/resources/config/liquibase/changelog/20180308162645_enable_envers_revisions.xml +++ /dev/null @@ -1,370 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/main/resources/config/liquibase/changelog/20180313103735_add_missing_fks.xml b/src/main/resources/config/liquibase/changelog/20180313103735_add_missing_fks.xml new file mode 100644 index 000000000..da371b9a9 --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20180313103735_add_missing_fks.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20180313103735_enable_envers_revisions.xml b/src/main/resources/config/liquibase/changelog/20180313103735_enable_envers_revisions.xml new file mode 100644 index 000000000..257258209 --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20180313103735_enable_envers_revisions.xml @@ -0,0 +1,545 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + insert into _revisions_info values (1, 'system', ${now}) + insert into project_aud(id, rev, revtype, description, description_mod, end_date, end_date_mod, location, location_mod, jhi_organization, organization_mod, project_admin, project_admin_mod, project_name, project_name_mod, project_status, project_status_mod, start_date, start_date_mod, attributes_mod, roles_mod, source_types_mod) select id, 1, 0, description, true, end_date, true, location, true, jhi_organization, true, project_admin, true, project_name, true, project_status, true, start_date, true, true, true, true from project + insert into project_metadata_aud(id, rev, revtype, attribute_key, attribute_value) select id, 1, 0, attribute_key, attribute_value from project_metadata + insert into project_source_type_aud(rev, revtype, projects_id, source_types_id) select 1, 0, projects_id, source_types_id from project_source_type + insert into radar_authority_aud(rev, revtype, name) select 1, 0, name from radar_authority + insert into radar_role_aud(id, rev, revtype, authority_name, authority_mod, project_id, project_mod, users_mod) select id, 1, 0, authority_name, true, project_id, true, true from radar_role + insert into radar_source_aud(id, rev, revtype, assigned, assigned_mod, expected_source_name, expected_source_name_mod, source_id, source_id_mod, source_name, source_name_mod, attributes_mod, project_id, project_mod, source_type_id, source_type_mod, subjects_mod) select id, 1, 0, assigned, true, expected_source_name, true, source_id, true, source_name, true, true, project_id, true, source_type_id, true, true from radar_source + insert into radar_user_aud(id, rev, revtype, activated, activated_mod, activation_key, activation_key_mod, email, email_mod, first_name, first_name_mod, lang_key, lang_key_mod, last_name, last_name_mod, login, login_mod, password_hash, password_mod, reset_date, reset_date_mod, reset_key, reset_key_mod, roles_mod) select id, 1, 0, activated, true, activation_key, true, email, true, first_name, true, lang_key, true, last_name, true, login, true, password_hash, true, reset_date, true, reset_key, true, true from radar_user + insert into role_users_aud(rev, revtype, users_id, roles_id) select 1, 0, users_id, roles_id from role_users + insert into source_data_aud(id, rev, revtype, data_class, data_class_mod, enabled, enabled_mod, frequency, frequency_mod, key_schema, key_schema_mod, processing_state, processing_state_mod, provider, provider_mod, source_data_name, source_data_name_mod, source_data_type, source_data_type_mod, topic, topic_mod, unit, unit_mod, value_schema, value_schema_mod, source_type_id, source_type_mod) select id, 1, 0, data_class, true, enabled, true, frequency, true, key_schema, true, processing_state, true, provider, true, source_data_name, true, source_data_type, true, topic, true, unit, true, value_schema, true, source_type_id, true from source_data + insert into source_metadata_aud(id, rev, revtype, attribute_key, attribute_value) select id, 1, 0, attribute_key, attribute_value from source_metadata + insert into source_type_aud(id, rev, revtype, app_provider, app_provider_mod, assessment_type, assessment_type_mod, dynamic_registration, can_register_dynamically_mod, catalog_version, catalog_version_mod, description, description_mod, model, model_mod, name, name_mod, producer, producer_mod, source_type_scope, source_type_scope_mod, projects_mod, source_data_mod) select id, 1, 0, app_provider, true, assessment_type, true, dynamic_registration, true, catalog_version, true, description, true, model, true, name, true, producer, true, source_type_scope, true, true, true from source_type + insert into subject_aud(id, rev, revtype, external_id, external_id_mod, external_link, external_link_mod, removed, removed_mod, attributes_mod, sources_mod, user_id, user_mod) select id, 1, 0, external_id, true, external_link, true, removed, true, true, true, user_id, true from subject + insert into subject_metadata_aud(id, rev, revtype, attribute_key, attribute_value) select id, 1, 0, attribute_key, attribute_value from subject_metadata + insert into subject_sources_aud(rev, revtype, subjects_id, sources_id) select 1, 0, subjects_id, sources_id from subject_sources + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/master.xml b/src/main/resources/config/liquibase/master.xml index 3398ca096..68fb8fab4 100644 --- a/src/main/resources/config/liquibase/master.xml +++ b/src/main/resources/config/liquibase/master.xml @@ -23,6 +23,7 @@ - + + diff --git a/src/test/java/org/radarcns/management/service/UserServiceIntTest.java b/src/test/java/org/radarcns/management/service/UserServiceIntTest.java index dde294f4e..4362bb01e 100644 --- a/src/test/java/org/radarcns/management/service/UserServiceIntTest.java +++ b/src/test/java/org/radarcns/management/service/UserServiceIntTest.java @@ -1,17 +1,17 @@ package org.radarcns.management.service; -import static org.assertj.core.api.Assertions.assertThat; - -import java.time.ZonedDateTime; -import java.util.List; -import java.util.Optional; -import javax.persistence.EntityManager; +import org.apache.commons.lang3.RandomStringUtils; +import org.hibernate.envers.AuditReader; +import org.hibernate.envers.AuditReaderFactory; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.radarcns.management.ManagementPortalTestApp; import org.radarcns.auth.config.Constants; +import org.radarcns.management.ManagementPortalTestApp; import org.radarcns.management.domain.User; +import org.radarcns.management.domain.audit.CustomRevisionEntity; +import org.radarcns.management.domain.support.AbstractEntityListener; +import org.radarcns.management.repository.CustomRevisionEntityRepository; import org.radarcns.management.repository.UserRepository; import org.radarcns.management.service.dto.UserDTO; import org.radarcns.management.service.mapper.UserMapper; @@ -22,8 +22,23 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.context.transaction.TestTransaction; +import org.springframework.test.util.ReflectionTestUtils; import org.springframework.transaction.annotation.Transactional; +import javax.persistence.EntityManager; +import java.time.Instant; +import java.time.Period; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + /** * Test class for the UserResource REST controller. * @@ -43,14 +58,20 @@ public class UserServiceIntTest { @Autowired private UserService userService; + @Autowired + private CustomRevisionEntityRepository revisionEntityRepository; + @Autowired private EntityManager em; + @Autowired + private AbstractEntityListener abstractEntityListener; + private UserDTO userDto; @Before - public void setUpUser() { - userDto = userMapper.userToUserDTO(UserResourceIntTest.createEntity(em)); + public void setUp() { + userDto = userMapper.userToUserDTO(UserResourceIntTest.createEntity()); } @Test @@ -97,7 +118,6 @@ public void assertThatResetKeyMustNotBeOlderThan24Hours() { @Test public void assertThatResetKeyMustBeValid() { User user = userService.createUser(userDto); - ZonedDateTime daysAgo = ZonedDateTime.now().minusHours(25); user.setActivated(true); user.setResetDate(daysAgo); @@ -131,11 +151,37 @@ public void assertThatUserCanResetPassword() { @Test public void testFindNotActivatedUsersByCreationDateBefore() { + User expiredUser = addExpiredUser(); + AuditReader auditReader = commitTransactionAndStartNew(); + + // Get the first revision of our new user (there should only be one anyway) + List revisions = auditReader.getRevisions(expiredUser.getClass(), + expiredUser.getId()); + Number first = Collections.min(revisions, Comparator.comparingLong(Number::longValue)); + + CustomRevisionEntity firstRevision = auditReader.findRevision(CustomRevisionEntity.class, + first); + + // Update the timestamp of the revision so it appears to have been created 5 days ago + Instant expInstant = Instant.now().minus(Period.ofDays(5)); + firstRevision.setTimestamp(Date.from(expInstant)); + revisionEntityRepository.save(firstRevision); + + // make sure when we reload the expired user we have the new created date + expiredUser = userRepository.findOne(expiredUser.getId()); + ZonedDateTime expZdt = ZonedDateTime.ofInstant(expInstant, ZoneOffset.UTC); + assertThat(expiredUser.getCreatedDate().getDayOfMonth()).isEqualTo(expZdt.getDayOfMonth()); + + // Now we know we have an 'old' user in the database, we can test our deletion method + int numUsers = userRepository.findAll().size(); userService.removeNotActivatedUsers(); - ZonedDateTime now = ZonedDateTime.now(); - List users = userRepository.findAllByActivatedIsFalseAndCreatedDateBefore( - now.minusDays(3)); - assertThat(users).isEmpty(); + List users = userRepository.findAll(); + // make sure have actually deleted some users, otherwise this test is pointless + assertThat(numUsers - users.size()).isEqualTo(1); + // remaining users should be either activated or have a created date less then 3 days ago + ZonedDateTime cutoff = ZonedDateTime.now().minusDays(3); + users.forEach(u -> + assertThat(u.getActivated() || u.getCreatedDate().isAfter(cutoff)).isTrue()); } @Test @@ -146,4 +192,27 @@ public void assertThatAnonymousUserIsNotGet() { .noneMatch(user -> Constants.ANONYMOUS_USER.equals(user.getLogin()))) .isTrue(); } + + private User addExpiredUser() { + User user = new User(); + user.setLogin("expired"); + user.setEmail("expired@expired"); + user.setFirstName("ex"); + user.setLastName("pired"); + user.setActivated(false); + user.setPassword(RandomStringUtils.random(60)); + return userRepository.save(user); + } + + private AuditReader commitTransactionAndStartNew() { + // flag this transaction for commit and end it + TestTransaction.flagForCommit(); + TestTransaction.end(); + TestTransaction.start(); + TestTransaction.flagForCommit(); + em = em.getEntityManagerFactory().createEntityManager(); + AuditReader auditReader = AuditReaderFactory.get(em); + ReflectionTestUtils.setField(abstractEntityListener, "em", em); + return auditReader; + } } diff --git a/src/test/java/org/radarcns/management/web/rest/UserResourceIntTest.java b/src/test/java/org/radarcns/management/web/rest/UserResourceIntTest.java index dbd90d4fd..c8dfcc2f8 100644 --- a/src/test/java/org/radarcns/management/web/rest/UserResourceIntTest.java +++ b/src/test/java/org/radarcns/management/web/rest/UserResourceIntTest.java @@ -29,7 +29,6 @@ import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.transaction.annotation.Transactional; -import javax.persistence.EntityManager; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import java.util.HashSet; @@ -92,9 +91,6 @@ public class UserResourceIntTest { @Autowired private ExceptionTranslator exceptionTranslator; - @Autowired - private EntityManager em; - @Autowired private SubjectRepository subjectRepository; @@ -132,7 +128,7 @@ public void setUp() throws ServletException { *

This is a static method, as tests for other entities might also need it, * if they test an entity which has a required relationship to the User entity.

*/ - public static User createEntity(EntityManager em) { + public static User createEntity() { User user = new User(); user.setLogin(DEFAULT_LOGIN); user.setPassword(RandomStringUtils.random(60)); @@ -146,7 +142,7 @@ public static User createEntity(EntityManager em) { @Before public void initTest() { - user = createEntity(em); + user = createEntity(); } @Test diff --git a/src/test/resources/config/application.yml b/src/test/resources/config/application.yml index aabc96e2a..b8b55d681 100644 --- a/src/test/resources/config/application.yml +++ b/src/test/resources/config/application.yml @@ -17,6 +17,11 @@ spring: application: name: ManagementPortal + profiles: + # The commented value for `active` can be replaced with valid Spring profiles to load. + # Otherwise, it will be filled in by gradle when building the WAR file + # Either way, it can be overridden by `--spring.profiles.active` value passed in the commandline or `-Dspring.profiles.active` set in `JAVA_OPTS` + active: #spring.profiles.active# jackson: serialization.write_dates_as_timestamps: false cache: @@ -43,6 +48,11 @@ spring: hibernate.cache.use_query_cache: false hibernate.generate_statistics: true hibernate.hbm2ddl.auto: validate + org.hibernate.envers: + store_data_at_delete: true + audit_strategy: org.hibernate.envers.strategy.ValidityAuditStrategy + audit_strategy_validity_store_revend_timestamp: true + global_with_modified_flag: true mail: host: localhost messages: