Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Auditing: add tracking of modified fields #301

Merged
merged 10 commits into from
Apr 13, 2021
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