Commit 7e8e9ea5 authored by Maxym Borodenko's avatar Maxym Borodenko

Auditing

- Configured auditing functionality
- Added @Audited annotation to audit Accession, Inventory, Cooperator
- Liquibase changes for adding auditlog table
- API for fetching inventory auditing data
parent b0a89ce9
......@@ -668,6 +668,11 @@
<artifactId>application-blocks-security</artifactId>
<version>${application.blocks.version}</version>
</dependency>
<dependency>
<groupId>org.genesys-pgr</groupId>
<artifactId>application-blocks-auditlog</artifactId>
<version>${application.blocks.version}</version>
</dependency>
<dependency>
<groupId>org.genesys-pgr</groupId>
<artifactId>file-repository-core</artifactId>
......
......@@ -22,6 +22,8 @@ import java.util.Map;
import javax.validation.Valid;
import org.apache.commons.lang3.ArrayUtils;
import org.genesys.blocks.auditlog.model.AuditLog;
import org.genesys.blocks.auditlog.service.AuditTrailService;
import org.genesys.filerepository.InvalidRepositoryFileDataException;
import org.genesys.filerepository.InvalidRepositoryPathException;
import org.gringlobal.api.v1.ApiBaseController;
......@@ -75,6 +77,9 @@ public class InventoryController extends FilteredCRUDController<Inventory, Inven
@Autowired
private InventoryAttachmentService attachmentFileService;
@Autowired
private AuditTrailService auditService;
@Override
protected OrderSpecifier<?>[] defaultSort() {
return new OrderSpecifier[] { QInventory.inventory.id.asc() };
......@@ -102,6 +107,17 @@ public class InventoryController extends FilteredCRUDController<Inventory, Inven
return crudService.getInventoryDetails(crudService.get(id));
}
/**
* Retrieve a list of audit logs for the specified inventory
*
* @param inventoryId the inventoryId
* @return the list of all log entries
*/
@GetMapping("/auditlog/{id}")
public List<AuditLog> inventoryAuditLogs(@PathVariable(value = "id") final Long inventoryId) {
return auditService.listAuditLogs(crudService.get(inventoryId));
}
@Override
public Inventory create(@RequestBody Inventory entity) {
return super.create(entity);
......
......@@ -44,7 +44,7 @@ import javax.validation.ValidatorFactory;
@Configuration
@Import({ CommonConfig.class, CacheConfig.class, DatabaseConfig.class, SecurityConfig.class, SpringMvcConfig.class,
SchedulerConfig.class, OpenAPIConfig.class, ElasticsearchConfig.class, FileRepositoryConfig.class })
SchedulerConfig.class, OpenAPIConfig.class, ElasticsearchConfig.class, FileRepositoryConfig.class, AuditConfig.class })
@ComponentScan(basePackages = { "org.gringlobal.compatibility", "org.gringlobal.service" })
@EnableAspectJAutoProxy
public class ApplicationConfig {
......
/*
* Copyright 2020 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.gringlobal.application.config;
import org.genesys.blocks.auditlog.service.AuditTrailService;
import org.genesys.blocks.auditlog.service.ClassPKService;
import org.genesys.blocks.auditlog.service.impl.AuditTrailServiceImpl;
import org.genesys.blocks.auditlog.service.impl.ClassPKServiceImpl;
import org.genesys.blocks.security.SpringSecurityAuditorAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.domain.AuditorAware;
/**
* Configuration of audit-related components and services.
*
* @author Matija Obreza
*/
@Configuration
public class AuditConfig {
@Bean
public AuditorAware<?> auditorAware() {
return new SpringSecurityAuditorAware();
}
@Bean
public ClassPKService classPkService() {
return new ClassPKServiceImpl();
}
@Bean
public AuditTrailService auditTrailService() {
return new AuditTrailServiceImpl();
}
}
......@@ -16,16 +16,24 @@
package org.gringlobal.application.config;
import java.io.Serializable;
import java.sql.SQLException;
import java.util.Iterator;
import java.util.Properties;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import org.apache.tomcat.jdbc.pool.DataSource;
import org.genesys.blocks.auditlog.component.AuditTrailInterceptor;
import org.gringlobal.spring.AclSidAuditorAware;
import org.gringlobal.spring.DatabaseSchemaCreator;
import org.hibernate.CallbackException;
import org.hibernate.EmptyInterceptor;
import org.hibernate.Interceptor;
import org.hibernate.Transaction;
import org.hibernate.jpa.HibernatePersistenceProvider;
import org.hibernate.type.Type;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
......@@ -49,7 +57,8 @@ import liquibase.integration.spring.SpringLiquibase;
@EnableTransactionManagement
@EnableJpaAuditing(auditorAwareRef = "auditorAware")
@EnableJpaRepositories(basePackages = {
"org.gringlobal.persistence", "org.genesys.blocks.security.persistence", "org.genesys.filerepository.persistence" }, entityManagerFactoryRef = "entityManagerFactory", transactionManagerRef = "transactionManager", repositoryImplementationPostfix = "CustomImpl")
"org.gringlobal.persistence", "org.genesys.blocks.security.persistence", "org.genesys.filerepository.persistence",
"org.genesys.blocks.persistence", "org.genesys.blocks.auditlog.persistence" }, entityManagerFactoryRef = "entityManagerFactory", transactionManagerRef = "transactionManager", repositoryImplementationPostfix = "CustomImpl")
public class DatabaseConfig {
@Value("${db.url}")
......@@ -149,12 +158,93 @@ public class DatabaseConfig {
System.err.println("JPA: " + key + " = " + jpaProperties.get(key));
}
jpaProperties.put("hibernate.ejb.interceptor", new EmptyInterceptor() {
private static final long serialVersionUID = 412280557897728434L;
// NOTE We're using the auditTrailInterceptor() to fetch the lazy-initialized
// bean. Otherwise you end up with "Requested bean is currently in creation: Is
// there an unresolvable circular reference?"
@Override
public boolean onFlushDirty(final Object entity, final Serializable id, final Object[] currentState, final Object[] previousState, final String[] propertyNames,
final Type[] types) {
return auditTrailInterceptor().onFlushDirty(entity, id, currentState, previousState, propertyNames, types);
}
@Override
public void onDelete(final Object entity, final Serializable id, final Object[] state, final String[] propertyNames, final Type[] types) {
auditTrailInterceptor().onDelete(entity, id, state, propertyNames, types);
}
@Override
public void onCollectionRecreate(final Object collection, final Serializable key) throws CallbackException {
auditTrailInterceptor().onCollectionRecreate(collection, key);
}
@Override
public void onCollectionRemove(final Object collection, final Serializable key) throws CallbackException {
auditTrailInterceptor().onCollectionRemove(collection, key);
}
@Override
public void onCollectionUpdate(final Object collection, final Serializable key) throws CallbackException {
auditTrailInterceptor().onCollectionUpdate(collection, key);
}
@Override
public boolean onSave(Object entity, Serializable id, Object[] state, String[] propertyNames, Type[] types) {
return auditTrailInterceptor().onSave(entity, id, state, propertyNames, types);
}
@Override
public void afterTransactionBegin(Transaction tx) {
auditTrailInterceptor().afterTransactionBegin(tx);
}
@Override
public void beforeTransactionCompletion(Transaction tx) {
auditTrailInterceptor().beforeTransactionCompletion(tx);
}
@Override
public void afterTransactionCompletion(Transaction tx) {
auditTrailInterceptor().afterTransactionCompletion(tx);
}
@SuppressWarnings("rawtypes")
@Override
public void postFlush(Iterator entities) {
auditTrailInterceptor().postFlush(entities);
}
@Override
public Boolean isTransient(Object entity) {
try {
return auditTrailInterceptor().isTransient(entity);
} catch (Throwable e) {
// System.err.println(e.getMessage());
return super.isTransient(entity);
}
}
@SuppressWarnings("rawtypes")
@Override
public void preFlush(Iterator entities) {
auditTrailInterceptor().preFlush(entities);
}
});
entityManager.setJpaProperties(jpaProperties);
entityManager.setPackagesToScan(jpaEntityPackageNames);
return entityManager;
}
@Bean
public Interceptor auditTrailInterceptor() {
// Has to stay here so the code in JPA Hibernate setup above can pick it up!
return new AuditTrailInterceptor();
}
@Bean
public JpaTransactionManager transactionManager(final DataSource dataSource, final EntityManagerFactory emf) {
final JpaTransactionManager transactionManager = new JpaTransactionManager();
......
......@@ -21,6 +21,7 @@ import java.util.List;
import javax.persistence.*;
import javax.validation.constraints.NotBlank;
import org.genesys.blocks.auditlog.annotations.Audited;
import org.genesys.blocks.model.JsonViews;
import org.gringlobal.component.GGCE;
import org.gringlobal.component.elastic.AppContextHelper;
......@@ -46,6 +47,7 @@ import com.fasterxml.jackson.annotation.ObjectIdGenerators;
*/
@Entity
@Table(name = "accession")
@Audited
@Document(indexName = "accession")
@JsonIdentityInfo(scope = Accession.class, generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
public class Accession extends CooperatorOwnedModel implements ElasticLoader, ElasticTrigger {
......
......@@ -22,6 +22,7 @@ import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import org.genesys.blocks.annotations.NotCopyable;
import org.genesys.blocks.auditlog.annotations.Audited;
import org.genesys.blocks.model.Copyable;
import org.gringlobal.component.elastic.AppContextHelper;
import org.gringlobal.custom.elasticsearch.ElasticLoader;
......@@ -42,6 +43,7 @@ import com.fasterxml.jackson.annotation.ObjectIdGenerators;
*/
@Entity
@Cacheable
@Audited
@Document(indexName = "cooperator")
@Table(name = "cooperator")
@JsonIdentityInfo(scope = Cooperator.class, generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
......
......@@ -23,6 +23,7 @@ import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import org.genesys.blocks.auditlog.annotations.Audited;
import org.genesys.blocks.model.Copyable;
import org.genesys.blocks.model.JsonViews;
import org.gringlobal.component.GGCE;
......@@ -49,6 +50,7 @@ import com.fasterxml.jackson.annotation.ObjectIdGenerators;
*/
@Entity
@Table(name = "inventory")
@Audited
@JsonIdentityInfo(scope = Inventory.class, generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
public class Inventory extends CooperatorOwnedModel implements Copyable<Inventory>, ElasticTrigger, ElasticLoader {
private static final long serialVersionUID = -3346623020309883861L;
......
......@@ -19,7 +19,7 @@ build.artifactId=${project.artifactId}
build.name=${project.artifactId}-${buildNumber}
build.revision=${buildNumber}
db.jpa.packages=org.gringlobal.model,org.genesys.blocks.model,org.genesys.blocks.security.model,org.genesys.blocks.oauth.model,org.genesys.filerepository.model
db.jpa.packages=org.gringlobal.model,org.genesys.blocks.model,org.genesys.blocks.security.model,org.genesys.blocks.oauth.model,org.genesys.filerepository.model,org.genesys.blocks.auditlog.model
host.name=localhost
host.nameAndPort=localhost:8080
......
......@@ -3342,3 +3342,95 @@ databaseChangeLog:
tableName: order_request_item_action
columnName: started_date
columnDataType: datetime2
- changeSet:
id: 1601898977889-1
author: mborodenko
comment: Add auditlog table
changes:
- createTable:
columns:
- column:
autoIncrement: true
constraints:
primaryKey: true
name: id
type: BIGINT
- column:
constraints:
nullable: false
name: action
type: VARCHAR(10)
- column:
name: created_by
type: BIGINT
- column:
name: entity_id
type: BIGINT
- column:
constraints:
nullable: false
name: logdate
type: datetime(6)
- column:
name: new_state
type: LONGTEXT
- column:
name: previous_state
type: LONGTEXT
- column:
constraints:
nullable: false
name: prop
type: VARCHAR(50)
- column:
constraints:
nullable: false
name: class_pk
type: BIGINT
- column:
name: entity_class_pk
type: BIGINT
tableName: auditlog
- createIndex:
columns:
- column:
name: class_pk
indexName: FK_ebwh7n4mlnm1e8ty78h6n279x
tableName: auditlog
- createIndex:
columns:
- column:
name: entity_class_pk
indexName: FK_p30btngbfm13fwebfkmklu4wn
tableName: auditlog
- createIndex:
columns:
- column:
name: entity_id
- column:
name: class_pk
- column:
name: prop
indexName: UK_4jxfygboql2y3pd68nmudrgw
tableName: auditlog
- addForeignKeyConstraint:
baseColumnNames: class_pk
baseTableName: auditlog
constraintName: FK_ebwh7n4mlnm1e8ty78h6n279x
deferrable: false
initiallyDeferred: false
onDelete: NO ACTION
onUpdate: NO ACTION
referencedColumnNames: id
referencedTableName: classpk
- addForeignKeyConstraint:
baseColumnNames: entity_class_pk
baseTableName: auditlog
constraintName: FK_p30btngbfm13fwebfkmklu4wn
deferrable: false
initiallyDeferred: false
onDelete: NO ACTION
onUpdate: NO ACTION
referencedColumnNames: id
referencedTableName: classpk
......@@ -21,6 +21,7 @@ import static org.hamcrest.Matchers.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import org.genesys.blocks.auditlog.model.AuditAction;
import org.genesys.filerepository.model.RepositoryImage;
import org.gringlobal.api.v1.impl.InventoryController;
import org.gringlobal.model.Accession;
......@@ -212,7 +213,41 @@ public class InventoryControllerTest extends AbstractApiV1Test {
assertThat(updated.getQuantityOnHand(), is(quantity.quantityOnHand));
assertThat(updated.getQuantityOnHandUnitCode(), is(quantity.quantityOnHandUnitCode));
}
@Test
public void testInventoryAuditLogs() throws Exception {
Accession accession = makeAccession();
String numberPart1_V1 = "INV";
String numberPart1_V2 = "INV2";
Inventory savedInventory = addInventoryToDB(accession, numberPart1_V1, -1L, null);
assertThat(inventoryRepository.findById(savedInventory.getId()).isPresent(), is(true));
savedInventory.setInventoryNumberPart1(numberPart1_V2);
Inventory updated = inventoryService.update(savedInventory);
assertThat(updated.getInventoryNumberPart1(), is(numberPart1_V2));
assertThat(updated.getId(), is(savedInventory.getId()));
/*@formatter:off*/
mockMvc
.perform(get(InventoryController.API_URL.concat("/auditlog/{id}"), savedInventory.getId())
.contentType(MediaType.APPLICATION_JSON)
)
// .andDo(org.springframework.test.web.servlet.result.MockMvcResultHandlers.print())
.andExpect(status().isOk())
.andExpect(jsonPath("$").isArray())
.andExpect(jsonPath("$.[0]").isMap())
.andExpect(jsonPath("$.[0].createdBy", is(USER_CURATOR1_ID)))
.andExpect(jsonPath("$.[0].logDate").exists())
.andExpect(jsonPath("$.[0].classPk").isMap())
.andExpect(jsonPath("$.[0].classPk.classname", is(Inventory.class.getCanonicalName())))
.andExpect(jsonPath("$.[0].entityId", is(savedInventory.getId().intValue())))
.andExpect(jsonPath("$.[0].propertyName", is("inventoryNumberPart1")))
.andExpect(jsonPath("$.[0].previousState", is(numberPart1_V1)))
.andExpect(jsonPath("$.[0].newState", is(numberPart1_V2)))
.andExpect(jsonPath("$.[0].action", is(AuditAction.UPDATE.name())))
;
/*@formatter:on*/
}
@Test
public void testInvalidSetQuantity() throws JsonProcessingException, Exception {
......
......@@ -22,6 +22,7 @@ import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import org.genesys.blocks.auditlog.persistence.AuditLogRepository;
import org.genesys.blocks.security.service.CustomAclService;
import org.genesys.filerepository.model.RepositoryFolder;
import org.genesys.filerepository.persistence.RepositoryFilePersistence;
......@@ -70,6 +71,8 @@ public abstract class AbstractServiceTest {
protected RepositoryFolderRepository repositoryFolderRepository;
@Autowired
protected RepositoryFilePersistence repositoryFileRepository;
@Autowired
protected AuditLogRepository auditLogRepository;
@Autowired
private CodeValueService codeValueService;
......@@ -103,6 +106,7 @@ public abstract class AbstractServiceTest {
@Transactional
public void cleanup() throws Exception {
deleteFolders(repositoryFolderRepository.findAll());
auditLogRepository.deleteAll();
}
protected void deleteFolders(List<RepositoryFolder> toDelete) {
......
......@@ -19,6 +19,7 @@ import java.util.ArrayList;
import java.util.List;
import org.genesys.blocks.util.CurrentApplicationContext;
import org.gringlobal.application.config.AuditConfig;
import org.gringlobal.application.config.CacheConfig;
import org.gringlobal.application.config.CommonConfig;
import org.gringlobal.application.config.DatabaseConfig;
......@@ -51,7 +52,7 @@ public final class ApplicationConfig {
*/
@Configuration
@Import({ NoHazelcastConfig.class, CommonConfig.class, CacheConfig.class, DatabaseConfig.class, SecurityConfig.class,
SchedulerConfig.class, TestElasticsearchConfig.class, TestFileRepositoryConfig.class })
SchedulerConfig.class, TestElasticsearchConfig.class, TestFileRepositoryConfig.class, AuditConfig.class })
@ComponentScan(basePackages = { "org.gringlobal.compatibility", "org.gringlobal.service", "org.gringlobal.component.repository" })
@EnableAspectJAutoProxy
public static class BaseConfig {
......
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