Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
404 changes: 404 additions & 0 deletions docs/src/concepts/audit.md

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 ?

Large diffs are not rendered by default.

10 changes: 8 additions & 2 deletions docs/src/concepts/index.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
---
title: Core Concepts
description: Understand the fundamental concepts of IDP-Core - Entity Templates, Entities, Properties, Relations
description: Understand the fundamental concepts of IDP-Core - Entity Templates, Entities, Properties, Relations, and Audit tracking
---

IDP-Core sits at the center of a flexible, runtime-configurable data model. This section explains the fundamental concepts you need to understand.
IDP-Core sits at the center of a flexible, runtime-configurable data model. This section explains the fundamental concepts you need to understand, including entity management and comprehensive audit tracking.

## Overview

Expand Down Expand Up @@ -55,6 +55,12 @@ graph TB

Query entities by attributes, property values, and relations using the filter DSL.

- πŸ“œ **[Audit](audit.md)**

---

Track all changes over time with comprehensive revision history and user attribution.

</div>

---
Expand Down
100 changes: 100 additions & 0 deletions docs/src/static/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ tags:
description: Operations related to entity management
- name: Entities Templates Management
description: Operations related to entity template management
- name: Audit
description: Operations related to audit history
paths:
/api/v1/entity-templates/{identifier}:
get:
Expand Down Expand Up @@ -461,6 +463,59 @@ paths:
'*/*':
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 )

get:
tags:
- Entities Audit
summary: Get entity audit history
Comment on lines +466 to +470
description: Retrieve the complete audit history for a specific entity,
including all revisions with timestamps and modification types
operationId: getEntityAuditHistory
parameters:
- name: templateIdentifier
in: path
required: true
schema:
type: string
minLength: 1
- name: entityIdentifier
in: path
required: true
schema:
type: string
minLength: 1
responses:
"200":
description: Successfully retrieved entity audit history
content:
"*/*":
schema:
type: array
items:
$ref: "#/components/schemas/EntityAuditDtoOut"
"400":
description: Invalid template or entity identifier
content:
"*/*":
schema:
$ref: "#/components/schemas/ErrorResponse"
"401":
description: Unauthorized - Missing or invalid token
"403":
description: Insufficient rights
"404":
description: Template not found with the provided identifier or Entity not found
with the provided identifier
content:
"*/*":
schema:
$ref: "#/components/schemas/ErrorResponse"
"500":
description: Unexpected server-side failure
content:
"*/*":
schema:
$ref: "#/components/schemas/ErrorResponse"
components:
schemas:
EntityTemplateUpdateDtoIn:
Expand Down Expand Up @@ -949,6 +1004,51 @@ components:
format: int32
empty:
type: boolean
EntityAuditDtoOut:
type: object
description: Audit information for an entity revision
properties:
revision_number:
type: number
description: Unique revision number in the audit log
example: 42
revision_date:
type: string
format: date-time
description: Timestamp when the revision was created
example: 2026-06-08T14:37:27.743Z
revision_type:
type: string
description: Type of operation performed (CREATED, UPDATED, DELETED)
example: UPDATED
modified_by:
type: string
description: Identifier of the user who performed the modification
example: user@example.com
snapshot:
$ref: "#/components/schemas/EntitySnapshotDtoOut"
description: Snapshot of the entity state at this revision
EntitySnapshotDtoOut:
type: object
description: Snapshot of entity state
properties:
id:
type: string
format: uuid
description: Unique identifier
example: 550e8400-e29b-41d4-a716-446655440000
template_identifier:
type: string
description: Template identifier
example: web-service
name:
type: string
description: Entity name
example: My Service
identifier:
type: string
description: Entity identifier
example: my-service-api
securitySchemes:
clientId:
type: oauth2
Expand Down
9 changes: 9 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,15 @@
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

<!-- Hibernate Envers for auditing -->
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-envers</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-envers</artifactId>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.decathlon.idp_core.domain.model.entity;

import java.time.Instant;
import java.util.UUID;

/// Domain model representing audit information for an [Entity] revision.
///
/// **Business purpose:** Tracks when and who modified an entity throughout its lifecycle.
/// This information is essential for:
/// - Compliance and regulatory requirements
/// - Change tracking and auditability
/// - Debugging and root cause analysis
/// - Historical reconstruction of entity state
///
/// **Ubiquitous language:** An EntityAuditInfo represents a single point-in-time
/// snapshot of entity modification metadata, capturing the revision number, timestamp,
/// and the user responsible for the change.
///
/// @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 +19 to +24
String modifiedBy, EntitySnapshot snapshot) {
// Inner record for the snapshot
public record EntitySnapshot(UUID id, String templateIdentifier, String name, String identifier) {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.decathlon.idp_core.domain.port.audit;

import java.util.List;

import com.decathlon.idp_core.domain.model.entity.EntityAuditInfo;

/// Port interface for retrieving entity audit information.
///
/// **Port contract:** Defines operations for accessing historical revision data
/// of entities. Implementations should interact with the audit storage system
/// (e.g., Hibernate Envers) to provide audit trail information.
///
/// **Hexagonal architecture:** This is a **driven port** (outbound), implemented
/// by infrastructure adapters and used by domain services to access audit data.
public interface EntityAuditPort {

/// Retrieves all audit revisions for a specific entity.
///
/// @param templateIdentifier the template identifier of the entity
/// @param entityIdentifier the unique identifier of the entity
/// @return list of audit information ordered by revision number (newest first)
List<EntityAuditInfo> getEntityAuditHistory(String templateIdentifier, String entityIdentifier);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.decathlon.idp_core.domain.service.entity;

import java.util.List;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.decathlon.idp_core.domain.model.entity.EntityAuditInfo;
import com.decathlon.idp_core.domain.port.audit.EntityAuditPort;
import com.decathlon.idp_core.domain.service.entity_template.EntityTemplateService;

import lombok.RequiredArgsConstructor;

/// Domain service for retrieving entity audit information.
///
/// **Business purpose:** Provides access to the audit trail of entities,
/// enabling compliance, debugging, and historical analysis. This service
/// orchestrates audit data retrieval while ensuring template existence
/// validation.
///
/// **Key responsibilities:**
/// - Retrieve audit history for entities including deleted ones
/// - Validate template existence before returning audit data
/// - Transform technical audit data into business-meaningful information
@Service
@RequiredArgsConstructor
public class EntityAuditService {

private final EntityAuditPort entityAuditPort;
private final EntityTemplateService entityTemplateService;

/// Retrieves the complete audit history for a specific entity.
///
/// **Business rule:** The template must exist to retrieve entity audit history.
/// This method allows retrieving audit history for deleted entities as well,
/// since the audit trail is stored independently.
///
/// @param templateIdentifier the template identifier of the entity
/// @param entityIdentifier the unique identifier of the entity
/// @return list of audit information ordered by revision number (newest first)
/// @throws
/// com.decathlon.idp_core.domain.exception.entity_template.EntityTemplateNotFoundException
/// if the template does not exist
@Transactional(readOnly = true)
public List<EntityAuditInfo> getEntityAuditHistory(String templateIdentifier,
String entityIdentifier) {
entityTemplateService.getEntityTemplateByIdentifier(templateIdentifier);
return entityAuditPort.getEntityAuditHistory(templateIdentifier, entityIdentifier);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package com.decathlon.idp_core.infrastructure.adapters.api.auth;

import java.util.Optional;

import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.stereotype.Component;

/// UnifiedUserProvider is a Spring component that implements the UserIdentityProvider interface to provide a consistent way
/// to retrieve the authenticated user's identity across different authentication mechanisms (JWT, OAuth2, OpenID).
/// It checks the current security context for the authentication type and extracts the user ID accordingly:
/// - For JWT authentication, it retrieves the subject (sub) claim from the JWT token.
/// - For OAuth2 authentication, it first checks if the user is an OIDC user to
/// retrieve the subject, otherwise it looks for a "sub" or "id" attribute in the OAuth2 user attributes, falling back to the authentication name if neither is found.
/// - For basic authentication, it simply returns the authentication name.
@Component
public class UnifiedUserProvider implements UserIdentityProvider {

@Override
public String getAuthId() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

if (authentication == null || !authentication.isAuthenticated()
|| authentication instanceof AnonymousAuthenticationToken) {
return "UNKNOWN";
}

// Jwt Case
if (authentication instanceof JwtAuthenticationToken jwtToken) {
return jwtToken.getToken().getSubject();
}

// OAuth2 and OpenId case
if (authentication.getPrincipal()instanceof OAuth2User oauth2Token) {

Comment on lines +37 to +39
if (oauth2Token instanceof OidcUser oidcUser) {
return oidcUser.getSubject();
}

return Optional.ofNullable(oauth2Token.getAttribute("sub")).map(Object::toString)
.orElseGet(() -> Optional.ofNullable(oauth2Token.getAttribute("id")).map(Object::toString)
.orElse(authentication.getName()));
}

// Basic Auth case
return authentication.getName();
}

@Override
public String getName() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

// Guard against unauthenticated/null context to prevent NullPointerExceptions
if (authentication == null || !authentication.isAuthenticated()
|| authentication instanceof AnonymousAuthenticationToken) {
return "UNKNOWN";
}

return authentication.getName();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.decathlon.idp_core.infrastructure.adapters.api.auth;

public interface UserIdentityProvider {
String getAuthId();
String getName();
}
Loading
Loading