-
Notifications
You must be signed in to change notification settings - Fork 0
feat(core): audit implementation #69
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
3a4e46a
bd048fe
1b84e18
2f7115f
de0a5a7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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: | ||
|
|
@@ -461,6 +463,59 @@ paths: | |
| '*/*': | ||
| schema: | ||
| $ref: '#/components/schemas/ErrorResponse' | ||
| /api/v1/audit/entities/{templateIdentifier}/{entityIdentifier}: | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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: | ||
|
|
@@ -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 | ||
|
|
||
| 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(); | ||
| } |
There was a problem hiding this comment.
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, ...)