Skip to content

feat(core): audit implementation#69

Open
RVANDO12 wants to merge 5 commits into
mainfrom
feat/audit/envers
Open

feat(core): audit implementation#69
RVANDO12 wants to merge 5 commits into
mainfrom
feat/audit/envers

Conversation

@RVANDO12

@RVANDO12 RVANDO12 commented Jun 11, 2026

Copy link
Copy Markdown
Collaborator

PR Description

📝 Summary of Changes

This Pull Request introduces comprehensive database version control and auditing mechanisms across core entity tables using Hibernate Envers and Spring Data Envers.

image

Key Architectures & Feature Additions:

  1. Automated Audit Table Interception: Added hibernate-envers to capture and persist historical snapshots into matching database target tables (*_aud) on all core entity transactions.
  2. Spring Data Abstraction Layer: Integrated spring-data-envers and updated repositories (JpaEntityRepository, etc.) to extend RevisionRepository. This unlocks access to the full timeline history natively without relying on complex handwritten native JOIN operations.
  3. Advanced Transaction Metadata Tracking: Implemented an EntityTrackingRevisionListener coupled with @ElementCollection mapping to dynamically log the precise list of elements modified inside a single transaction unit.
  4. Resilient Security Context Audit Parsing: * Configured authentication tracking to extract identity mappings (sub or id) across JWT, OAuth2, and OpenID scopes.
    • Hardened Security Boundaries: Added protective filtering for anonymous users (AnonymousAuthenticationToken) and missing context environments to avoid NullPointerExceptions (NPEs) when interacting with unauthenticated callers, background jobs, or malicious scanner traffic.
  5. Schema Control Rigor: Established standard migration frameworks using Flyway to manually align shadow audit layout definitions, mapping boolean metadata tracking tags (*_MOD), and deploying optimal query indexes to support scale performance.

Review

The reviewer must double-check these points :

  • Schema Migration scripts verified manually on startup.
  • Dynamic auditing intercepts mapping data context synchronously.
  • Security identity parsing successfully outputs user string context labels without generating NPE anomalies.
  • The reviewer has tested the feature
  • The reviewer has reviewed the implementation of the feature
  • The documentation has been updated
  • The feature implementation respects the Technical Doc / ADR previously produced
  • The Pull Request title has a ! after the type/scope to identify the breaking
    change in the release note and ensure we will release a major version.

How to test

🧪 Local Testing & Verification Report

The feature set has been thoroughly validated against local environments using target endpoint APIs. The results below verify successful timeline serialization, event capture types, and active identity mapping logging.

1. Create Operation (Revision 1)

Action: Submitting a POST request to register a brand new entity (web-api-audit).

curl -X 'POST' \
  'http://localhost:8084/api/v1/entities/web-service' \
  -H 'accept: */*' \
  -H 'Content-Type: application/json' \
  -d '{
  "identifier": "web-api-audit",
  "name": "web-api-audit",
  "properties": {
    "port": "8080",
    "applicationName": "audit",
    "baseUrl": "[http://www.audit.fr](http://www.audit.fr)",
    "environment": "PROD",
    "ownerEmail": "audit@lazar.com",
    "programmingLanguage": "JAVA",
    "protocol": "HTTP",
    "teamName": "lazar", 
    "version": "1.1.1"
  },
  "relations": []
}'

Audit Trail Output (GET /api/v1/audit/entities/web-service/web-api-audit):

[
  {
    "modified_by": "local-developer",
    "revision_date": "2026-06-11T07:06:33.951Z",
    "revision_number": 1,
    "revision_type": "CREATED",
    "snapshot": {
      "id": "fc66faf8-b975-4829-83cd-f7b6c11ecb20",
      "identifier": "web-api-audit",
      "name": "web-api-audit",
      "properties": [
        {
          "id": "203dfa60-f775-4cae-8683-0a4bae46ea79",
          "name": "applicationName",
          "value": "toto"
        }
      ],
      "relations": [],
      "template_identifier": "web-service"
    }
  }
]

Verification Status: ✅ Passthrough Successful. revision_number: 1 created with revision_type: "CREATED". Context operator identity local-developer extracted properly.

  1. Update Operation (Revision 2)
    Action: Submitting a PUT request modifying structural parameter fields (e.g. updating baseUrl and port).
curl -X 'PUT' \
  'http://localhost:8084/api/v1/entities/web-service/web-api-audit' \
  -H 'accept: */*' \
  -H 'Content-Type: application/json' \
  -d '{
  "name": "web-api-audit",
  "properties": {
    "port": "8084",
    "applicationName": "audit",
    "baseUrl": "[http://www.audit.com](http://www.audit.com)",
    "environment": "PROD",
    "ownerEmail": "audit@lazar.com",
    "programmingLanguage": "JAVA",
    "protocol": "HTTP",
    "teamName": "lazar", 
    "version": "1.1.1"
  },
  "relations": []
}'

Audit Trail Output:

[
  {
    "modified_by": "local-developer",
    "revision_date": "2026-06-11T07:07:51.598Z",
    "revision_number": 2,
    "revision_type": "UPDATED",
    "snapshot": {
      "id": "fc66faf8-b975-4829-83cd-f7b6c11ecb20",
      "identifier": "web-api-audit",
      "name": "web-api-audit",
      "template_identifier": "web-service"
    }
  },
  {
    "modified_by": "local-developer",
    "revision_date": "2026-06-11T07:06:33.951Z",
    "revision_number": 1,
    "revision_type": "CREATED",
    "snapshot": {
      "id": "fc66faf8-b975-4829-83cd-f7b6c11ecb20",
      "identifier": "web-api-audit",
      "name": "web-api-audit",
      "properties": [
        {
          "id": "203dfa60-f775-4cae-8683-0a4bae46ea79",
          "name": "applicationName",
          "value": "toto"
        }
      ],
      "relations": [],
      "template_identifier": "web-service"
    }
  }
]

Verification Status: ✅ Passthrough Successful. System append logs a separate record with revision_number: 2 tagged as UPDATED.

  1. Delete Operation (Revision 3)
    Action: Issuing a DELETE command targeting the runtime asset item tracker.
curl -X 'DELETE' \
  'http://localhost:8084/api/v1/entities/web-service/web-api-audit' \
  -H 'accept: */*'
  

Audit Trail Output:

[
  {
    "modified_by": "local-developer",
    "revision_date": "2026-06-11T07:08:52.525Z",
    "revision_number": 3,
    "revision_type": "DELETED",
    "snapshot": {
      "id": "fc66faf8-b975-4829-83cd-f7b6c11ecb20",
      "identifier": null,
      "name": null,
      "template_identifier": null
    }
  },
  {
    "modified_by": "local-developer",
    "revision_date": "2026-06-11T07:07:51.598Z",
    "revision_number": 2,
    "revision_type": "UPDATED",
    "snapshot": {
      "id": "fc66faf8-b975-4829-83cd-f7b6c11ecb20",
      "identifier": "web-api-audit",
      "name": "web-api-audit",
      "template_identifier": "web-service"
    }
  },
  {
    "modified_by": "local-developer",
    "revision_date": "2026-06-11T07:06:33.951Z",
    "revision_number": 1,
    "revision_type": "CREATED",
    "snapshot": {
      "id": "fc66faf8-b975-4829-83cd-f7b6c11ecb20",
      "identifier": "web-api-audit",
      "name": "web-api-audit",
      "template_identifier": "web-service"
    }
  }
]

Verification Status: ✅ Passthrough Successful. Final chronological list accurately outputs DELETED state logic (revision_number: 3) with structural string identifiers reset gracefully to null, while maintaining the immutable historical data snapshots from earlier transactions.

Breaking changes (if any)

N/A

@RVANDO12 RVANDO12 force-pushed the feat/audit/envers branch 2 times, most recently from db2e766 to acac340 Compare June 11, 2026 10:48
@github-code-quality

github-code-quality Bot commented Jun 11, 2026

Copy link
Copy Markdown

Code Coverage Overview

Languages: Java

Java / code-coverage/jacoco

The overall coverage in the branch is 89%. Coverage data for the branch is not yet available.

Show a code coverage summary of the most covered files.
File 2f7115f +/-
com/decathlon/i...rDslParser.java 99%
com/decathlon/i...ionService.java 99%
com/decathlon/i...ityService.java 96%
com/decathlon/i...MapperImpl.java 90%
com/decathlon/i...MapperImpl.java 88%
com/decathlon/i...ionHandler.java 87%
com/decathlon/i...ionService.java 87%
com/decathlon/i...oOutMapper.java 86%
com/decathlon/i...cification.java 81%
com/decathlon/i...ionService.java 77%

Updated June 11, 2026 14:22 UTC
Code Coverage is in Public Preview. Learn more and provide us with your feedback.

@RVANDO12 RVANDO12 force-pushed the feat/audit/envers branch from b7be593 to 3a4e46a Compare June 11, 2026 13:01
@sonarqubecloud

Copy link
Copy Markdown

'*/*':
schema:
$ref: '#/components/schemas/ErrorResponse'
/api/v1/audit/entities/{templateIdentifier}/{entityIdentifier}:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We will also probably need a GET /api/v1/audit/entities/{templateIdentifier} to get all the events for a particular entity_template on all the entities to see global events. But let keep it for later regarding product requirements

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes the task is only to expose entity audit , but i take the point and create other necessary task for entity template and properties ( need for following )

import jakarta.persistence.Embeddable;

@Embeddable
public class CustomRevinfoRecord {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Revinfo ? Sounds not very clear to me

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

my bad is a name give by hibernate envers, but when checking i found that i can be more clear i changed it

private long rev;

@RevisionTimestamp
@Column(name = "revtstmp")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can't stay with the full name ? For clarity

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@RevisionTimestamp
@column(name = "revision_timestamp")
private long revisionTimestamp;

-- Indexes are created on the revision number and other relevant columns to optimize query performance when retrieving audit data.
-- Note: The actual structure of the audit tables may need to be adjusted based on the specific entities and properties used in the application, but this provides a general framework for implementing Envers auditing in a PostgreSQL database.

CREATE SEQUENCE envers_seq START WITH 1 INCREMENT BY 50;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of SEQUENCE you can use a IDENTITY column type but it's up to you 😃

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the two is ok for me but to be iso with actual archi i changed it to identity

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You may also add a section in the contributing section on "how to add an audit log for your object"?
Like what are the steps to implement it (decorator, flyway, ...)

  • update instructions or skills related to this for the IA ?

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces entity audit/history capabilities to IDP-Core using Hibernate Envers, exposing a new REST endpoint to retrieve revision history, and adding supporting schema, security identity extraction, tests, and documentation.

Changes:

  • Add Envers audit schema via Flyway and enable Envers delete snapshot storage.
  • Add audit retrieval flow (domain port/service + persistence adapter + REST controller + DTOs/mapper).
  • Add identity extraction abstraction (UserIdentityProvider) plus local mock security support, and update tests/docs accordingly.

Reviewed changes

Copilot reviewed 43 out of 45 changed files in this pull request and generated 14 comments.

Show a summary per file
File Description
src/test/resources/integration_test/json/audit/v1/getAudit_200_history_update.json Test JSON payload for audit update scenario
src/test/resources/integration_test/json/audit/v1/getAudit_200_history_create.json Test JSON payload for audit create scenario
src/test/resources/db/test/R__2_Insert_entities_test_data.sql Adjust test entity seed data ordering/content
src/test/resources/db/test/R__1_Insert_test_data.sql Add audited template seed data for tests
src/test/java/com/decathlon/idp_core/infrastructure/adapters/persistence/model/audit/CustomRevisionListenerTest.java Unit tests for Envers revision listener behavior
src/test/java/com/decathlon/idp_core/infrastructure/adapters/api/controller/EntityTemplateControllerTest.java Update expected template counts due to new seed template
src/test/java/com/decathlon/idp_core/infrastructure/adapters/api/controller/AuditControllerTest.java New integration tests for audit endpoint
src/test/java/com/decathlon/idp_core/infrastructure/adapters/api/auth/UnifiedUserProviderTest.java Unit tests for unified identity extraction
src/test/java/com/decathlon/idp_core/infrastructure/adapters/api/auth/mock/MockSecurityConfigurationTest.java Tests for mock security configuration/filter
src/test/java/com/decathlon/idp_core/AbstractIntegrationTest.java Test configuration tweaks for integration tests
src/main/resources/db/migration/V4_1__create_envers_audit_schema.sql New Flyway migration creating Envers audit tables
src/main/resources/application.yml Enable Envers delete snapshot storage
src/main/resources/application-local.yml Local security/mock-security configuration updates
src/main/java/com/decathlon/idp_core/infrastructure/adapters/persistence/repository/JpaRelationRepository.java Add revision repository support
src/main/java/com/decathlon/idp_core/infrastructure/adapters/persistence/repository/JpaEntityTemplateRepository.java Add revision repository support
src/main/java/com/decathlon/idp_core/infrastructure/adapters/persistence/repository/JpaEntityRepository.java Add revision repository support
src/main/java/com/decathlon/idp_core/infrastructure/adapters/persistence/PostgresEntityAuditAdapter.java New persistence adapter to query audit history
src/main/java/com/decathlon/idp_core/infrastructure/adapters/persistence/model/entity/RelationJpaEntity.java Enable auditing on relation entity
src/main/java/com/decathlon/idp_core/infrastructure/adapters/persistence/model/entity/PropertyRulesJpaEntity.java Enable auditing on property rules entity
src/main/java/com/decathlon/idp_core/infrastructure/adapters/persistence/model/entity/PropertyJpaEntity.java Enable auditing on property entity
src/main/java/com/decathlon/idp_core/infrastructure/adapters/persistence/model/entity/EntityJpaEntity.java Enable auditing on entity entity
src/main/java/com/decathlon/idp_core/infrastructure/adapters/persistence/model/entity_template/RelationDefinitionJpaEntity.java Enable auditing on relation definition entity
src/main/java/com/decathlon/idp_core/infrastructure/adapters/persistence/model/entity_template/PropertyDefinitionJpaEntity.java Enable auditing on property definition entity
src/main/java/com/decathlon/idp_core/infrastructure/adapters/persistence/model/entity_template/EntityTemplateJpaEntity.java Enable auditing on entity template entity
src/main/java/com/decathlon/idp_core/infrastructure/adapters/persistence/model/audit/UserIdentityProviderHolder.java Bridge Envers listener to Spring identity provider
src/main/java/com/decathlon/idp_core/infrastructure/adapters/persistence/model/audit/CustomRevisionListener.java Custom Envers revision listener implementation
src/main/java/com/decathlon/idp_core/infrastructure/adapters/persistence/model/audit/CustomRevisionEntity.java Custom Envers revision entity mapping
src/main/java/com/decathlon/idp_core/infrastructure/adapters/persistence/model/audit/CustomRevinfoRecord.java Embeddable record for per-transaction changed-entity metadata
src/main/java/com/decathlon/idp_core/infrastructure/adapters/api/mapper/entity/EntityAuditDtoOutMapper.java Map domain audit model to API DTO
src/main/java/com/decathlon/idp_core/infrastructure/adapters/api/dto/out/entity/audit/EntitySnapshotDtoOut.java New audit snapshot DTO
src/main/java/com/decathlon/idp_core/infrastructure/adapters/api/dto/out/entity/audit/EntityAuditDtoOut.java New audit response DTO
src/main/java/com/decathlon/idp_core/infrastructure/adapters/api/controller/AuditController.java New REST endpoint to retrieve audit history
src/main/java/com/decathlon/idp_core/infrastructure/adapters/api/configuration/SwaggerDescription.java Swagger constants for audit endpoint
src/main/java/com/decathlon/idp_core/infrastructure/adapters/api/configuration/SecurityConfiguration.java Conditional load when mock security is disabled
src/main/java/com/decathlon/idp_core/infrastructure/adapters/api/auth/UserIdentityProvider.java New abstraction for identity lookup
src/main/java/com/decathlon/idp_core/infrastructure/adapters/api/auth/UnifiedUserProvider.java Implementation of identity extraction from SecurityContext
src/main/java/com/decathlon/idp_core/infrastructure/adapters/api/auth/mock/MockSecurityConfiguration.java Local mock JWT security filter chain
src/main/java/com/decathlon/idp_core/infrastructure/adapters/api/auth/mock/exception/MockSecurityConfigurationException.java Specific exception for mock security setup failures
src/main/java/com/decathlon/idp_core/domain/service/entity/EntityAuditService.java Domain service to retrieve audit history
src/main/java/com/decathlon/idp_core/domain/port/audit/EntityAuditPort.java Domain port for audit retrieval
src/main/java/com/decathlon/idp_core/domain/model/entity/EntityAuditInfo.java Domain audit model record
pom.xml Add Envers dependencies
docs/src/static/swagger.yaml Document new audit endpoint + schemas
docs/src/concepts/index.md Link audit concept page from concepts index
docs/src/concepts/audit.md New audit feature documentation

Comment on lines +37 to +39
// OAuth2 and OpenId case
if (authentication.getPrincipal()instanceof OAuth2User oauth2Token) {

Comment on lines +45 to +69
private UUID getEntityId(String templateIdentifier, String entityIdentifier) {

var entity = jpaEntityRepository.findByTemplateIdentifierAndIdentifier(templateIdentifier,
entityIdentifier);
if (entity.isPresent()) {
return entity.get().getId();
}

AuditReader auditReader = AuditReaderFactory.get(entityManager);

@SuppressWarnings("unchecked")
List<Object[]> revisions = auditReader.createQuery()
.forRevisionsOfEntity(EntityJpaEntity.class, false, true).getResultList();

for (Object[] revision : revisions) {
EntityJpaEntity auditedEntity = (EntityJpaEntity) revision[0];
if (auditedEntity != null && templateIdentifier.equals(auditedEntity.getTemplateIdentifier())
&& entityIdentifier.equals(auditedEntity.getIdentifier())) {
return auditedEntity.getId();
}
}

throw new IllegalArgumentException("Entity not found in current or audit data: "
+ templateIdentifier + "/" + entityIdentifier);
}
Comment on lines +34 to +47
@Override
public void entityChanged(final Class entityClass, final String entityName, final Object entityId,
final RevisionType revisionType, final Object revisionEntity) {
var customRevisionEntity = (CustomRevisionEntity) revisionEntity;

String auditTableName = entityClass.getSimpleName().replaceAll("([a-z])([A-Z]+)", "$1_$2")
.toLowerCase() + "_aud";

if (entityId instanceof UUID uuid) {
customRevisionEntity.addCustomRecord(uuid, auditTableName);
} else if (entityId instanceof String str) {
customRevisionEntity.addCustomRecord(UUID.fromString(str), auditTableName);
}
}
@Component
public class UserIdentityProviderHolder {

private static UserIdentityProvider userIdentityProvider;
Comment on lines +61 to +62
The audit system is transparent—no special configuration is needed. Every operation is tracked automatically using
Hibernate .
Comment on lines +95 to +102
@Test
void shouldThrowExceptionWhenMockIsDisabled() {
contextRunner.withPropertyValues("app.security.mock-enabled=false");
contextRunner.run(context -> {
assertThat(context).doesNotHaveBean(MockSecurityConfiguration.class);
assertThat(context).doesNotHaveBean("securityFilterChainMock");
});
}
Comment on lines +39 to +44
PRIMARY KEY (id, rev),
CONSTRAINT fk_entity_aud_revinfo FOREIGN KEY (rev) REFERENCES envers_transaction_log (rev) ON DELETE CASCADE
);

CREATE INDEX idx_entity_aud_rev ON entity_aud (rev);

Comment on lines +19 to +24
/// @param revisionNumber unique identifier of the revision in the audit log
/// @param revisionDate timestamp when the revision was created
/// @param revisionType type of operation performed (ADD, MOD, DEL)
/// @param modifiedBy identifier of the user who performed the modification
/// @param snapshot optional snapshot of the entity's state at the time of revision
public record EntityAuditInfo(Number revisionNumber, Instant revisionDate, String revisionType,
Comment on lines +252 to +259
@Bean
@Primary
UserIdentityProvider userIdentityProvider() {
var provider = mock(UserIdentityProvider.class);
when(provider.getAuthId()).thenReturn("test-user");
when(provider.getName()).thenReturn("Test User");
return provider;
}
Comment on lines +110 to +114
.perform(get(AUDIT_BASE_PATH + "/{templateIdentifier}/{entityIdentifier}",
templateIdentifier, entityIdentifier).with(csrf()))
.andExpect(status().isOk()).andExpect(jsonPath("$[0].revision_type").value("UPDATED"))
.andExpect(jsonPath("$[0].modified_by").value("test-user"))
.andExpect(jsonPath("$[0].snapshot.name").value("Audit Test Entity Updated"));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants