feat(core): audit implementation#69
Conversation
db2e766 to
acac340
Compare
Code Coverage OverviewLanguages: Java Java / code-coverage/jacocoThe 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.
Updated |
b7be593 to
3a4e46a
Compare
|
| '*/*': | ||
| schema: | ||
| $ref: '#/components/schemas/ErrorResponse' | ||
| /api/v1/audit/entities/{templateIdentifier}/{entityIdentifier}: |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
Revinfo ? Sounds not very clear to me
There was a problem hiding this comment.
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") |
There was a problem hiding this comment.
You can't stay with the full name ? For clarity
There was a problem hiding this comment.
@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; |
There was a problem hiding this comment.
Instead of SEQUENCE you can use a IDENTITY column type but it's up to you 😃
There was a problem hiding this comment.
the two is ok for me but to be iso with actual archi i changed it to identity
There was a problem hiding this comment.
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 ?
There was a problem hiding this comment.
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 |
| // OAuth2 and OpenId case | ||
| if (authentication.getPrincipal()instanceof OAuth2User oauth2Token) { | ||
|
|
| 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); | ||
| } |
| @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; |
| The audit system is transparent—no special configuration is needed. Every operation is tracked automatically using | ||
| Hibernate . |
| @Test | ||
| void shouldThrowExceptionWhenMockIsDisabled() { | ||
| contextRunner.withPropertyValues("app.security.mock-enabled=false"); | ||
| contextRunner.run(context -> { | ||
| assertThat(context).doesNotHaveBean(MockSecurityConfiguration.class); | ||
| assertThat(context).doesNotHaveBean("securityFilterChainMock"); | ||
| }); | ||
| } |
| 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); | ||
|
|
| /// @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, |
| @Bean | ||
| @Primary | ||
| UserIdentityProvider userIdentityProvider() { | ||
| var provider = mock(UserIdentityProvider.class); | ||
| when(provider.getAuthId()).thenReturn("test-user"); | ||
| when(provider.getName()).thenReturn("Test User"); | ||
| return provider; | ||
| } |
| .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")); |



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.
Key Architectures & Feature Additions:
hibernate-enversto capture and persist historical snapshots into matching database target tables (*_aud) on all core entity transactions.spring-data-enversand updated repositories (JpaEntityRepository, etc.) to extendRevisionRepository. This unlocks access to the full timeline history natively without relying on complex handwritten native JOIN operations.EntityTrackingRevisionListenercoupled with@ElementCollectionmapping to dynamically log the precise list of elements modified inside a single transaction unit.suborid) across JWT, OAuth2, and OpenID scopes.AnonymousAuthenticationToken) and missing context environments to avoid NullPointerExceptions (NPEs) when interacting with unauthenticated callers, background jobs, or malicious scanner traffic.*_MOD), and deploying optimal query indexes to support scale performance.Review
The reviewer must double-check these points :
!after the type/scope to identify the breakingchange 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
POSTrequest to register a brand new entity (web-api-audit).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.
Action: Submitting a PUT request modifying structural parameter fields (e.g. updating baseUrl and port).
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.
Action: Issuing a DELETE command targeting the runtime asset item tracker.
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