-
Notifications
You must be signed in to change notification settings - Fork 3
feat: Enhanced Audit System with Special Operations Tracking #782
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: development
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| package com.epam.aidial.cfg.dao.audit.listener; | ||
|
|
||
| import lombok.extern.slf4j.Slf4j; | ||
| import org.springframework.stereotype.Component; | ||
|
|
||
| import java.util.Optional; | ||
| import java.util.UUID; | ||
|
|
||
| @Component | ||
| @Slf4j | ||
| public class AuditParentActivityHolder { | ||
| private final ThreadLocal<UUID> parentActivityId = new ThreadLocal<>(); | ||
|
|
||
| public Scope openScope(UUID parentId) { | ||
| UUID previous = parentActivityId.get(); | ||
|
|
||
| if (parentId != null) { | ||
| parentActivityId.set(parentId); | ||
| } | ||
|
|
||
| return () -> { | ||
| if (previous == null) { | ||
| parentActivityId.remove(); | ||
| } else { | ||
| parentActivityId.set(previous); | ||
| } | ||
| }; | ||
| } | ||
|
|
||
| public Optional<UUID> getParentActivityId() { | ||
| return Optional.ofNullable(parentActivityId.get()); | ||
| } | ||
|
|
||
| public interface Scope extends AutoCloseable { | ||
| @Override | ||
| void close(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,4 +4,14 @@ public enum ActivityType { | |
| Create, | ||
| Update, | ||
| Delete, | ||
| } | ||
| Import, | ||
| Export, | ||
| ExportRawConfig, | ||
| PublicationApprove, | ||
| PublicationUpdate, | ||
|
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. Can we use existing Create, Update, Delete actions? |
||
| PublicationReject, | ||
| PublicationDelete, | ||
| PublicationCreate, | ||
| FileUpload, | ||
| FileUploadZip, | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,203 @@ | ||
| package com.epam.aidial.cfg.domain.service; | ||
|
|
||
| import com.epam.aidial.cfg.dao.audit.jpa.AuditActivityJpaRepository; | ||
| import com.epam.aidial.cfg.dao.audit.listener.AuditParentActivityHolder; | ||
| import com.epam.aidial.cfg.dao.audit.model.AuditActivityEntity; | ||
| import com.epam.aidial.cfg.domain.model.activity.ActivityResourceType; | ||
| import com.epam.aidial.cfg.domain.model.activity.ActivityType; | ||
| import com.epam.aidial.cfg.domain.util.AuditMetaBuilder; | ||
| import com.epam.aidial.cfg.model.ConfigImportOptions; | ||
| import com.epam.aidial.cfg.model.ExportRequest; | ||
| import com.epam.aidial.cfg.model.FullExportRequest; | ||
| import com.epam.aidial.cfg.model.ImportResources; | ||
| import com.epam.aidial.cfg.model.ImportResourcesFileResult; | ||
| import com.epam.aidial.cfg.model.ImportResourcesResult; | ||
| import com.epam.aidial.cfg.model.SelectedItemsExportRequest; | ||
| import com.epam.aidial.cfg.security.SecurityClaimsExtractor; | ||
| import com.epam.aidial.cfg.transaction.timestamp.TransactionTimestampContext; | ||
| import com.epam.aidial.cfg.utils.PathUtils; | ||
| import com.fasterxml.jackson.databind.ObjectMapper; | ||
| import com.github.f4b6a3.uuid.UuidCreator; | ||
| import jakarta.persistence.EntityManager; | ||
| import lombok.RequiredArgsConstructor; | ||
| import lombok.extern.slf4j.Slf4j; | ||
| import org.apache.commons.lang3.StringUtils; | ||
| import org.springframework.stereotype.Service; | ||
| import org.springframework.transaction.annotation.Transactional; | ||
|
|
||
| import java.util.UUID; | ||
|
|
||
| @Service | ||
| @RequiredArgsConstructor | ||
| @Slf4j | ||
| public class AuditActivityLogService { | ||
|
|
||
| private final AuditActivityJpaRepository auditActivityJpaRepository; | ||
| private final TransactionTimestampContext transactionTimestampContext; | ||
| private final AuditParentActivityHolder auditParentActivityHolder; | ||
| private final ObjectMapper objectMapper; | ||
| private final EntityManager entityManager; | ||
|
|
||
| @Transactional | ||
|
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.
|
||
| private UUID startParentOperation(ActivityType activityType, | ||
|
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. Let's specify public method before private ones |
||
| ActivityResourceType resourceType, | ||
| String resourceId, | ||
| String operationMetadataJson) { | ||
| var entity = createAuditEntity(activityType, resourceType, resourceId, operationMetadataJson); | ||
| resourceId = StringUtils.isNotBlank(resourceId) ? resourceId : entity.getActivityId().toString(); | ||
|
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. Can |
||
| entity.setResourceId(resourceId); | ||
| auditActivityJpaRepository.save(entity); | ||
| entityManager.flush(); | ||
|
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. Why is that needed? |
||
| return entity.getActivityId(); | ||
| } | ||
|
|
||
| @Transactional | ||
| public void logAuditOperation(ActivityType activityType, | ||
|
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. should it be public? |
||
| ActivityResourceType resourceType, | ||
| String resourceId, | ||
| String operationMetadataJson) { | ||
| try { | ||
| var entity = createAuditEntity(activityType, resourceType, resourceId, operationMetadataJson); | ||
| resourceId = StringUtils.isNotBlank(resourceId) ? resourceId : entity.getActivityId().toString(); | ||
| entity.setResourceId(resourceId); | ||
| auditParentActivityHolder.getParentActivityId() | ||
| .ifPresent(entity::setParentActivityId); | ||
| auditActivityJpaRepository.save(entity); | ||
| } catch (Exception e) { | ||
| log.warn("Failed to record audit for {} {}", activityType, resourceId, e); | ||
| } | ||
| } | ||
|
|
||
| @Transactional | ||
| public void logAssetChange(ActivityType activityType, | ||
| ActivityResourceType resourceType, | ||
| String assetId) { | ||
| if (StringUtils.isBlank(assetId)) { | ||
| return; | ||
| } | ||
| var metaJson = AuditMetaBuilder.with(objectMapper) | ||
| .put("assetId", assetId) | ||
| .buildAsJson(); | ||
| logAuditOperation(activityType, resourceType, assetId, metaJson); | ||
| } | ||
|
|
||
| @Transactional | ||
| public void logPublication(String publicationPath, | ||
| ActivityType activityType, | ||
| String comment) { | ||
| var metaJson = AuditMetaBuilder.with(objectMapper) | ||
| .put("path", publicationPath) | ||
| .putIfNotBlank("comment", comment) | ||
| .buildAsJson(); | ||
| logAuditOperation(activityType, ActivityResourceType.Publication, publicationPath, metaJson); | ||
| } | ||
|
|
||
| @Transactional | ||
| public UUID logPublicationUpdate(String path, String fileNames) { | ||
| var metaJson = AuditMetaBuilder.with(objectMapper) | ||
| .put("path", path) | ||
| .putIfNotBlank("fileNames", fileNames) | ||
| .buildAsJson(); | ||
| return startParentOperation(ActivityType.PublicationUpdate, ActivityResourceType.Publication, | ||
| null, metaJson); | ||
| } | ||
|
|
||
| @Transactional | ||
| public void logConfigExport(ExportRequest request) { | ||
| var builder = AuditMetaBuilder.with(objectMapper) | ||
| .put("exportFormat", request.getExportFormat().name()) | ||
| .put("addSecrets", request.isAddSecrets()); | ||
|
|
||
| if (request instanceof FullExportRequest fullExportRequest) { | ||
| builder.put("exportKind", "full") | ||
| .put("componentTypes", fullExportRequest.getComponentTypes()) | ||
| .put("topicCount", fullExportRequest.getTopics() != null ? fullExportRequest.getTopics().size() : null); | ||
| } else if (request instanceof SelectedItemsExportRequest selectedItemsExportRequest) { | ||
| builder.put("exportKind", "selected") | ||
| .put("componentCount", selectedItemsExportRequest.getComponents() != null ? selectedItemsExportRequest.getComponents().size() : 0); | ||
| } else { | ||
| throw new IllegalArgumentException("Unsupported ExportRequest type: " + request.getClass()); | ||
| } | ||
|
|
||
| var metaJson = builder.buildAsJson(); | ||
| logAuditOperation(ActivityType.Export, ActivityResourceType.ExportConfig, null, metaJson); | ||
| } | ||
|
|
||
| @Transactional | ||
| public void logExportRawConfig(boolean addSecrets) { | ||
| var metaJson = AuditMetaBuilder.with(objectMapper) | ||
| .put("addSecrets", addSecrets).buildAsJson(); | ||
| logAuditOperation(ActivityType.ExportRawConfig, ActivityResourceType.ExportConfig, null, metaJson); | ||
| } | ||
|
|
||
| @Transactional | ||
| public void logFileUpload(ActivityType activityType, | ||
| ImportResources importResources, | ||
| String zipArchiveName, | ||
| String fileNames, | ||
| ImportResourcesFileResult result) { | ||
| if (result == null) { | ||
| return; | ||
| } | ||
|
|
||
| var filesMeta = getFilesMeta(result); | ||
| var metaData = AuditMetaBuilder.with(objectMapper) | ||
| .put("importPath", importResources.getPath()) | ||
| .put("conflictResolution", importResources.getConflictResolutionStrategy()) | ||
| .put("flatImport", importResources.isFlatImport()) | ||
| .putIfNotBlank("zipArchiveName", zipArchiveName) | ||
| .putIfNotBlank("files", filesMeta).buildAsJson(); | ||
| String resourceId = StringUtils.firstNonBlank(zipArchiveName, fileNames); | ||
| logAuditOperation(activityType, ActivityResourceType.File, resourceId, metaData); | ||
| } | ||
|
|
||
| private String getFilesMeta(ImportResourcesFileResult result) { | ||
| var builder = AuditMetaBuilder.with(objectMapper); | ||
| if (result == null || result.getImportResults() == null) { | ||
| return builder.buildAsJson(); | ||
| } | ||
| for (ImportResourcesResult resourcesResult : result.getImportResults()) { | ||
| var pathForName = StringUtils.firstNonBlank(resourcesResult.getSourcePath(), resourcesResult.getTargetPath()); | ||
| if (StringUtils.isNotBlank(pathForName)) { | ||
| try { | ||
| builder.put("fileName", PathUtils.parsePath(pathForName).getName()); | ||
| } catch (IllegalArgumentException e) { | ||
| builder.put("fileName", pathForName); | ||
| } | ||
| } | ||
| builder.put("result", resourcesResult.getStatus().name()); | ||
| builder.putIfNotBlank("error", resourcesResult.getError()); | ||
| } | ||
| return builder.buildAsJson(); | ||
| } | ||
|
|
||
| @Transactional | ||
| public UUID logImportOperation(String scope, ConfigImportOptions importOptions) { | ||
|
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. I think logging of operations like import should not start transaction on their own if transaction doesn't exist, they should just support existing transaction or throw exception if transaction doesn't exist |
||
| var metaJson = AuditMetaBuilder.with(objectMapper) | ||
| .put("scope", scope) | ||
|
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. Considering that possible values are "core" or "admin" I think it should be "format"/"type" rather than "scope" |
||
| .put("conflictResolution", importOptions.conflictResolutionPolicy().name()) | ||
| .put("createRoleIfAbsent", importOptions.createRoleIfAbsent()) | ||
| .put("createRoleIfAbsent", importOptions.createRoleIfAbsent()) | ||
| .put("createAdapterIfAbsent", importOptions.createAdapterIfAbsent()) | ||
| .buildAsJson(); | ||
| return startParentOperation(ActivityType.Import, ActivityResourceType.ImportConfig, | ||
| null, metaJson); | ||
| } | ||
|
|
||
| private AuditActivityEntity createAuditEntity(ActivityType activityType, | ||
| ActivityResourceType resourceType, | ||
| String resourceId, | ||
| String operationMetadataJson) { | ||
| UUID id = UuidCreator.getTimeOrderedEpoch(); | ||
| AuditActivityEntity entity = new AuditActivityEntity(); | ||
| entity.setActivityId(id); | ||
| entity.setActivityType(activityType); | ||
| entity.setResourceType(resourceType); | ||
| entity.setResourceId(resourceId); | ||
| entity.setEpochTimestampMs(transactionTimestampContext.getTimestamp()); | ||
| entity.setInitiatedAuthor(SecurityClaimsExtractor.getAuthor()); | ||
| entity.setInitiatedEmail(SecurityClaimsExtractor.getEmail()); | ||
| entity.setOperationMetadata(operationMetadataJson); | ||
| return entity; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| package com.epam.aidial.cfg.domain.util; | ||
|
|
||
| import com.fasterxml.jackson.core.JsonProcessingException; | ||
| import com.fasterxml.jackson.databind.ObjectMapper; | ||
| import org.apache.commons.lang3.StringUtils; | ||
|
|
||
| import java.util.LinkedHashMap; | ||
| import java.util.Map; | ||
|
|
||
| public final class AuditMetaBuilder { | ||
|
|
||
| private final Map<String, Object> meta; | ||
| private final ObjectMapper objectMapper; | ||
|
|
||
| private AuditMetaBuilder(ObjectMapper objectMapper) { | ||
| this.objectMapper = objectMapper; | ||
| this.meta = new LinkedHashMap<>(); | ||
| } | ||
|
|
||
| public static AuditMetaBuilder with(ObjectMapper objectMapper) { | ||
| return new AuditMetaBuilder(objectMapper); | ||
| } | ||
|
|
||
| public AuditMetaBuilder put(String key, Object value) { | ||
| if (value != null) { | ||
| meta.put(key, value); | ||
| } | ||
| return this; | ||
| } | ||
|
|
||
| public AuditMetaBuilder putIfNotBlank(String key, String value) { | ||
| if (StringUtils.isNotBlank(value)) { | ||
| meta.put(key, value); | ||
| } | ||
| return this; | ||
| } | ||
|
|
||
| public String buildAsJson() { | ||
| try { | ||
| return objectMapper.writeValueAsString(meta); | ||
| } catch (JsonProcessingException e) { | ||
| throw new IllegalStateException("Failed to serialize audit metadata", e); | ||
| } | ||
| } | ||
| } |
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.
May be ToolSetResource