Commit 96ec6f45 authored by Matija Obreza's avatar Matija Obreza

Merge branch '105-auditing' into 'master'

Resolve "Auditing"

Closes #105

See merge request !115
parents b0a89ce9 520cc726
......@@ -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>
......
......@@ -18,10 +18,15 @@ package org.gringlobal.api.v1.impl;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import javax.validation.Valid;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.apache.commons.lang3.ArrayUtils;
import org.genesys.blocks.auditlog.model.AuditLog;
import org.genesys.blocks.auditlog.model.filters.AuditLogFilter;
import org.genesys.blocks.auditlog.service.AuditTrailService;
import org.genesys.filerepository.InvalidRepositoryFileDataException;
import org.genesys.filerepository.InvalidRepositoryPathException;
import org.gringlobal.api.v1.ApiBaseController;
......@@ -29,6 +34,7 @@ import org.gringlobal.api.v1.FilteredCRUDController;
import org.gringlobal.api.v1.FilteredPage;
import org.gringlobal.api.v1.Pagination;
import org.gringlobal.custom.elasticsearch.SearchException;
import org.gringlobal.custom.json.converter.JsonCooperatorConverter;
import org.gringlobal.model.AccessionInvAttach;
import org.gringlobal.model.Inventory;
import org.gringlobal.model.InventoryAction;
......@@ -40,6 +46,8 @@ import org.gringlobal.service.InventoryService;
import org.gringlobal.service.filter.InventoryActionFilter;
import org.gringlobal.service.filter.InventoryFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.http.MediaType;
import org.springframework.security.access.prepost.PreAuthorize;
......@@ -75,6 +83,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 +113,24 @@ 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
* @param page the page request
* @return the list of all log entries
*/
@GetMapping("/auditlog/{id}")
public Page<WrappedAuditLog> inventoryAuditLogs(@PathVariable(value = "id") final Long inventoryId, @Parameter(hidden = true) final Pagination page) {
var filter = new AuditLogFilter();
filter.entityId = crudService.get(inventoryId).getId();
filter.classname = Inventory.class.getName();
Page<AuditLog> auditLogs = auditService.listAuditLogs(filter, page.toPageRequest(100));
var content = auditLogs.stream().map(WrappedAuditLog::new).collect(Collectors.toList());
return new PageImpl<>(content, auditLogs.getPageable(), auditLogs.getTotalElements());
}
@Override
public Inventory create(@RequestBody Inventory entity) {
return super.create(entity);
......@@ -179,4 +208,16 @@ public class InventoryController extends FilteredCRUDController<Inventory, Inven
return attachmentFileService.removeFile(crudService.get(inventoryId), attachmentId);
}
private static class WrappedAuditLog extends AuditLog {
public WrappedAuditLog(AuditLog auditLog) {
super(auditLog);
}
@JsonSerialize(converter = JsonCooperatorConverter.class)
@Override
public Long getCreatedBy() {
return super.getCreatedBy();
}
}
}
......@@ -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.gringlobal.custom.json.converter.JsonCooperatorConverter;
import org.gringlobal.service.CooperatorService;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
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 implements InitializingBean {
@Autowired
private CooperatorService cooperatorService;
@Override
public void afterPropertiesSet() throws Exception {
JsonCooperatorConverter.setCooperatorProvider(cooperatorService);
}
@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();
......
/*
* 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.custom.json.converter;
import com.fasterxml.jackson.databind.util.StdConverter;
/**
* The JsonCooperatorConverter converts Cooperator IDs to Cooperator names.
*/
public class JsonCooperatorConverter extends StdConverter<Long, String> {
private static CooperatorProvider COOPERATOR_PROVIDER;
public interface CooperatorProvider {
String getCooperatorName(long id);
}
/**
* Sets the cooperator provider.
*
* @param cooperatorProvider the new cooperator provider
*/
public static void setCooperatorProvider(CooperatorProvider cooperatorProvider) {
COOPERATOR_PROVIDER = cooperatorProvider;
}
/**
* Convert Cooperator ID to Cooperator name using COOPERATOR_PROVIDER (when available)
*/
@Override
public String convert(Long value) {
if (value == null) {
return null;
} else {
if (COOPERATOR_PROVIDER == null) {
return Long.toString(value);
} else {
return COOPERATOR_PROVIDER.getCooperatorName(value);
}
}
}
}
......@@ -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;
......
......@@ -15,13 +15,14 @@
*/
package org.gringlobal.service;
import org.gringlobal.custom.json.converter.JsonCooperatorConverter;
import org.gringlobal.model.Cooperator;
import org.gringlobal.service.filter.CooperatorFilter;
/**
* The Interface CooperatorService.
*/
public interface CooperatorService extends FilteredCRUDService<Cooperator, CooperatorFilter> {
public interface CooperatorService extends FilteredCRUDService<Cooperator, CooperatorFilter>, JsonCooperatorConverter.CooperatorProvider {
}
......@@ -15,6 +15,7 @@
*/
package org.gringlobal.service.impl;
import org.apache.commons.lang3.StringUtils;
import org.gringlobal.api.InvalidApiUsageException;
import org.gringlobal.custom.elasticsearch.SearchException;
import org.gringlobal.model.Cooperator;
......@@ -94,4 +95,13 @@ public class CooperatorServiceImpl extends FilteredCRUDServiceImpl<Cooperator, C
return QCooperator.cooperator.id;
}
@Override
public String getCooperatorName(long id) {
Cooperator cooperator = repository.findById(id).orElse(null);
if (cooperator != null) {
String name = cooperator.getFirstName() + " " + StringUtils.defaultIfBlank(cooperator.getLastName(), "");
return name.strip();
}
return null;
}
}
......@@ -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;
......@@ -43,8 +44,6 @@ import org.springframework.mock.web.MockMultipartFile;
import org.springframework.security.test.context.support.WithUserDetails;
import org.springframework.transaction.annotation.Transactional;
import com.fasterxml.jackson.core.JsonProcessingException;
/**
* @author Matija Obreza
*/
......@@ -136,7 +135,7 @@ public class InventoryControllerTest extends AbstractApiV1Test {
Accession accession = accessionService.create(a);
assertThat(accession, is(notNullValue()));
Inventory savedInventory = addInventoryToDB(accession, "INV", 99l, null);
Inventory savedInventory = addInventoryToDB(accession, "INV", 99L, null);
assertThat(savedInventory, is(notNullValue()));
assertThat(inventoryRepository.count(), equalTo(2L));
......@@ -184,9 +183,9 @@ public class InventoryControllerTest extends AbstractApiV1Test {
}
@Test
public void testSetQuantity() throws JsonProcessingException, Exception {
public void testSetQuantity() throws Exception {
Accession accession = makeAccession();
Inventory savedInventory = addInventoryToDB(accession, "INV", -1l, null);
Inventory savedInventory = addInventoryToDB(accession, "INV", -1L, null);
assertThat(savedInventory, is(notNullValue()));
assertThat(inventoryRepository.count(), equalTo(2L));
assertThat(savedInventory.getQuantityOnHand(), nullValue());
......@@ -212,12 +211,50 @@ public class InventoryControllerTest extends AbstractApiV1Test {
assertThat(updated.getQuantityOnHand(), is(quantity.quantityOnHand));
assertThat(updated.getQuantityOnHandUnitCode(), is(quantity.quantityOnHandUnitCode));
}
@Test
public void testInvalidSetQuantity() throws JsonProcessingException, Exception {
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}?s=propertyName"), savedInventory.getId())
.contentType(MediaType.APPLICATION_JSON)
)
// .andDo(org.springframework.test.web.servlet.result.MockMvcResultHandlers.print())
.andExpect(status().isOk())
.andExpect(jsonPath("$.totalElements", is(1)))
.andExpect(jsonPath("$.sort[0]", not(nullValue())))
.andExpect(jsonPath("$.sort[0].direction", is("ASC")))
.andExpect(jsonPath("$.sort[0].property", is("propertyName")))
.andExpect(jsonPath(</