Skip to content
Draft
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
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
Expand Up @@ -36,12 +36,14 @@ public class ConfigRevisionListener implements EntityTrackingRevisionListener, A
private TransactionTimestampContext transactionTimestampContext;
private DeploymentJpaRepository deploymentJpaRepository;
private AuditActivityMapper auditActivityMapper;
private AuditParentActivityHolder auditParentActivityHolder;

@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
this.transactionTimestampContext = applicationContext.getBean(TransactionTimestampContext.class);
this.auditActivityMapper = applicationContext.getBean(AuditActivityMapper.class);
this.auditParentActivityHolder = applicationContext.getBean(AuditParentActivityHolder.class);
}

@Override
Expand Down Expand Up @@ -119,6 +121,7 @@ private AuditActivityEntity buildAuditActivity(ConfigRevisionEntity revEntity, A
auditActivity.setInitiatedAuthor(revEntity.getAuthor());
auditActivity.setInitiatedEmail(revEntity.getEmail());
auditActivity.setRevision(revEntity.getId());
auditParentActivityHolder.getParentActivityId().ifPresent(auditActivity::setParentActivityId);
return auditActivity;
}

Expand Down Expand Up @@ -168,4 +171,4 @@ private void reset() {
changeListHolder.get().clear();
issuedUpdateActivitiesHolder.get().clear();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.epam.aidial.cfg.domain.model.activity.ActivityResourceType;
import com.epam.aidial.cfg.domain.model.activity.ActivityType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
Expand Down Expand Up @@ -29,4 +30,8 @@ public class AuditActivityEntity {
private String initiatedAuthor;
private String initiatedEmail;
private Integer revision;
@JdbcTypeCode(SqlTypes.VARCHAR)
private UUID parentActivityId;
@Column(columnDefinition = "CLOB")
private String operationMetadata;
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,6 @@ public class AuditActivity {
private String initiatedAuthor;
private String initiatedEmail;
private Integer revision;
private UUID parentActivityId;
private String operationMetadata;
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,12 @@ public enum ActivityResourceType {
ToolSet,
GlobalSettings,
AdminSettings,
ImportConfig,
ExportConfig,
ToolResource,
Copy link
Collaborator

Choose a reason for hiding this comment

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

May be ToolSetResource

ApplicationResource,
Prompt,
File,
Publication,
Conversation
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,14 @@ public enum ActivityType {
Create,
Update,
Delete,
}
Import,
Export,
ExportRawConfig,
PublicationApprove,
PublicationUpdate,
Copy link
Collaborator

Choose a reason for hiding this comment

The 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
Copy link
Collaborator

@KirylKurnosenka KirylKurnosenka Mar 23, 2026

Choose a reason for hiding this comment

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

@Transactional on private method doesn't work

private UUID startParentOperation(ActivityType activityType,
Copy link
Collaborator

Choose a reason for hiding this comment

The 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();
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can resourceId be not null in certain cases?

entity.setResourceId(resourceId);
auditActivityJpaRepository.save(entity);
entityManager.flush();
Copy link
Collaborator

Choose a reason for hiding this comment

The 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,
Copy link
Collaborator

Choose a reason for hiding this comment

The 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) {
Copy link
Collaborator

Choose a reason for hiding this comment

The 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)
Copy link
Collaborator

@KirylKurnosenka KirylKurnosenka Mar 23, 2026

Choose a reason for hiding this comment

The 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
Expand Up @@ -27,7 +27,8 @@ public class AuditActivityService {
"resourceType",
"resourceId",
"initiatedAuthor",
"initiatedEmail"
"initiatedEmail",
"parentActivityId"
);

private final AuditActivityEntityMapper auditActivityEntityMapper;
Expand Down Expand Up @@ -68,4 +69,4 @@ private static Specification<AuditActivityEntity> defaultFilters() {
(root, query, criteriaBuilder) -> criteriaBuilder.notEqual(root.get("resourceType"), "SecuredResource")
);
}
}
}
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);
}
}
}
Loading
Loading