Application Auditing

Application Auditing

Overview

In this tutorial, I will show you how to implement auditing in your application. Some aspects of the implementation, which are prerequisites for executing this tutorial, are beyond its scope but will be covered at a later stage.

Tutorial Overview

This tutorial explores the process of auditing an application, focusing on identifying the creator of a record, the creation timestamp, and the originating IP address. Additionally, if modifications have been made, it will detail the user involved, the modification timestamp, and the corresponding IP address. In the concluding section, I will introduce how the API Javers functions, providing a comprehensive snapshot of all records created and modified. This feature is invaluable when you need to revert to or review the original state of a record.

Project Setup

To set up the project, check out the demo project and run the following commands:

git clone git@github.com:softwarebuilding-io/application-auditing.git

After cloning the application, run the following command to create the tables:

mvn clean compile -P docker

Import your application into your favorite IDE, then run the application.

After the application starts, please check the following URL: http://localhost:8080/fusion-plex/. The login details are:

  • Username: user
  • Password: User@1234

Step 1: Updating the SQL Script

We will start by updating the SQL script that creates the tables. This script will include the following columns in the genre and title tables:

  • created_ip: IP of the user who created the record
  • created_on: Timestamp when the record was created
  • created_user: Username of the person who created the record
  • updated_ip: IP of the user who updated the record
  • updated_on: Timestamp when the record was updated
  • updated_user: Username of the person who updated the record

Open the create-tables.sql file and update it as follows:

CREATE TABLE public.genre
(
    id           UUID    NOT NULL DEFAULT uuid_generate_v4(),
    name         VARCHAR(255)    NOT NULL UNIQUE,
    created_ip   VARCHAR(15)     NOT NULL DEFAULT '127.0.0.1',
    created_on   TIMESTAMP       NOT NULL DEFAULT CURRENT_TIMESTAMP,
    created_user VARCHAR(60)     NOT NULL DEFAULT 'ADMIN',
    updated_ip   VARCHAR(15),
    updated_on   TIMESTAMP,
    updated_user VARCHAR(60),
    PRIMARY KEY  (id)
);

CREATE TABLE public.title
(
    id           UUID            NOT NULL DEFAULT uuid_generate_v4(),
    name         VARCHAR(255)    NOT NULL UNIQUE,
    description  VARCHAR(255)    NOT NULL,
    release_year SMALLINT        NOT NULL,
    type         INTEGER         NOT NULL,
    created_ip   VARCHAR(15)     NOT NULL DEFAULT '127.0.0.1',
    created_on   TIMESTAMP       NOT NULL DEFAULT CURRENT_TIMESTAMP,
    created_user VARCHAR(60)     NOT NULL DEFAULT 'ADMIN',
    updated_ip   VARCHAR(15),
    updated_on   TIMESTAMP,
    updated_user VARCHAR(60),
    PRIMARY KEY  (id)
);

Finally, run the following command to update the database with the new columns:

mvn clean compile -P docker

Step 2: Base Entity

Next, we will update the Base Entity and add the following attributes and methods. All concrete entities will inherit from the Base Entity, which is a best practice to avoid recreating the same functionality throughout the application.

package io.softwarebuilding.fusionplex.entity;

import io.softwarebuilding.fusionplex.util.IpUtil;
import io.softwarebuilding.fusionplex.util.LoginUtil;
import jakarta.persistence.*;

import java.io.Serial;
import java.io.Serializable;
import java.time.ZonedDateTime;

@MappedSuperclass
public abstract class BaseEntity implements Serializable {

    @Serial
    private static final long serialVersionUID = -33114089982657335L;

    @Column(name = "created_on", nullable = false)
    private ZonedDateTime createdOn;

    @Column(name = "created_ip", nullable = false, length = 15)
    private String createdIp;

    @Column(name = "created_user", nullable = false, length = 60)
    private String createdUser;

    @Column(name = "updated_on")
    private ZonedDateTime updatedOn;

    @Column(name = "updated_ip", length = 15)
    private String updatedIp;

    @Column(name = "updated_user", length = 60)
    private String updatedUser;

    // Getter and setter methods for each field

    /**
     * PrePersist method for auditing purposes.
     * Populates created_* fields before a new record is persisted.
     */
    @PrePersist
    public void prePersist() {
        this.createdUser = getAuthenticatedUser();
        this.createdOn = ZonedDateTime.now();
        this.createdIp = getClientIpAddress();
    }

    /**
     * PreUpdate method for auditing purposes.
     * Populates updated_* fields before an existing record is updated.
     */
    @PreUpdate
    public void preUpdate() {
        this.updatedUser = getAuthenticatedUser();
        this.updatedOn = ZonedDateTime.now();
        this.updatedIp = getClientIpAddress();
    }

    private String getAuthenticatedUser() {
        return LoginUtil.getAuthenticatedUser();
    }

    private String getClientIpAddress() {
        return IpUtil.getClientIpAddress();
    }
}

In this section, we have streamlined the SQL script and Java code to clearly align the database schema with the application’s auditing logic.

Step 3: Adding Javers Dependency

Javers is an open-source library used for auditing your application. It provides a comprehensive view of the entire lifecycle of a record, tracking initial changes and maintaining snapshots of all modifications. This allows you to revert to the very first version of a record. To include Javers in your project, add the following dependency to your pom.xml file:

<dependency>
    <groupId>org.javers</groupId>
    <artifactId>javers-spring-boot-starter-sql</artifactId>
    <version>7.3.8</version>
</dependency>

Step 4: Creating the Audit Layers

We will begin by creating the AuditService layer, which will interact with the Javers API. Note that there is no need for a repository layer because Javers directly interacts with the database.

package io.softwarebuilding.fusionplex.service;

import io.softwarebuilding.fusionplex.entity.Genre;
import io.softwarebuilding.fusionplex.entity.Title;
import org.javers.core.Javers;
import org.javers.core.diff.Change;
import org.javers.repository.jql.QueryBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class AuditService {

    private final Javers javers;

    @Autowired
    public AuditService(Javers javers) {
        this.javers = javers;
    }

    public String getGenresChanges() {
        QueryBuilder jqlQuery = QueryBuilder.byClass(Genre.class);
        List<Change> changes = this.javers.findChanges(jqlQuery.build());
        return this.javers.getJsonConverter().toJson(changes);
    }

    public String getTitleChanges() {
        QueryBuilder jqlQuery = QueryBuilder.byClass(Title.class);
        List<Change> changes = this.javers.findChanges(jqlQuery.build());
        return this.javers.getJsonConverter().toJson(changes);
    }
}

Next, we will implement the AuditController layer, which will display all the audit data on the screen.

package io.softwarebuilding.fusionplex.controller;

import io.softwarebuilding.fusionplex.service.AuditService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class AuditController {

    private final AuditService auditService;

    @Autowired
    public AuditController(AuditService auditService) {
        this.auditService = auditService;
    }

    @GetMapping("/admin/audit/genres")
    public String showAuditGenresPage(Model model) {
        String changes = this.auditService.getGenresChanges();
        model.addAttribute("changes", changes);
        return "auditGenre";
    }

    @GetMapping("/admin/audit/titles")
    public String showAuditTitlesPage(Model model) {
        String changes = this.auditService.getTitleChanges();
        model.addAttribute("changes", changes);
        return "auditTitle";
    }
}

Next, we will create the auditGenre.html and auditTitle.html files, which are the actual UIs to display to the user. These files include placeholders for your HTML code, so ensure you replace them with actual content and path references.

Step 5: Adding Javers Annotation

Finally, to enable Javers auditing for your entities, add the @JaversSpringDataAuditable annotation to all the repositories you want audited. Below are examples for Genre and Title entities:

package io.softwarebuilding.fusionplex.repository;

import io.softwarebuilding.fusionplex.entity.Genre;
import org.javers.spring.annotation.JaversSpringDataAuditable;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;
import java.util.UUID;

@JaversSpringDataAuditable
public interface GenreRepository extends JpaRepository<Genre, UUID> {
    Optional<Genre> findByName(String name);
}

package io.softwarebuilding.fusionplex.repository;

import io.softwarebuilding.fusionplex.entity.Title;
import org.javers.spring.annotation.JaversSpringDataAuditable;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.UUID;

@JaversSpringDataAuditable
public interface TitleRepository extends JpaRepository<Title, UUID> {
}

With these annotations in place, both Genre and Title entities will have all their records audited.

Conclusion

Congratulations on completing this tutorial on implementing auditing in your application using Javers. You’ve learned how to integrate Javers into your Spring Boot application, manage changes with service and controller layers, and ensure changes to your entities are auditable.

Auditing is essential for maintaining data integrity and transparency, enhancing security, and complying with data governance standards. This tutorial has equipped you with the tools to provide detailed insights into the lifecycle of records.

Feel free to adapt and expand these concepts for your specific project needs. Refer back to this guide whenever you need a refresher on auditing with Javers.

Happy coding!

Mobile Logo