Skip to content

Commit

Permalink
Audit user changes (#8137)
Browse files Browse the repository at this point in the history
* Audit user changes

* Audit user changes - update translations

* Add README.md file

* Update UserAuditable  model to track user address changes and add unit test for UserAuditable

* Audit user changes - change to use Hibernate Envers due to Javers dependency conflict with Geotools for the picocontainer version

* Audit user changes - update README.md documentation

* Code formatting and styling

* Update UsersApi to remove invalid user group assignment in the user update endpoint

---------

Co-authored-by: Juan Luis Rodríguez <[email protected]>
  • Loading branch information
josegar74 and juanluisrp authored Jan 16, 2025
1 parent 815191e commit f4890d5
Show file tree
Hide file tree
Showing 41 changed files with 1,526 additions and 49 deletions.
14 changes: 14 additions & 0 deletions auditable/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Auditable Module

The auditable module contains the classes that allow auditing changes in user information using [Hibernate Envers](https://hibernate.org/orm/envers/).

Support for new auditable entities can be added, for example to audit changes in group information. For users auditing:

- Entity with the information to audit: [UserAuditable](../domain/src/main/java/org/fao/geonet/domain/auditable/UserAuditable.java).
- Related JPA repository: [UserAuditableRepository](../domain/src/main/java/org/fao/geonet/repository/UserAuditableRepository.java).
- The auditable service: [UserAuditableService](src/main/java/org/fao/geonet/auditable/UserAuditableService.java).
- The users API updated to use the auditable service: [UserApi](../services/src/main/java/org/fao/geonet/api/users/UsersApi.java).




51 changes: 51 additions & 0 deletions auditable/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.geonetwork-opensource</groupId>
<artifactId>geonetwork</artifactId>
<version>4.4.7-SNAPSHOT</version>
</parent>

<!-- =========================================================== -->
<!-- Module Description -->
<!-- =========================================================== -->
<artifactId>gn-auditable</artifactId>
<packaging>jar</packaging>
<name>GeoNetwork auditable objects</name>

<licenses>
<license>
<name>General Public License (GPL)</name>
<url>http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt</url>
<distribution>repo</distribution>
</license>
</licenses>


<dependencies>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>gn-domain</artifactId>
<version>${project.version}</version>
</dependency>

<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>gn-core</artifactId>
<version>${project.version}</version>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
</dependency>
</dependencies>

<properties>
<rootProjectDir>${basedir}/..</rootProjectDir>
</properties>

</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
/*
* Copyright (C) 2001-2024 Food and Agriculture Organization of the
* United Nations (FAO-UN), United Nations World Food Programme (WFP)
* and United Nations Environment Programme (UNEP)
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or (at
* your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
*
* Contact: Jeroen Ticheler - FAO - Viale delle Terme di Caracalla 2,
* Rome - Italy. email: [email protected]
*/
package org.fao.geonet.auditable;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.MapDifference;
import com.google.common.collect.Maps;
import org.fao.geonet.auditable.model.RevisionFieldChange;
import org.fao.geonet.auditable.model.RevisionInfo;
import org.fao.geonet.domain.auditable.AuditableEntity;
import org.fao.geonet.kernel.setting.SettingManager;
import org.fao.geonet.kernel.setting.Settings;
import org.fao.geonet.repository.BaseAuditableRepository;
import org.springframework.data.history.Revision;
import org.springframework.data.history.Revisions;
import org.springframework.util.StringUtils;

import java.util.*;

public abstract class BaseAuditableService<U> {
protected static final String LINE_SEPARATOR = System.lineSeparator();

protected BaseAuditableRepository<U> repository;
protected SettingManager settingManager;

public abstract String getEntityType();

public void auditSave(U auditableEntity) {
if (!isAuditableEnabled()) return;

repository.save(auditableEntity);
}

public void auditDelete(U auditableEntity) {
if (!isAuditableEnabled()) return;

repository.delete(auditableEntity);
}

public String getEntityHistoryAsString(Integer entityIdentifier, ResourceBundle messages) {
if (!isAuditableEnabled()) return "";

Revisions<Integer, U> revisions = repository.findRevisions(entityIdentifier);

return retrieveRevisionHistoryAsString(revisions, messages);
}

public List<RevisionInfo> getEntityHistory(Integer entityIdentifier) {
if (!isAuditableEnabled()) return new ArrayList<>();

Revisions<Integer, U> revisions = repository.findRevisions(entityIdentifier);

return retrieveRevisionHistory(revisions);
}


protected String retrieveRevisionHistoryAsString(Revisions<Integer, U> revisions, ResourceBundle messages) {
List<RevisionInfo> revisionInfoList = retrieveRevisionHistory(revisions);

List<String> diffs = new ArrayList<>();

revisionInfoList.stream().forEach(revision -> {
List<String> revisionChanges = new ArrayList<>();
revisionChanges.add(revision.getValue());

revision.getChanges().forEach(change -> {
boolean oldValueIsDefined = StringUtils.hasLength(change.getOldValue());
boolean newValueIsDefined = StringUtils.hasLength(change.getNewValue());

if (oldValueIsDefined && newValueIsDefined) {
revisionChanges.add(String.format(messages.getString("audit.revision.field.updated"),
change.getName(), change.getOldValue(), change.getNewValue()));
} else if (!oldValueIsDefined && newValueIsDefined) {
revisionChanges.add(String.format(messages.getString("audit.revision.field.set"),
change.getName(), change.getNewValue()));
} else if (oldValueIsDefined && !newValueIsDefined) {
revisionChanges.add(String.format(messages.getString("audit.revision.field.unset"), change.getName()));
}
});

String revisionInfo = String.format(messages.getString("audit.revision"),
revision.getUser(),
revision.getDate(),
String.join(LINE_SEPARATOR, revisionChanges));

diffs.add(revisionInfo);

});

return String.join(LINE_SEPARATOR, diffs);
}

protected List<RevisionInfo> retrieveRevisionHistory(Revisions<Integer, U> revisions) {
String idFieldName = "id";
List<Revision<Integer, U>> revisionList = revisions.toList();
int numRevisions = revisions.toList().size();

List<RevisionInfo> revisionInfoList = new ArrayList<>();

if (numRevisions > 0) {
Revision<Integer, U> initialRevision = revisionList.get(0);
AuditableEntity initialRevisionEntity = (AuditableEntity) initialRevision.getEntity();

// Initial revision
ObjectMapper objectMapper = new ObjectMapper();
Map<String, Object> revisionMap = objectMapper.convertValue(initialRevision.getEntity(), Map.class);
// Remove empty values and id
revisionMap.values().removeAll(Arrays.asList("", null));
revisionMap.remove(idFieldName);

RevisionInfo initialRevisionInfo = new RevisionInfo(
initialRevision.getMetadata().getRequiredRevisionNumber(),
initialRevisionEntity.getCreatedBy(),
initialRevisionEntity.getCreatedDate(), revisionMap.toString());

revisionInfoList.add(initialRevisionInfo);

int i = 0;
while (i + 1 < numRevisions) {
Revision<Integer, U> revision1 = revisionList.get(i);
Revision<Integer, U> revision2 = revisionList.get(i + 1);

Map<String, Object> revision1Map = objectMapper.convertValue(revision1.getEntity(), Map.class);
revision1Map.remove(idFieldName);
Map<String, Object> revision2Map = objectMapper.convertValue(revision2.getEntity(), Map.class);
revision2Map.remove(idFieldName);

MapDifference<String, Object> diff = Maps.difference(revision1Map, revision2Map);

revision2Map.values().removeAll(Arrays.asList("", null));

final RevisionInfo revisionInfo = new RevisionInfo(
revision2.getMetadata().getRequiredRevisionNumber(),
((AuditableEntity) revision2.getEntity()).getLastModifiedBy(),
((AuditableEntity) revision2.getEntity()).getLastModifiedDate(),
revision2Map.toString());

diff.entriesDiffering().forEach((key, entry) -> {
String oldValueAsString = (entry.leftValue() != null) ? entry.leftValue().toString() : "";
String newValueAsString = (entry.rightValue() != null) ? entry.rightValue().toString() : "";

RevisionFieldChange revisionFieldChange = new RevisionFieldChange(key, oldValueAsString, newValueAsString);

revisionInfo.addChange(revisionFieldChange);
});


revisionInfoList.add(revisionInfo);
i++;
}
}

Collections.reverse(revisionInfoList);
return revisionInfoList;
}

protected boolean isAuditableEnabled() {
return settingManager.getValueAsBool(Settings.SYSTEM_AUDITABLE_ENABLE, false);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* Copyright (C) 2001-2024 Food and Agriculture Organization of the
* United Nations (FAO-UN), United Nations World Food Programme (WFP)
* and United Nations Environment Programme (UNEP)
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or (at
* your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
*
* Contact: Jeroen Ticheler - FAO - Viale delle Terme di Caracalla 2,
* Rome - Italy. email: [email protected]
*/

package org.fao.geonet.auditable;

import org.fao.geonet.domain.auditable.UserAuditable;
import org.fao.geonet.kernel.setting.SettingManager;
import org.fao.geonet.repository.UserAuditableRepository;
import org.springframework.stereotype.Service;

@Service
public class UserAuditableService extends BaseAuditableService<UserAuditable> {

public static final String ENTITY_TYPE = "user";

public UserAuditableService(SettingManager settingManager, UserAuditableRepository repository) {
this.settingManager = settingManager;
this.repository = repository;
}

@Override
public String getEntityType() {
return ENTITY_TYPE;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* Copyright (C) 2001-2024 Food and Agriculture Organization of the
* United Nations (FAO-UN), United Nations World Food Programme (WFP)
* and United Nations Environment Programme (UNEP)
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or (at
* your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
*
* Contact: Jeroen Ticheler - FAO - Viale delle Terme di Caracalla 2,
* Rome - Italy. email: [email protected]
*/
package org.fao.geonet.auditable.model;

/**
* This class represents a change in an entity field. It stores the field name,
* the previous and the new value.
*/
public class RevisionFieldChange {
private final String name;
private final String oldValue;
private final String newValue;

public RevisionFieldChange(String name, String oldValue, String newValue) {
this.name = name;
this.oldValue = oldValue;
this.newValue = newValue;
}

public String getName() {
return name;
}

public String getOldValue() {
return oldValue;
}

public String getNewValue() {
return newValue;
}
}
Loading

0 comments on commit f4890d5

Please sign in to comment.