Skip to content

Commit

Permalink
Merge pull request #301 from LukasLohoff/audit-mod-tracking
Browse files Browse the repository at this point in the history
Auditing: add tracking of modified fields
  • Loading branch information
LukasLohoff committed Apr 13, 2021
2 parents cf4bc46 + 356f9a4 commit 0ca106d
Show file tree
Hide file tree
Showing 12 changed files with 651 additions and 7 deletions.
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -269,3 +269,15 @@ app.

8. Integrate your app into the Keycloak clients list as new redirect URI for
`shogun-app` (e.g. `http://localhost:8080/shogun-example-app/*`).

## Entity Auditing

Shogun supports auditing of entities, powered by [Hibernate Envers](https://hibernate.org/orm/envers/).

Auditing is enabled by default and can be disabled by setting `spring.jpa.properties.hibernate.integration.envers` to `false`.

### Enabling envers mid-project

If envers is enabled mid-way and there is already data this can result in errors when querying audit data. To fix this, a revision with revision type `0` (created) has to be manually inserted for each existing entity into the respective audit table.

See https://discourse.hibernate.org/t/safe-envers-queries-when-the-audit-history-is-incomplete/771.
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package de.terrestris.shogun.boot.config;

import de.terrestris.shogun.lib.envers.ShogunEnversRevisionRepositoryFactoryBean;
import de.terrestris.shogun.properties.FileUploadProperties;
import de.terrestris.shogun.properties.ImageFileUploadProperties;
import de.terrestris.shogun.properties.UploadProperties;
Expand All @@ -8,13 +9,12 @@
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.data.envers.repository.support.EnversRevisionRepositoryFactoryBean;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;

@SpringBootApplication
@EnableJpaRepositories(
basePackages = { "de.terrestris.shogun" },
repositoryFactoryBeanClass = EnversRevisionRepositoryFactoryBean.class
repositoryFactoryBeanClass = ShogunEnversRevisionRepositoryFactoryBean.class
)
@ComponentScan(basePackages = { "de.terrestris.shogun" })
@EntityScan(basePackages = { "de.terrestris.shogun" })
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
SET search_path TO shogun_rev, public;

alter table shogun_rev.applications_rev add column if not exists created_mod bool;
alter table shogun_rev.applications_rev add column if not exists modified_mod bool;
alter table shogun_rev.applications_rev add column if not exists client_config_mod bool;
alter table shogun_rev.applications_rev add column if not exists i18n_mod bool;
alter table shogun_rev.applications_rev add column if not exists layer_config_mod bool;
alter table shogun_rev.applications_rev add column if not exists layer_tree_mod bool;
alter table shogun_rev.applications_rev add column if not exists name_mod bool;
alter table shogun_rev.applications_rev add column if not exists state_only_mod bool;
alter table shogun_rev.applications_rev add column if not exists tool_config_mod bool;

alter table shogun_rev.groupclasspermissions_rev add column if not exists created_mod bool;
alter table shogun_rev.groupclasspermissions_rev add column if not exists modified_mod bool;
alter table shogun_rev.groupclasspermissions_rev add column if not exists class_name_mod bool;
alter table shogun_rev.groupclasspermissions_rev add column if not exists permissions_mod bool;
alter table shogun_rev.groupclasspermissions_rev add column if not exists group_mod bool;

alter table shogun_rev.groupinstancepermissions_rev add column if not exists created_mod bool;
alter table shogun_rev.groupinstancepermissions_rev add column if not exists modified_mod bool;
alter table shogun_rev.groupinstancepermissions_rev add column if not exists entity_id_mod bool;
alter table shogun_rev.groupinstancepermissions_rev add column if not exists permissions_mod bool;
alter table shogun_rev.groupinstancepermissions_rev add column if not exists group_mod bool;

alter table shogun_rev.groups_rev add column if not exists created_mod bool;
alter table shogun_rev.groups_rev add column if not exists modified_mod bool;
alter table shogun_rev.groups_rev add column if not exists keycloak_id_mod bool;

alter table shogun_rev.layers_rev add column if not exists created_mod bool;
alter table shogun_rev.layers_rev add column if not exists modified_mod bool;
alter table shogun_rev.layers_rev add column if not exists client_config_mod bool;
alter table shogun_rev.layers_rev add column if not exists features_mod bool;
alter table shogun_rev.layers_rev add column if not exists name_mod bool;
alter table shogun_rev.layers_rev add column if not exists source_config_mod bool;
alter table shogun_rev.layers_rev add column if not exists type_mod bool;

alter table shogun_rev.userclasspermissions_rev add column if not exists created_mod bool;
alter table shogun_rev.userclasspermissions_rev add column if not exists modified_mod bool;
alter table shogun_rev.userclasspermissions_rev add column if not exists class_name_mod bool;
alter table shogun_rev.userclasspermissions_rev add column if not exists permissions_mod bool;
alter table shogun_rev.userclasspermissions_rev add column if not exists user_mod bool;

alter table shogun_rev.userinstancepermissions_rev add column if not exists created_mod bool;
alter table shogun_rev.userinstancepermissions_rev add column if not exists modified_mod bool;
alter table shogun_rev.userinstancepermissions_rev add column if not exists entity_id_mod bool;
alter table shogun_rev.userinstancepermissions_rev add column if not exists permissions_mod bool;
alter table shogun_rev.userinstancepermissions_rev add column if not exists user_mod bool;

alter table shogun_rev.users_rev add column if not exists created_mod bool;
alter table shogun_rev.users_rev add column if not exists modified_mod bool;
alter table shogun_rev.users_rev add column if not exists client_config_mod bool;
alter table shogun_rev.users_rev add column if not exists details_mod bool;
alter table shogun_rev.users_rev add column if not exists keycloak_id_mod bool;
1 change: 1 addition & 0 deletions shogun-config/src/main/resources/application-base.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ spring:
envers:
default_schema: shogun_rev
audit_table_prefix: _rev
global_with_modified_flag: true
hibernate:
javax:
cache:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,19 @@

import de.terrestris.shogun.interceptor.config.properties.InterceptorProperties;
import de.terrestris.shogun.interceptor.config.properties.NamespaceProperties;
import de.terrestris.shogun.lib.envers.ShogunEnversRevisionRepositoryFactoryBean;
import de.terrestris.shogun.properties.KeycloakAuthProperties;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.data.envers.repository.support.EnversRevisionRepositoryFactoryBean;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;

@SpringBootApplication
@EnableJpaRepositories(
basePackages = {"de.terrestris.shogun", "${scan.package:null}"},
repositoryFactoryBeanClass = EnversRevisionRepositoryFactoryBean.class
repositoryFactoryBeanClass = ShogunEnversRevisionRepositoryFactoryBean.class
)
@ComponentScan(basePackages = {"de.terrestris.shogun", "${scan.package:null}"})
@EntityScan(basePackages = {"de.terrestris.shogun", "${scan.package:null}"})
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Copyright 2012-2021 the original author or authors.
* See https://github.com/spring-projects/spring-data-envers/
*
* 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
*
* https://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 de.terrestris.shogun.lib.envers;

import org.hibernate.envers.DefaultRevisionEntity;
import org.springframework.data.repository.history.support.RevisionEntityInformation;

/**
* {@link RevisionEntityInformation} for {@link DefaultRevisionEntity}.
*
* @author Oliver Gierke
*/
class DefaultRevisionEntityInformation implements RevisionEntityInformation {

/*
* (non-Javadoc)
* @see org.springframework.data.repository.history.support.RevisionEntityInformation#getRevisionNumberType()
*/
public Class<?> getRevisionNumberType() {
return Integer.class;
}

/*
* (non-Javadoc)
* @see org.springframework.data.repository.history.support.RevisionEntityInformation#isDefaultRevisionEntity()
*/
public boolean isDefaultRevisionEntity() {
return true;
}

/*
* (non-Javadoc)
* @see org.springframework.data.repository.history.support.RevisionEntityInformation#getRevisionEntityClass()
*/
public Class<?> getRevisionEntityClass() {
return DefaultRevisionEntity.class;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
/*
* Modified work - Copyright 2012-2020 the original author or authors.
* See https://github.com/spring-projects/spring-data-envers/
*
* 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
*
* https://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 de.terrestris.shogun.lib.envers;

import org.springframework.data.history.RevisionMetadata;
import org.springframework.data.util.AnnotationDetectionFieldCallback;
import org.springframework.data.util.Lazy;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;

import java.lang.annotation.Annotation;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.Date;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;

/**
* A {@link RevisionMetadata} implementation that inspects the given object for fields with the configured annotations
* and returns the field's values on calls to {@link #getRevisionInstant()} and
* {@link #getRevisionNumber()}.
*
* @author Oliver Gierke
* @author Jens Schauder
*/
public class ShogunAnnotationRevisionMetadata<N extends Number & Comparable<N>> implements RevisionMetadata<N> {

private final Object entity;
private final Lazy<Optional<N>> revisionNumber;
private final Lazy<Optional<Object>> revisionDate;
private final RevisionType revisionType;
private final Set<String> changedFields;

/**
* Creates a new {@link ShogunAnnotationRevisionMetadata} inspecting the given entity for the given annotations. If no
* annotations will be provided these values will not be looked up from the entity and return {@literal null}. The
* revisionType will be set to {@literal unknown}
*
* @param entity must not be {@literal null}.
* @param revisionNumberAnnotation must not be {@literal null}.
* @param revisionTimeStampAnnotation must not be {@literal null}.
*/
public ShogunAnnotationRevisionMetadata(Object entity, Class<? extends Annotation> revisionNumberAnnotation,
Class<? extends Annotation> revisionTimeStampAnnotation) {

this(entity, revisionNumberAnnotation, revisionTimeStampAnnotation, RevisionType.UNKNOWN, new HashSet<>());
}

/**
* Creates a new {@link org.springframework.data.history.AnnotationRevisionMetadata} inspecting the given entity for the given annotations. If no
* annotations will be provided these values will not be looked up from the entity and return {@literal null}.
*
* @param entity must not be {@literal null}.
* @param revisionNumberAnnotation must not be {@literal null}.
* @param revisionTimeStampAnnotation must not be {@literal null}.
* @param revisionType must not be {@literal null}.
* @since 2.2.0
*/
public ShogunAnnotationRevisionMetadata(Object entity, Class<? extends Annotation> revisionNumberAnnotation,
Class<? extends Annotation> revisionTimeStampAnnotation, RevisionType revisionType, Set<String> changedFields) {

Assert.notNull(entity, "Entity must not be null!");
Assert.notNull(revisionNumberAnnotation, "Revision number annotation must not be null!");
Assert.notNull(revisionTimeStampAnnotation, "Revision time stamp annotation must not be null!");
Assert.notNull(revisionType, "Revision Type must not be null!");
Assert.notNull(changedFields, "Changed fields must not be null!");

this.entity = entity;
this.revisionNumber = detectAnnotation(entity, revisionNumberAnnotation);
this.revisionDate = detectAnnotation(entity, revisionTimeStampAnnotation);
this.revisionType = revisionType;
this.changedFields = changedFields;
}

/*
* (non-Javadoc)
* @see org.springframework.data.repository.history.RevisionMetadata#getRevisionNumber()
*/
public Optional<N> getRevisionNumber() {
return revisionNumber.get();
}

/*
* (non-Javadoc)
* @see org.springframework.data.history.RevisionMetadata#getRevisionDate()
*/
public Optional<Instant> getRevisionInstant() {
return revisionDate.get().map(ShogunAnnotationRevisionMetadata::convertToInstant);
}

/*
* (non-Javadoc)
* @see org.springframework.data.history.RevisionMetadata#getRevisionDate()
*/
public RevisionType getRevisionType() {
return revisionType;
}

/*
* (non-Javadoc)
* @see org.springframework.data.history.RevisionMetadata#getRevisionDate()
*/
public Set<String> getChangedFields() {
return changedFields;
}

/*
* (non-Javadoc)
* @see org.springframework.data.repository.history.RevisionMetadata#getDelegate()
*/
@SuppressWarnings("unchecked")
public <T> T getDelegate() {
return (T) entity;
}

private static <T> Lazy<Optional<T>> detectAnnotation(Object entity, Class<? extends Annotation> annotationType) {

return Lazy.of(() -> {
AnnotationDetectionFieldCallback callback = new AnnotationDetectionFieldCallback(annotationType);
ReflectionUtils.doWithFields(entity.getClass(), callback);
return Optional.ofNullable(callback.getValue(entity));
});
}

private static Instant convertToInstant(Object timestamp) {

if (timestamp instanceof Instant) {
return (Instant) timestamp;
}

if (timestamp instanceof LocalDateTime) {
return ((LocalDateTime) timestamp).atZone(ZoneOffset.systemDefault()).toInstant();
}

if (timestamp instanceof Long) {
return Instant.ofEpochMilli((Long) timestamp);
}

if (Date.class.isInstance(timestamp)) {
return Date.class.cast(timestamp).toInstant();
}

throw new IllegalArgumentException(String.format("Can't convert %s to Instant!", timestamp));
}
}
Loading

0 comments on commit 0ca106d

Please sign in to comment.