From 0272734051438400ff9eba217c0214e53e73f438 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 13 Jan 2026 14:50:48 +0000 Subject: [PATCH 1/3] Initial plan --- .../migration/data/HistoryMigrator.java | 10 + .../migration/data/RuntimeMigrator.java | 4 +- .../data/config/InterceptorConfiguration.java | 6 + .../config/MigratorAutoConfiguration.java | 2 + .../data/constants/MigratorConstants.java | 1 + .../migration/data/impl/clients/C7Client.java | 29 + .../migration/data/impl/clients/C8Client.java | 22 + .../migration/data/impl/clients/DbClient.java | 18 +- .../data/impl/history/AuditLogMigrator.java | 273 +++++++++ .../data/impl/history/BaseMigrator.java | 46 +- .../impl/history/ProcessInstanceMigrator.java | 38 -- .../history/entity/AuditLogTransformer.java | 221 ++++++++ .../data/impl/logging/C8ClientLogs.java | 2 + .../data/impl/logging/DbClientLogs.java | 2 +- .../impl/logging/HistoryMigratorLogs.java | 35 ++ .../data/impl/persistence/IdKeyDbModel.java | 6 +- .../data/impl/persistence/IdKeyMapper.java | 1 + .../db/changelog/migrator/db.0.3.0.xml | 25 + .../migrator/db.changelog-master.yaml | 2 + .../core/src/main/resources/mapper/IdKey.xml | 6 +- .../resources/MigratorResourceTest.java | 4 +- .../history/HistoryMigrationAbstractTest.java | 23 + .../HistoryMigrationListSkippedTest.java | 1 + .../history/IdKeyCreateTimeMappingTest.java | 2 +- .../qa/history/TransactionRollbackTest.java | 2 +- .../entity/HistoryAuditLogAdminTest.java | 410 ++++++++++++++ .../history/entity/HistoryAuditLogTest.java | 520 ++++++++++++++++++ .../entity/HistoryAuditLogUserTaskTest.java | 415 ++++++++++++++ .../entity/HistoryProcessInstanceTest.java | 2 +- 29 files changed, 2072 insertions(+), 56 deletions(-) create mode 100644 data-migrator/core/src/main/java/io/camunda/migration/data/impl/history/AuditLogMigrator.java create mode 100644 data-migrator/core/src/main/java/io/camunda/migration/data/impl/interceptor/history/entity/AuditLogTransformer.java create mode 100644 data-migrator/core/src/main/resources/db/changelog/migrator/db.0.3.0.xml create mode 100644 data-migrator/qa/integration-tests/src/test/java/io/camunda/migration/data/qa/history/entity/HistoryAuditLogAdminTest.java create mode 100644 data-migrator/qa/integration-tests/src/test/java/io/camunda/migration/data/qa/history/entity/HistoryAuditLogTest.java create mode 100644 data-migrator/qa/integration-tests/src/test/java/io/camunda/migration/data/qa/history/entity/HistoryAuditLogUserTaskTest.java diff --git a/data-migrator/core/src/main/java/io/camunda/migration/data/HistoryMigrator.java b/data-migrator/core/src/main/java/io/camunda/migration/data/HistoryMigrator.java index 2937e21d4..98076437f 100644 --- a/data-migrator/core/src/main/java/io/camunda/migration/data/HistoryMigrator.java +++ b/data-migrator/core/src/main/java/io/camunda/migration/data/HistoryMigrator.java @@ -22,6 +22,7 @@ import io.camunda.migration.data.impl.history.ProcessInstanceMigrator; import io.camunda.migration.data.impl.history.UserTaskMigrator; import io.camunda.migration.data.impl.history.VariableMigrator; +import io.camunda.migration.data.impl.history.AuditLogMigrator; import io.camunda.migration.data.impl.clients.DbClient; import io.camunda.migration.data.impl.util.ExceptionUtils; import io.camunda.migration.data.impl.util.PrintUtils; @@ -63,6 +64,9 @@ public class HistoryMigrator { @Autowired protected DecisionInstanceMigrator decisionInstanceMigrator; + @Autowired + protected AuditLogMigrator auditLogMigrator; + @Autowired protected DbClient dbClient; @@ -106,6 +110,7 @@ public void migrate() { migrateDecisionRequirementsDefinitions(); migrateDecisionDefinitions(); migrateDecisionInstances(); + migrateAuditLogs(); } public void migrateProcessDefinitions() { @@ -144,6 +149,10 @@ public void migrateDecisionInstances() { decisionInstanceMigrator.migrate(); } + public void migrateAuditLogs() { + auditLogMigrator.migrate(); + } + public void setRequestedEntityTypes(List requestedEntityTypes) { this.requestedEntityTypes = requestedEntityTypes; } @@ -159,5 +168,6 @@ public void setMode(MigratorMode mode) { processInstanceMigrator.setMode(mode); userTaskMigrator.setMode(mode); variableMigrator.setMode(mode); + auditLogMigrator.setMode(mode); } } diff --git a/data-migrator/core/src/main/java/io/camunda/migration/data/RuntimeMigrator.java b/data-migrator/core/src/main/java/io/camunda/migration/data/RuntimeMigrator.java index 5b20f07d0..a17c29d1b 100644 --- a/data-migrator/core/src/main/java/io/camunda/migration/data/RuntimeMigrator.java +++ b/data-migrator/core/src/main/java/io/camunda/migration/data/RuntimeMigrator.java @@ -84,7 +84,7 @@ protected void migrate() { if (skipReason == null && shouldStartProcessInstance(c7ProcessInstanceId)) { startProcessInstance(c7ProcessInstanceId, createTime); } else if (isUnknown(c7ProcessInstanceId)) { - dbClient.insert(c7ProcessInstanceId, null, createTime, TYPE.RUNTIME_PROCESS_INSTANCE, skipReason); + dbClient.insert(c7ProcessInstanceId, (Long) null, createTime, TYPE.RUNTIME_PROCESS_INSTANCE, skipReason); } else { dbClient.updateSkipReason(c7ProcessInstanceId, TYPE.RUNTIME_PROCESS_INSTANCE, skipReason); } @@ -131,7 +131,7 @@ protected void handleVariableInterceptorException(VariableInterceptorException e RuntimeMigratorLogs.stacktrace(e); if (MIGRATE.equals(mode)) { - dbClient.insert(c7ProcessInstanceId, null, createTime, TYPE.RUNTIME_PROCESS_INSTANCE, e.getMessage()); + dbClient.insert(c7ProcessInstanceId, (Long) null, createTime, TYPE.RUNTIME_PROCESS_INSTANCE, e.getMessage()); } else if (RETRY_SKIPPED.equals(mode)) { dbClient.updateSkipReason(c7ProcessInstanceId, TYPE.RUNTIME_PROCESS_INSTANCE, e.getMessage()); } diff --git a/data-migrator/core/src/main/java/io/camunda/migration/data/config/InterceptorConfiguration.java b/data-migrator/core/src/main/java/io/camunda/migration/data/config/InterceptorConfiguration.java index 50dcab969..8c224d8e8 100644 --- a/data-migrator/core/src/main/java/io/camunda/migration/data/config/InterceptorConfiguration.java +++ b/data-migrator/core/src/main/java/io/camunda/migration/data/config/InterceptorConfiguration.java @@ -23,6 +23,7 @@ import io.camunda.migration.data.impl.interceptor.PrimitiveVariableTransformer; import io.camunda.migration.data.impl.interceptor.SpinJsonVariableTransformer; import io.camunda.migration.data.impl.interceptor.SpinXmlVariableTransformer; +import io.camunda.migration.data.impl.interceptor.history.entity.AuditLogTransformer; import io.camunda.migration.data.impl.interceptor.history.entity.DecisionDefinitionTransformer; import io.camunda.migration.data.impl.interceptor.history.entity.DecisionInstanceTransformer; import io.camunda.migration.data.impl.interceptor.history.entity.DecisionRequirementsDefinitionTransformer; @@ -367,4 +368,9 @@ public UserTaskTransformer userTaskTransformer() { public VariableTransformer variableTransformer() { return new VariableTransformer(); } + + @Bean + public AuditLogTransformer auditLogTransformer() { + return new AuditLogTransformer(); + } } diff --git a/data-migrator/core/src/main/java/io/camunda/migration/data/config/MigratorAutoConfiguration.java b/data-migrator/core/src/main/java/io/camunda/migration/data/config/MigratorAutoConfiguration.java index a3fec15eb..39adb80fe 100644 --- a/data-migrator/core/src/main/java/io/camunda/migration/data/config/MigratorAutoConfiguration.java +++ b/data-migrator/core/src/main/java/io/camunda/migration/data/config/MigratorAutoConfiguration.java @@ -26,6 +26,7 @@ import io.camunda.migration.data.impl.clients.DbClient; import io.camunda.migration.data.impl.VariableService; import io.camunda.migration.data.impl.RuntimeValidator; +import io.camunda.migration.data.impl.history.AuditLogMigrator; import io.camunda.migration.data.impl.identity.AuthorizationManager; import io.camunda.migration.data.impl.history.DecisionDefinitionMigrator; import io.camunda.migration.data.impl.history.DecisionInstanceMigrator; @@ -86,6 +87,7 @@ ProcessInstanceMigrator.class, UserTaskMigrator.class, VariableMigrator.class, + AuditLogMigrator.class, SchemaShutdownCleaner.class }) @Configuration diff --git a/data-migrator/core/src/main/java/io/camunda/migration/data/constants/MigratorConstants.java b/data-migrator/core/src/main/java/io/camunda/migration/data/constants/MigratorConstants.java index 3648f27e9..f711cf28c 100644 --- a/data-migrator/core/src/main/java/io/camunda/migration/data/constants/MigratorConstants.java +++ b/data-migrator/core/src/main/java/io/camunda/migration/data/constants/MigratorConstants.java @@ -17,6 +17,7 @@ protected MigratorConstants() {} * collide with actual Zeebe partition keys during migration. */ public static int C7_HISTORY_PARTITION_ID = 4095; + public static int C7_AUDIT_LOG_ENTITY_VERSION = -4095; public static final String LEGACY_ID_VAR_NAME = "legacyId"; public static final String C8_DEFAULT_TENANT = ""; diff --git a/data-migrator/core/src/main/java/io/camunda/migration/data/impl/clients/C7Client.java b/data-migrator/core/src/main/java/io/camunda/migration/data/impl/clients/C7Client.java index 4b6e5fd4e..686b40926 100644 --- a/data-migrator/core/src/main/java/io/camunda/migration/data/impl/clients/C7Client.java +++ b/data-migrator/core/src/main/java/io/camunda/migration/data/impl/clients/C7Client.java @@ -40,6 +40,7 @@ import org.camunda.bpm.engine.history.HistoricProcessInstance; import org.camunda.bpm.engine.history.HistoricTaskInstance; import org.camunda.bpm.engine.history.HistoricVariableInstance; +import org.camunda.bpm.engine.history.UserOperationLogEntry; import org.camunda.bpm.engine.identity.Tenant; import org.camunda.bpm.engine.identity.TenantQuery; import org.camunda.bpm.engine.impl.AuthorizationQueryImpl; @@ -49,6 +50,7 @@ import org.camunda.bpm.engine.impl.HistoricProcessInstanceQueryImpl; import org.camunda.bpm.engine.impl.HistoricTaskInstanceQueryImpl; import org.camunda.bpm.engine.impl.HistoricVariableInstanceQueryImpl; +import org.camunda.bpm.engine.impl.UserOperationLogQueryImpl; import org.camunda.bpm.engine.impl.ProcessDefinitionQueryImpl; import org.camunda.bpm.engine.impl.TenantQueryImpl; import org.camunda.bpm.engine.impl.cfg.ProcessEngineConfigurationImpl; @@ -495,6 +497,33 @@ public void fetchAndHandleHistoricFlowNodes(Consumer c .callback(callback); } + /** + * Processes historic user operation log entries with pagination using the provided callback consumer. + */ + public void fetchAndHandleUserOperationLogEntries(Consumer callback, Date timestampAfter) { + UserOperationLogQueryImpl query = (UserOperationLogQueryImpl) historyService.createUserOperationLogQuery() + .orderByTimestamp() + .asc(); + + if (timestampAfter != null) { + query.afterTimestamp(timestampAfter); + } + + new Pagination() + .pageSize(properties.getPageSize()) + .query(query) + .maxCount(query::count) + .callback(callback); + } + + /** + * Gets a single user operation log entry by ID. + */ + public UserOperationLogEntry getUserOperationLogEntry(String c7Id) { + var query = historyService.createUserOperationLogQuery().operationId(c7Id); + return callApi(query::singleResult, format(FAILED_TO_FETCH_HISTORIC_ELEMENT, "UserOperationLogEntry", c7Id)); + } + /** * Processes tenant entities with pagination using the provided callback consumer. */ diff --git a/data-migrator/core/src/main/java/io/camunda/migration/data/impl/clients/C8Client.java b/data-migrator/core/src/main/java/io/camunda/migration/data/impl/clients/C8Client.java index d923a2da5..a2dfb0d1b 100644 --- a/data-migrator/core/src/main/java/io/camunda/migration/data/impl/clients/C8Client.java +++ b/data-migrator/core/src/main/java/io/camunda/migration/data/impl/clients/C8Client.java @@ -14,6 +14,7 @@ import static io.camunda.migration.data.impl.logging.C8ClientLogs.FAILED_TO_FETCH_PROCESS_DEFINITION_XML; import static io.camunda.migration.data.impl.logging.C8ClientLogs.FAILED_TO_FETCH_VARIABLE; import static io.camunda.migration.data.impl.logging.C8ClientLogs.FAILED_TO_FIND_PROCESS_INSTANCE_BY_KEY; +import static io.camunda.migration.data.impl.logging.C8ClientLogs.FAILED_TO_INSERT_AUDIT_LOG; import static io.camunda.migration.data.impl.logging.C8ClientLogs.FAILED_TO_INSERT_DECISION_DEFINITION; import static io.camunda.migration.data.impl.logging.C8ClientLogs.FAILED_TO_INSERT_DECISION_INSTANCE; import static io.camunda.migration.data.impl.logging.C8ClientLogs.FAILED_TO_INSERT_DECISION_INSTANCE_INPUT; @@ -33,6 +34,7 @@ import static io.camunda.migration.data.impl.logging.C8ClientLogs.FAILED_TO_SEARCH_FLOW_NODE_INSTANCES; import static io.camunda.migration.data.impl.logging.C8ClientLogs.FAILED_TO_SEARCH_PROCESS_DEFINITIONS; import static io.camunda.migration.data.impl.logging.C8ClientLogs.FAILED_TO_SEARCH_PROCESS_INSTANCE; +import static io.camunda.migration.data.impl.logging.C8ClientLogs.FAILED_TO_SEARCH_USER_TASKS; import static io.camunda.migration.data.impl.util.ConverterUtil.getTenantId; import static io.camunda.migration.data.impl.util.ExceptionUtils.callApi; @@ -56,6 +58,8 @@ import io.camunda.db.rdbms.read.domain.FlowNodeInstanceDbQuery; import io.camunda.db.rdbms.read.domain.ProcessDefinitionDbQuery; import io.camunda.db.rdbms.read.domain.ProcessInstanceDbQuery; +import io.camunda.db.rdbms.read.domain.UserTaskDbQuery; +import io.camunda.db.rdbms.sql.AuditLogMapper; import io.camunda.db.rdbms.sql.DecisionDefinitionMapper; import io.camunda.db.rdbms.sql.DecisionInstanceMapper; import io.camunda.db.rdbms.sql.DecisionRequirementsMapper; @@ -65,6 +69,7 @@ import io.camunda.db.rdbms.sql.ProcessInstanceMapper; import io.camunda.db.rdbms.sql.UserTaskMapper; import io.camunda.db.rdbms.sql.VariableMapper; +import io.camunda.db.rdbms.write.domain.AuditLogDbModel; import io.camunda.db.rdbms.write.domain.DecisionDefinitionDbModel; import io.camunda.db.rdbms.write.domain.DecisionInstanceDbModel; import io.camunda.db.rdbms.write.domain.DecisionRequirementsDbModel; @@ -133,6 +138,9 @@ public class C8Client { @Autowired(required = false) protected DecisionRequirementsMapper decisionRequirementsMapper; + @Autowired(required = false) + protected AuditLogMapper auditLogMapper; + /** * Creates a new process instance with the given BPMN process ID and variables. */ @@ -332,6 +340,13 @@ public void insertFlowNodeInstance(FlowNodeInstanceDbModel dbModel) { callApi(() -> flowNodeInstanceMapper.insert(new BatchInsertDto(List.of(dbModel))), FAILED_TO_INSERT_FLOW_NODE_INSTANCE); } + /** + * Inserts an AuditLog into the database. + */ + public void insertAuditLog(AuditLogDbModel dbModel) { + callApi(() -> auditLogMapper.insert(new BatchInsertDto(List.of(dbModel))), FAILED_TO_INSERT_AUDIT_LOG); + } + /** * Searches for FlowNodeInstances matching the query. */ @@ -339,6 +354,13 @@ public List searchFlowNodeInstances(FlowNodeInstanceDbQ return callApi(() -> flowNodeInstanceMapper.search(query), FAILED_TO_SEARCH_FLOW_NODE_INSTANCES); } + /** + * Searches for User tasks matching the query. + */ + public List searchUserTasks(UserTaskDbQuery query){ + return callApi(() -> userTaskMapper.search(query), FAILED_TO_SEARCH_USER_TASKS); + } + /** * Searches for ProcessDefinitions matching the query. */ diff --git a/data-migrator/core/src/main/java/io/camunda/migration/data/impl/clients/DbClient.java b/data-migrator/core/src/main/java/io/camunda/migration/data/impl/clients/DbClient.java index 1cc00960e..658425685 100644 --- a/data-migrator/core/src/main/java/io/camunda/migration/data/impl/clients/DbClient.java +++ b/data-migrator/core/src/main/java/io/camunda/migration/data/impl/clients/DbClient.java @@ -99,6 +99,13 @@ public List findAllC7Ids() { * Updates a record by setting the key for an existing ID and type. */ public void updateC8KeyByC7IdAndType(String c7Id, Long c8Key, TYPE type) { + updateC8KeyByC7IdAndType(c7Id, (c8Key == null) ? null : c8Key.toString(), type); + } + + /** + * Updates a record by setting the key for an existing ID and type. + */ + public void updateC8KeyByC7IdAndType(String c7Id, String c8Key, TYPE type) { DbClientLogs.updatingC8KeyForC7Id(c7Id, c8Key); var model = createIdKeyDbModel(c7Id, null, c8Key, type); callApi(() -> idKeyMapper.updateC8KeyByC7IdAndType(model), FAILED_TO_UPDATE_KEY + c8Key); @@ -132,6 +139,13 @@ public void insert(String c7Id, Long c8Key, TYPE type) { * Inserts a new process instance record into the mapping table. */ public void insert(String c7Id, Long c8Key, Date createTime, TYPE type, String skipReason) { + insert(c7Id, (c8Key == null) ? null : c8Key.toString(), createTime, type, skipReason); + } + + /** + * Inserts a new process instance record into the mapping table. + */ + public void insert(String c7Id, String c8Key, Date createTime, TYPE type, String skipReason) { String finalSkipReason = properties.getSaveSkipReason() ? skipReason : null; DbClientLogs.insertingRecord(c7Id, createTime, null, finalSkipReason); var model = createIdKeyDbModel(c7Id, createTime, c8Key, type, finalSkipReason); @@ -201,7 +215,7 @@ protected void deleteByC7Id(String c7Id) { /** * Creates a new IdKeyDbModel instance with the provided parameters including skip reason. */ - protected IdKeyDbModel createIdKeyDbModel(String c7Id, Date createTime, Long c8Key, TYPE type, String skipReason) { + protected IdKeyDbModel createIdKeyDbModel(String c7Id, Date createTime, String c8Key, TYPE type, String skipReason) { var keyIdDbModel = new IdKeyDbModel(); keyIdDbModel.setC7Id(c7Id); keyIdDbModel.setCreateTime(createTime); @@ -214,7 +228,7 @@ protected IdKeyDbModel createIdKeyDbModel(String c7Id, Date createTime, Long c8K /** * Creates a new IdKeyDbModel instance with the provided parameters. */ - protected IdKeyDbModel createIdKeyDbModel(String c7Id, Date createTime, Long c8Key, TYPE type) { + protected IdKeyDbModel createIdKeyDbModel(String c7Id, Date createTime, String c8Key, TYPE type) { return createIdKeyDbModel(c7Id, createTime, c8Key, type, null); } } diff --git a/data-migrator/core/src/main/java/io/camunda/migration/data/impl/history/AuditLogMigrator.java b/data-migrator/core/src/main/java/io/camunda/migration/data/impl/history/AuditLogMigrator.java new file mode 100644 index 000000000..1dd00dc4c --- /dev/null +++ b/data-migrator/core/src/main/java/io/camunda/migration/data/impl/history/AuditLogMigrator.java @@ -0,0 +1,273 @@ +/* + * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH under + * one or more contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright ownership. + * Licensed under the Camunda License 1.0. You may not use this file + * except in compliance with the Camunda License 1.0. + */ +package io.camunda.migration.data.impl.history; + +import static io.camunda.migration.data.MigratorMode.RETRY_SKIPPED; +import static io.camunda.migration.data.constants.MigratorConstants.C7_HISTORY_PARTITION_ID; +import static io.camunda.migration.data.impl.logging.HistoryMigratorLogs.SKIPPING_AUDIT_LOG_MISSING_ROOT_INSTANCE; +import static io.camunda.migration.data.impl.logging.HistoryMigratorLogs.SKIPPING_AUDIT_LOG_MISSING_USER_TASK; +import static io.camunda.migration.data.impl.logging.HistoryMigratorLogs.SKIP_REASON_BELONGS_TO_SKIPPED_TASK; +import static io.camunda.migration.data.impl.logging.HistoryMigratorLogs.SKIP_REASON_MISSING_PROCESS_DEFINITION; +import static io.camunda.migration.data.impl.logging.HistoryMigratorLogs.SKIP_REASON_MISSING_PROCESS_INSTANCE; +import static io.camunda.migration.data.impl.logging.HistoryMigratorLogs.SKIP_REASON_MISSING_ROOT_PROCESS_INSTANCE; +import static io.camunda.migration.data.impl.persistence.IdKeyMapper.TYPE.HISTORY_AUDIT_LOG; +import static io.camunda.migration.data.impl.persistence.IdKeyMapper.TYPE.HISTORY_PROCESS_DEFINITION; +import static io.camunda.migration.data.impl.persistence.IdKeyMapper.TYPE.HISTORY_PROCESS_INSTANCE; +import static io.camunda.migration.data.impl.persistence.IdKeyMapper.TYPE.HISTORY_USER_TASK; +import static io.camunda.migration.data.impl.util.ConverterUtil.getNextKey; + +import io.camunda.db.rdbms.read.domain.UserTaskDbQuery; +import io.camunda.db.rdbms.write.domain.AuditLogDbModel; +import io.camunda.db.rdbms.write.domain.UserTaskDbModel; +import io.camunda.migration.data.exception.EntityInterceptorException; +import io.camunda.migration.data.impl.logging.HistoryMigratorLogs; +import io.camunda.migration.data.interceptor.property.EntityConversionContext; +import io.camunda.search.entities.ProcessInstanceEntity; +import java.util.Date; +import org.camunda.bpm.engine.history.UserOperationLogEntry; +import org.springframework.stereotype.Service; + +/** + * Service class responsible for migrating audit log entries from Camunda 7 to Camunda 8. + *

+ * Audit logs in Camunda 7 (UserOperationLogEntry) track user operations and changes made to + * process instances, tasks, and other entities. This migrator converts these entries to the + * Camunda 8 audit log format. + *

+ */ +@Service +public class AuditLogMigrator extends BaseMigrator { + + /** + * Migrates all audit log entries from Camunda 7 to Camunda 8. + *

+ * This method handles pagination and processes audit logs in batches, either migrating + * new entries or retrying skipped ones based on the migration mode. + *

+ */ + @Override + public void migrate() { + HistoryMigratorLogs.migratingHistoricAuditLogs(); + + if (RETRY_SKIPPED.equals(mode)) { + dbClient.fetchAndHandleSkippedForType(HISTORY_AUDIT_LOG, idKeyDbModel -> { + UserOperationLogEntry userOperationLogEntry = c7Client.getUserOperationLogEntry(idKeyDbModel.getC7Id()); + self.migrateOne(userOperationLogEntry); + }); + } else { + c7Client.fetchAndHandleUserOperationLogEntries(self::migrateOne, + dbClient.findLatestCreateTimeByType(HISTORY_AUDIT_LOG)); + } + } + + /** + * Migrates a single audit log entry from Camunda 7 to Camunda 8. + *

+ * Audit log entries track user operations such as creating, updating, or deleting + * process instances, tasks, variables, and other entities. The migration preserves + * the operation details, timestamps, and user information. + *

+ * + * @param c7AuditLog the user operation log entry from Camunda 7 to be migrated + * @throws EntityInterceptorException if an error occurs during entity conversion + */ + @Override + public void migrateOne(UserOperationLogEntry c7AuditLog) { + String c7AuditLogId = c7AuditLog.getOperationId(); + if (shouldMigrate(c7AuditLogId, HISTORY_AUDIT_LOG)) { + HistoryMigratorLogs.migratingHistoricAuditLog(c7AuditLogId); + + try { + AuditLogDbModel.Builder auditLogDbModelBuilder = configureAuditLogBuilder(c7AuditLog); + EntityConversionContext context = createEntityConversionContext( + c7AuditLog, UserOperationLogEntry.class, auditLogDbModelBuilder); + + validateDependenciesAndInsert(c7AuditLog, context, c7AuditLogId); + } catch (EntityInterceptorException e) { + handleInterceptorException(c7AuditLogId, HISTORY_AUDIT_LOG, c7AuditLog.getTimestamp(), e); + } + } + } + + /** + * Configures the audit log builder with keys and relationships. + *

+ * This method sets the following properties on the builder: + *

    + *
  • auditLogKey - Generated unique key for the audit log entry
  • + *
  • processInstanceKey - Resolved from C7 process instance ID
  • + *
  • rootProcessInstanceKey - Resolved from C7 root process instance ID
  • + *
  • processDefinitionKey - Resolved from C7 process definition ID
  • + *
  • userTaskKey - Resolved from C7 task ID
  • + *
  • timestamp - Converted from C7 timestamp
  • + *
  • historyCleanupDate - Calculated based on TTL configuration
  • + *
+ *

+ * Note: Other properties (actorId, actorType, processDefinitionId, annotation, tenantId, + * tenantScope, category, entityType, operationType, result, partitionId) are set by the + * AuditLogTransformer interceptor during entity conversion. + *

+ * + * @param c7AuditLog the user operation log entry from Camunda 7 + * @return the configured builder + */ + protected AuditLogDbModel.Builder configureAuditLogBuilder(UserOperationLogEntry c7AuditLog) { + AuditLogDbModel.Builder builder = new AuditLogDbModel.Builder(); + + String key = String.format("%s-%s", C7_HISTORY_PARTITION_ID, getNextKey()); + builder.auditLogKey(key); + + resolveEntityKeys(builder, c7AuditLog); + + setHistoryCleanupDate(c7AuditLog, builder); + + return builder; + } + + /** + * Resolves and sets process instance and process definition keys on the builder. + *

+ * This method looks up the Camunda 8 keys for related entities based on the + * Camunda 7 IDs from the audit log entry: + *

    + *
  • processInstanceKey - from C7 process instance ID
  • + *
  • rootProcessInstanceKey - from C7 root process instance ID
  • + *
  • processDefinitionKey - from C7 process definition ID
  • + *
  • userTaskKey - from C7 task ID
  • + *
+ * Keys are only set if the corresponding entity has already been migrated. + *

+ * + * @param builder the audit log builder + * @param c7AuditLog the user operation log entry from Camunda 7 + */ + protected void resolveEntityKeys( + AuditLogDbModel.Builder builder, + UserOperationLogEntry c7AuditLog) { + + String c7ProcessInstanceId = c7AuditLog.getProcessInstanceId(); + String c7RootProcessInstanceId = c7AuditLog.getRootProcessInstanceId(); + if (c7ProcessInstanceId != null && isMigrated(c7ProcessInstanceId, HISTORY_PROCESS_INSTANCE)) { + ProcessInstanceEntity processInstance = findProcessInstanceByC7Id(c7ProcessInstanceId); + builder.processInstanceKey(processInstance.processInstanceKey()); + if (c7RootProcessInstanceId != null && isMigrated(c7RootProcessInstanceId, HISTORY_PROCESS_INSTANCE)) { + ProcessInstanceEntity rootProcessInstance = findProcessInstanceByC7Id(c7RootProcessInstanceId); + if (rootProcessInstance != null && rootProcessInstance.processInstanceKey() != null) { + builder.rootProcessInstanceKey(rootProcessInstance.processInstanceKey()); + } + } + } + + String c7ProcessDefinitionId = c7AuditLog.getProcessDefinitionId(); + if (c7ProcessDefinitionId != null && isMigrated(c7ProcessDefinitionId, HISTORY_PROCESS_DEFINITION)) { + Long processDefinitionKey = findProcessDefinitionKey(c7ProcessDefinitionId); + builder.processDefinitionKey(processDefinitionKey); + } + + String c7TaskId = c7AuditLog.getTaskId(); + if (c7TaskId != null && isMigrated(c7TaskId, HISTORY_USER_TASK)) { + Long taskKey = dbClient.findC8KeyByC7IdAndType(c7TaskId, + HISTORY_USER_TASK); + UserTaskDbModel userTaskDbModel = searchUserTasksByKey(taskKey); + builder.userTaskKey(taskKey) + .elementInstanceKey(userTaskDbModel.elementInstanceKey()); + } + } + + protected UserTaskDbModel searchUserTasksByKey(Long taskKey) { + return c8Client.searchUserTasks(UserTaskDbQuery.of(b -> b.filter(f -> f.userTaskKeys(taskKey)))) + .stream() + .findFirst() + .orElse(null); + } + + /** + * Validates dependencies and inserts the audit log or marks it as skipped. + * + * @param c7AuditLog the user operation log entry from Camunda 7 + * @param context the entity conversion context + * @param c7AuditLogId the Camunda 7 audit log ID + */ + protected void validateDependenciesAndInsert( + UserOperationLogEntry c7AuditLog, + EntityConversionContext context, + String c7AuditLogId) { + + AuditLogDbModel dbModel = convertAuditLog(context); + + if (c7AuditLog.getProcessDefinitionKey() != null && dbModel.processDefinitionKey() == null) { + markSkipped(c7AuditLogId, HISTORY_AUDIT_LOG, c7AuditLog.getTimestamp(), + SKIP_REASON_MISSING_PROCESS_DEFINITION); + HistoryMigratorLogs.skippingAuditLogDueToMissingDefinition(c7AuditLogId); + } else if (c7AuditLog.getProcessInstanceId() != null && dbModel.processInstanceKey() == null) { + markSkipped(c7AuditLogId, HISTORY_AUDIT_LOG, c7AuditLog.getTimestamp(), + SKIP_REASON_MISSING_PROCESS_INSTANCE); + HistoryMigratorLogs.skippingAuditLogDueToMissingProcess(c7AuditLogId); + } else if (c7AuditLog.getRootProcessInstanceId() != null && dbModel.rootProcessInstanceKey() == null) { + markSkipped(c7AuditLogId, HISTORY_AUDIT_LOG, c7AuditLog.getTimestamp(), SKIP_REASON_MISSING_ROOT_PROCESS_INSTANCE); + HistoryMigratorLogs.skippingHistoricAuditLog(SKIPPING_AUDIT_LOG_MISSING_ROOT_INSTANCE, c7AuditLogId); + } else if (c7AuditLog.getTaskId() != null && dbModel.userTaskKey() == null) { + markSkipped(c7AuditLogId, HISTORY_AUDIT_LOG, c7AuditLog.getTimestamp(), SKIP_REASON_BELONGS_TO_SKIPPED_TASK); + HistoryMigratorLogs.skippingHistoricAuditLog(SKIPPING_AUDIT_LOG_MISSING_USER_TASK, c7AuditLogId); + } else { + insertAuditLog(c7AuditLog, dbModel, c7AuditLogId); + } + } + + /** + * Inserts the migrated audit log into Camunda 8 and marks it as migrated. + * + * @param c7AuditLog the original Camunda 7 audit log entry + * @param dbModel the converted Camunda 8 database model + * @param c7AuditLogId the Camunda 7 audit log ID + */ + protected void insertAuditLog(UserOperationLogEntry c7AuditLog, AuditLogDbModel dbModel, String c7AuditLogId) { + c8Client.insertAuditLog(dbModel); + markMigrated(c7AuditLogId, dbModel.auditLogKey(), c7AuditLog.getTimestamp(), HISTORY_AUDIT_LOG); + HistoryMigratorLogs.migratingHistoricAuditLogCompleted(c7AuditLogId); + } + + /** + * Converts a Camunda 7 user operation log entry to a Camunda 8 audit log database model. + * + * @param context the entity conversion context + * @return the converted audit log database model + */ + protected AuditLogDbModel convertAuditLog(EntityConversionContext context) { + EntityConversionContext entityConversionContext = entityConversionService.convertWithContext(context); + AuditLogDbModel.Builder builder = (AuditLogDbModel.Builder) entityConversionContext.getC8DbModelBuilder(); + return builder.build(); + } + + /** + * Sets the history cleanup date and timestamp on the audit log builder. + *

+ * This method: + *

    + *
  • Converts the C7 timestamp to C8 format (OffsetDateTime)
  • + *
  • Calculates the history cleanup date based on the configured TTL
  • + *
  • Sets both timestamp and historyCleanupDate on the builder
  • + *
+ * The historyCleanupDate determines when the audit log entry will be eligible + * for cleanup/deletion in Camunda 8. + *

+ * + * @param c7AuditLog the user operation log entry from Camunda 7 + * @param auditLogDbModelBuilder the audit log builder to configure + */ + protected void setHistoryCleanupDate(UserOperationLogEntry c7AuditLog, + AuditLogDbModel.Builder auditLogDbModelBuilder) { + Date c7EndTime = c7AuditLog.getTimestamp(); + var c8EndTime = calculateEndDate(c7EndTime); + var c8HistoryCleanupDate = calculateHistoryCleanupDate(c8EndTime, c7AuditLog.getRemovalTime()); + + auditLogDbModelBuilder + .historyCleanupDate(c8HistoryCleanupDate) + .timestamp(c8EndTime); + } +} diff --git a/data-migrator/core/src/main/java/io/camunda/migration/data/impl/history/BaseMigrator.java b/data-migrator/core/src/main/java/io/camunda/migration/data/impl/history/BaseMigrator.java index 48bfd2fef..ab958ff02 100644 --- a/data-migrator/core/src/main/java/io/camunda/migration/data/impl/history/BaseMigrator.java +++ b/data-migrator/core/src/main/java/io/camunda/migration/data/impl/history/BaseMigrator.java @@ -14,6 +14,7 @@ import static io.camunda.migration.data.impl.persistence.IdKeyMapper.TYPE.HISTORY_FLOW_NODE; import static io.camunda.migration.data.impl.persistence.IdKeyMapper.TYPE.HISTORY_PROCESS_INSTANCE; import static io.camunda.migration.data.impl.util.ConverterUtil.convertDate; +import static io.camunda.search.entities.DecisionInstanceEntity.DecisionDefinitionType; import io.camunda.db.rdbms.read.domain.DecisionDefinitionDbQuery; import io.camunda.db.rdbms.read.domain.DecisionInstanceDbQuery; @@ -202,6 +203,14 @@ protected boolean shouldMigrate(String id, TYPE type) { return !dbClient.checkExistsByC7IdAndType(id, type); } + protected void markMigrated(String c7Id, String c8Key, Date createTime, TYPE type) { + if (RETRY_SKIPPED.equals(mode)) { + dbClient.updateC8KeyByC7IdAndType(c7Id, c8Key, type); + } else if (MIGRATE.equals(mode)) { + dbClient.insert(c7Id, c8Key, createTime, type, null); + } + } + protected void markMigrated(String c7Id, Long c8Key, Date createTime, TYPE type) { saveRecord(c7Id, c8Key, type, createTime, null); } @@ -241,9 +250,9 @@ protected DecisionInstanceEntity.DecisionDefinitionType determineDecisionType(Dm } if (decision.getExpression() instanceof LiteralExpression) { - return DecisionInstanceEntity.DecisionDefinitionType.LITERAL_EXPRESSION; + return DecisionDefinitionType.LITERAL_EXPRESSION; } else { - return DecisionInstanceEntity.DecisionDefinitionType.DECISION_TABLE; + return DecisionDefinitionType.DECISION_TABLE; } } @@ -326,6 +335,39 @@ protected Period getAutoCancelTtl() { // Return configured TTL (will have default of 180 days if not set) return migratorProperties.getHistory().getAutoCancel().getCleanup().getTtl(); } + /** + * Calculates the history cleanup date for an entity. + * Only calculates a new cleanup date when C7 removalTime is null. + * For entities with existing removalTime, uses that value directly. + * + * @param endTime the C7 or auto-canceled end time + * @param c7RemovalTime the C7 removal time + * @return the calculated history cleanup date + */ + protected OffsetDateTime calculateHistoryCleanupDate(OffsetDateTime endTime, Date c7RemovalTime) { + if (c7RemovalTime != null) { + return convertDate(c7RemovalTime); + } + + Period ttl = getAutoCancelTtl(); + if (ttl == null || ttl.isZero()) { + return null; + } + return endTime.plus(ttl); + } + + /** + * Determines if the entity should have endDate set to now when converting to C8. + * + * @param c7EndDate the C7 end date + * @return the endDate to use (now if active, original if completed) + */ + protected OffsetDateTime calculateEndDate(Date c7EndDate) { + if (c7EndDate == null) { + return convertDate(ClockUtil.now()); + } + return convertDate(c7EndDate); + } /** * Migrates all entities of type T from Camunda 7 to Camunda 8. diff --git a/data-migrator/core/src/main/java/io/camunda/migration/data/impl/history/ProcessInstanceMigrator.java b/data-migrator/core/src/main/java/io/camunda/migration/data/impl/history/ProcessInstanceMigrator.java index b09302a0a..e913f4c0f 100644 --- a/data-migrator/core/src/main/java/io/camunda/migration/data/impl/history/ProcessInstanceMigrator.java +++ b/data-migrator/core/src/main/java/io/camunda/migration/data/impl/history/ProcessInstanceMigrator.java @@ -13,7 +13,6 @@ import static io.camunda.migration.data.impl.logging.HistoryMigratorLogs.SKIP_REASON_MISSING_ROOT_PROCESS_INSTANCE; import static io.camunda.migration.data.impl.persistence.IdKeyMapper.TYPE.HISTORY_PROCESS_DEFINITION; import static io.camunda.migration.data.impl.persistence.IdKeyMapper.TYPE.HISTORY_PROCESS_INSTANCE; -import static io.camunda.migration.data.impl.util.ConverterUtil.convertDate; import static io.camunda.migration.data.impl.util.ConverterUtil.getNextKey; import io.camunda.db.rdbms.write.domain.ProcessInstanceDbModel; @@ -22,11 +21,8 @@ import io.camunda.migration.data.impl.logging.HistoryMigratorLogs; import io.camunda.migration.data.interceptor.property.EntityConversionContext; import io.camunda.search.entities.ProcessInstanceEntity; -import java.time.OffsetDateTime; -import java.time.Period; import java.util.Date; import org.camunda.bpm.engine.history.HistoricProcessInstance; -import org.camunda.bpm.engine.impl.util.ClockUtil; import org.springframework.stereotype.Service; /** @@ -234,39 +230,5 @@ protected void insertProcessInstance(HistoricProcessInstance c7ProcessInstance, HistoryMigratorLogs.migratingProcessInstanceCompleted(c7ProcessInstanceId); } - /** - * Calculates the history cleanup date for an entity. - * Only calculates a new cleanup date when C7 removalTime is null. - * For entities with existing removalTime, uses that value directly. - * - * @param endTime the C7 or auto-canceled end time - * @param c7RemovalTime the C7 removal time - * @return the calculated history cleanup date - */ - protected OffsetDateTime calculateHistoryCleanupDate(OffsetDateTime endTime, Date c7RemovalTime) { - if (c7RemovalTime != null) { - return convertDate(c7RemovalTime); - } - - Period ttl = getAutoCancelTtl(); - if (ttl == null || ttl.isZero()) { - return null; - } - return endTime.plus(ttl); - } - - /** - * Determines if the entity should have endDate set to now when converting to C8. - * - * @param c7EndDate the C7 end date - * @return the endDate to use (now if active, original if completed) - */ - protected OffsetDateTime calculateEndDate(Date c7EndDate) { - if (c7EndDate == null) { - return convertDate(ClockUtil.now()); - } - return convertDate(c7EndDate); - } - } diff --git a/data-migrator/core/src/main/java/io/camunda/migration/data/impl/interceptor/history/entity/AuditLogTransformer.java b/data-migrator/core/src/main/java/io/camunda/migration/data/impl/interceptor/history/entity/AuditLogTransformer.java new file mode 100644 index 000000000..e25cef1e0 --- /dev/null +++ b/data-migrator/core/src/main/java/io/camunda/migration/data/impl/interceptor/history/entity/AuditLogTransformer.java @@ -0,0 +1,221 @@ +/* + * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH under + * one or more contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright ownership. + * Licensed under the Camunda License 1.0. You may not use this file + * except in compliance with the Camunda License 1.0. + */ +package io.camunda.migration.data.impl.interceptor.history.entity; + +import static io.camunda.migration.data.constants.MigratorConstants.C7_AUDIT_LOG_ENTITY_VERSION; +import static io.camunda.migration.data.constants.MigratorConstants.C7_HISTORY_PARTITION_ID; +import static io.camunda.migration.data.constants.MigratorConstants.C8_DEFAULT_TENANT; +import static io.camunda.migration.data.impl.util.ConverterUtil.getTenantId; +import static io.camunda.migration.data.impl.util.ConverterUtil.prefixDefinitionId; + +import io.camunda.db.rdbms.write.domain.AuditLogDbModel; +import io.camunda.migration.data.exception.EntityInterceptorException; +import io.camunda.migration.data.impl.logging.HistoryMigratorLogs; +import io.camunda.migration.data.interceptor.EntityInterceptor; +import io.camunda.migration.data.interceptor.property.EntityConversionContext; +import io.camunda.search.entities.AuditLogEntity; +import java.util.Set; +import org.camunda.bpm.engine.EntityTypes; +import org.camunda.bpm.engine.history.UserOperationLogEntry; +import org.jspecify.annotations.NonNull; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +/** + * Transformer for converting Camunda 7 UserOperationLogEntry to Camunda 8 AuditLogDbModel. + *

+ * This transformer handles the conversion of user operation logs (audit logs) from Camunda 7 + * to the Camunda 8 audit log format. It maps operation details, timestamps, user information, + * and entity references. + *

+ */ +@Order(12) +@Component +public class AuditLogTransformer implements EntityInterceptor { + + @Override + public Set> getTypes() { + return Set.of(UserOperationLogEntry.class); + } + + @Override + public void execute(EntityConversionContext context) { + UserOperationLogEntry userOperationLog = (UserOperationLogEntry) context.getC7Entity(); + AuditLogDbModel.Builder builder = (AuditLogDbModel.Builder) context.getC8DbModelBuilder(); + + if (builder == null) { + throw new EntityInterceptorException("C8 AuditLogDbModel.Builder is null in context"); + } + String tenantId = getTenantId(userOperationLog.getTenantId()); + builder + .entityType(convertEntityType(userOperationLog)) + .operationType(convertOperationType(userOperationLog, builder)) + .partitionId(C7_HISTORY_PARTITION_ID) + .result(AuditLogEntity.AuditLogOperationResult.SUCCESS) + .entityVersion(C7_AUDIT_LOG_ENTITY_VERSION) + .actorId(userOperationLog.getUserId()) + .actorType(AuditLogEntity.AuditLogActorType.USER) + .processDefinitionId(prefixDefinitionId(userOperationLog.getProcessDefinitionKey())) + .annotation(userOperationLog.getAnnotation()) + .tenantId(tenantId) + .tenantScope(getAuditLogTenantScope(tenantId)) + .category(convertCategory(userOperationLog.getCategory())); + // Note: auditLogKey, processInstanceKey, rootProcessInstanceKey, processDefinitionKey, userTaskKey, timestamp, historyCleanupDate + // are set externally in AuditLogMigrator + + updateEntityTypesThatDontMatchBetweenC7andC8(userOperationLog, builder); + + } + + protected AuditLogEntity.@NonNull AuditLogTenantScope getAuditLogTenantScope(String tenantId) { + AuditLogEntity.AuditLogTenantScope tenantScope; + if ( tenantId.equals(C8_DEFAULT_TENANT)) { + tenantScope = AuditLogEntity.AuditLogTenantScope.GLOBAL; + } else { + tenantScope = AuditLogEntity.AuditLogTenantScope.TENANT; + } + return tenantScope; + } + + protected AuditLogEntity.AuditLogOperationCategory convertCategory(String category) { + return switch (category) { + case UserOperationLogEntry.CATEGORY_ADMIN -> AuditLogEntity.AuditLogOperationCategory.ADMIN; + case UserOperationLogEntry.CATEGORY_OPERATOR -> AuditLogEntity.AuditLogOperationCategory.DEPLOYED_RESOURCES; + case UserOperationLogEntry.CATEGORY_TASK_WORKER -> AuditLogEntity.AuditLogOperationCategory.USER_TASKS; + default -> AuditLogEntity.AuditLogOperationCategory.UNKNOWN; + }; + } + protected AuditLogEntity.AuditLogEntityType convertEntityType(UserOperationLogEntry userOperationLog) { + return switch (userOperationLog.getEntityType()) { + case EntityTypes.PROCESS_INSTANCE -> AuditLogEntity.AuditLogEntityType.PROCESS_INSTANCE; + case EntityTypes.VARIABLE -> AuditLogEntity.AuditLogEntityType.VARIABLE; + case EntityTypes.TASK -> AuditLogEntity.AuditLogEntityType.USER_TASK; + case EntityTypes.DECISION_INSTANCE, EntityTypes.DECISION_DEFINITION, EntityTypes.DECISION_REQUIREMENTS_DEFINITION -> AuditLogEntity.AuditLogEntityType.DECISION; + case EntityTypes.USER -> AuditLogEntity.AuditLogEntityType.USER; + case EntityTypes.GROUP -> AuditLogEntity.AuditLogEntityType.GROUP; + case EntityTypes.TENANT -> AuditLogEntity.AuditLogEntityType.TENANT; + case EntityTypes.AUTHORIZATION -> AuditLogEntity.AuditLogEntityType.AUTHORIZATION; + case EntityTypes.INCIDENT -> AuditLogEntity.AuditLogEntityType.INCIDENT; + case EntityTypes.PROCESS_DEFINITION, EntityTypes.DEPLOYMENT -> AuditLogEntity.AuditLogEntityType.RESOURCE; + + // Camunda 7 entity types that are currently NOT converted: + // EntityTypes.BATCH, EntityTypes.IDENTITY_LINK, EntityTypes.ATTACHMENT, EntityTypes.JOB_DEFINITION, + // EntityTypes.JOB, EntityTypes.EXTERNAL_TASK, EntityTypes.CASE_DEFINITION, EntityTypes.CASE_INSTANCE, + // EntityTypes.METRICS, EntityTypes.TASK_METRICS, EntityTypes.OPERATION_LOG, EntityTypes.FILTER, + // EntityTypes.COMMENT, EntityTypes.PROPERTY + + default -> throw new EntityInterceptorException( + HistoryMigratorLogs.UNSUPPORTED_AUDIT_LOG_ENTITY_TYPE + userOperationLog.getEntityType()); + }; + } + + protected AuditLogEntity.AuditLogOperationType convertOperationType(UserOperationLogEntry userOperationLog, AuditLogDbModel.Builder builder) { + String operationType = userOperationLog.getOperationType(); + + return switch (operationType) { + // Task operations + case UserOperationLogEntry.OPERATION_TYPE_ASSIGN, + UserOperationLogEntry.OPERATION_TYPE_CLAIM, + UserOperationLogEntry.OPERATION_TYPE_DELEGATE -> + AuditLogEntity.AuditLogOperationType.ASSIGN; + case UserOperationLogEntry.OPERATION_TYPE_COMPLETE -> + AuditLogEntity.AuditLogOperationType.COMPLETE; + case UserOperationLogEntry.OPERATION_TYPE_SET_PRIORITY, + UserOperationLogEntry.OPERATION_TYPE_SET_OWNER, + UserOperationLogEntry.OPERATION_TYPE_UPDATE -> + AuditLogEntity.AuditLogOperationType.UPDATE; + + // ProcessInstance operations + case UserOperationLogEntry.OPERATION_TYPE_CREATE -> + AuditLogEntity.AuditLogOperationType.CREATE; + case UserOperationLogEntry.OPERATION_TYPE_DELETE -> { + // ProcessInstance Delete maps to CANCEL, but other entity types map to DELETE + if (EntityTypes.PROCESS_INSTANCE.equals(userOperationLog.getEntityType())) { + yield AuditLogEntity.AuditLogOperationType.CANCEL; + } else { + yield AuditLogEntity.AuditLogOperationType.DELETE; + } + } + case UserOperationLogEntry.OPERATION_TYPE_MODIFY_PROCESS_INSTANCE -> + AuditLogEntity.AuditLogOperationType.MODIFY; + case UserOperationLogEntry.OPERATION_TYPE_MIGRATE -> + AuditLogEntity.AuditLogOperationType.MIGRATE; + case UserOperationLogEntry.OPERATION_TYPE_DELETE_HISTORY, UserOperationLogEntry.OPERATION_TYPE_REMOVE_VARIABLE -> + AuditLogEntity.AuditLogOperationType.DELETE; + + // Variable operations + case UserOperationLogEntry.OPERATION_TYPE_MODIFY_VARIABLE, UserOperationLogEntry.OPERATION_TYPE_SET_VARIABLE, + UserOperationLogEntry.OPERATION_TYPE_SET_VARIABLES -> + AuditLogEntity.AuditLogOperationType.UPDATE; + + // DecisionDefinition operations + case UserOperationLogEntry.OPERATION_TYPE_EVALUATE -> + AuditLogEntity.AuditLogOperationType.EVALUATE; + + // Incident operations + case UserOperationLogEntry.OPERATION_TYPE_RESOLVE -> { + if (EntityTypes.PROCESS_INSTANCE.equals(userOperationLog.getEntityType())) { + builder.entityType(AuditLogEntity.AuditLogEntityType.INCIDENT); + yield AuditLogEntity.AuditLogOperationType.RESOLVE; + } else { + yield AuditLogEntity.AuditLogOperationType.UPDATE; + } + } + + default -> + throw new EntityInterceptorException(HistoryMigratorLogs.UNSUPPORTED_AUDIT_LOG_OPERATION_TYPE + operationType); + }; + } + + protected void updateEntityTypesThatDontMatchBetweenC7andC8(UserOperationLogEntry userOperationLog, AuditLogDbModel.Builder builder) { + if (UserOperationLogEntry.OPERATION_TYPE_RESOLVE.equals(userOperationLog.getOperationType()) + && EntityTypes.PROCESS_INSTANCE.equals(userOperationLog.getEntityType())) { + builder.entityType(AuditLogEntity.AuditLogEntityType.INCIDENT); + } else if (UserOperationLogEntry.OPERATION_TYPE_SET_VARIABLE.equals(userOperationLog.getOperationType()) + || UserOperationLogEntry.OPERATION_TYPE_SET_VARIABLES.equals(userOperationLog.getOperationType())) { + builder.entityType(AuditLogEntity.AuditLogEntityType.VARIABLE); + } + } + + + // entityValueType and entityOperationIntent are internal properties meant for future-proofing purposes + // protected void convertValueType(UserOperationLogEntry userOperationLog, AuditLogDbModel.Builder builder) { + // switch (userOperationLog.getEntityType()) { + // case EntityTypes.PROCESS_INSTANCE -> { + // switch (userOperationLog.getOperationType()) { + // case UserOperationLogEntry.OPERATION_TYPE_CREATE -> + // builder.entityValueType(ValueType.PROCESS_INSTANCE_CREATION.value()); + // case UserOperationLogEntry.OPERATION_TYPE_MODIFY_PROCESS_INSTANCE -> + // builder.entityValueType(ValueType.PROCESS_INSTANCE_MODIFICATION.value()); + // case UserOperationLogEntry.OPERATION_TYPE_MIGRATE -> + // builder.entityValueType(ValueType.PROCESS_INSTANCE_MIGRATION.value()); + // default -> builder.entityValueType(ValueType.PROCESS_INSTANCE.value()); + // } + // } + // case EntityTypes.VARIABLE -> builder.entityValueType(ValueType.VARIABLE.value()); + // case EntityTypes.TASK -> builder.entityValueType(ValueType.USER_TASK.value()); + // case EntityTypes.DECISION_INSTANCE, EntityTypes.DECISION_DEFINITION -> { + // if (userOperationLog.getOperationType().equals(UserOperationLogEntry.OPERATION_TYPE_EVALUATE)) { + // builder.entityValueType(ValueType.DECISION_EVALUATION.value()); + // } else { + // builder.entityValueType(ValueType.DECISION.value()); + // } + // } + // case EntityTypes.DECISION_REQUIREMENTS_DEFINITION -> + // builder.entityValueType(ValueType.DECISION_REQUIREMENTS.value()); + // case EntityTypes.USER -> builder.entityValueType(ValueType.USER.value()); + // case EntityTypes.GROUP -> builder.entityValueType(ValueType.GROUP.value()); + // case EntityTypes.TENANT -> builder.entityValueType(ValueType.TENANT.value()); + // case EntityTypes.AUTHORIZATION -> builder.entityValueType(ValueType.AUTHORIZATION.value()); + // case EntityTypes.PROCESS_DEFINITION -> builder.entityValueType(ValueType.PROCESS.value()); + // case EntityTypes.DEPLOYMENT -> builder.entityValueType(ValueType.RESOURCE.value()); + // case EntityTypes.INCIDENT -> builder.entityValueType(ValueType.INCIDENT.value()); + // default -> builder.entityValueType(ValueType.SBE_UNKNOWN.value()); + // } + // } +} diff --git a/data-migrator/core/src/main/java/io/camunda/migration/data/impl/logging/C8ClientLogs.java b/data-migrator/core/src/main/java/io/camunda/migration/data/impl/logging/C8ClientLogs.java index 00520bc58..c86cca5e3 100644 --- a/data-migrator/core/src/main/java/io/camunda/migration/data/impl/logging/C8ClientLogs.java +++ b/data-migrator/core/src/main/java/io/camunda/migration/data/impl/logging/C8ClientLogs.java @@ -39,5 +39,7 @@ public class C8ClientLogs { public static final String FAILED_TO_INSERT_USER_TASK = "Failed to insert user task"; public static final String FAILED_TO_INSERT_FLOW_NODE_INSTANCE = "Failed to insert flow node instance"; public static final String FAILED_TO_SEARCH_FLOW_NODE_INSTANCES = "Failed to search flow node instances"; + public static final String FAILED_TO_SEARCH_USER_TASKS = "Failed to search user tasks"; + public static final String FAILED_TO_INSERT_AUDIT_LOG = "Failed to insert audit log"; public static final String FAILED_TO_MIGRATE_AUTHORIZATION = "Failed to migrate authorization with legacy ID: "; } diff --git a/data-migrator/core/src/main/java/io/camunda/migration/data/impl/logging/DbClientLogs.java b/data-migrator/core/src/main/java/io/camunda/migration/data/impl/logging/DbClientLogs.java index 5516b659a..599cccdbb 100644 --- a/data-migrator/core/src/main/java/io/camunda/migration/data/impl/logging/DbClientLogs.java +++ b/data-migrator/core/src/main/java/io/camunda/migration/data/impl/logging/DbClientLogs.java @@ -42,7 +42,7 @@ public class DbClientLogs { public static final String FAILED_TO_DELETE = "Failed to delete mapping for C7 ID: "; public static final String FAILED_TO_DROP_MIGRATION_TABLE = "Failed to drop migration mapping table"; - public static void updatingC8KeyForC7Id(String c7Id, Long c8Key) { + public static void updatingC8KeyForC7Id(String c7Id, String c8Key) { LOGGER.debug(UPDATING_KEY_FOR_C7_ID, c7Id, c8Key); } diff --git a/data-migrator/core/src/main/java/io/camunda/migration/data/impl/logging/HistoryMigratorLogs.java b/data-migrator/core/src/main/java/io/camunda/migration/data/impl/logging/HistoryMigratorLogs.java index c70fb3154..62eb3a6f1 100644 --- a/data-migrator/core/src/main/java/io/camunda/migration/data/impl/logging/HistoryMigratorLogs.java +++ b/data-migrator/core/src/main/java/io/camunda/migration/data/impl/logging/HistoryMigratorLogs.java @@ -90,6 +90,17 @@ public class HistoryMigratorLogs { + "skipped. Missing parent flow node. Rerun migration for skipped flow nodes to ensure complete migration."; public static final String SKIPPING_FLOW_NODE_MISSING_ROOT = "Migration of historic flow nodes with C7 ID [{}] skipped. Root process instance yet not available."; + public static final String MIGRATING_AUDIT_LOGS = "Migrating historic audit logs"; + public static final String MIGRATING_AUDIT_LOG = "Migrating historic audit log with C7 ID: [{}]"; + public static final String MIGRATING_AUDIT_LOG_COMPLETED = "Migration of historic audit log with C7 ID [{}] completed."; + public static final String SKIPPING_AUDIT_LOG = "Migration of historic audit log with C7 ID [{}] skipped."; + public static final String SKIPPING_AUDIT_LOG_MISSING_DEFINITION = SKIPPING_AUDIT_LOG + " Process definition not yet available."; + public static final String SKIPPING_AUDIT_LOG_MISSING_PROCESS = SKIPPING_AUDIT_LOG + " Process instance not yet available."; + public static final String SKIPPING_AUDIT_LOG_MISSING_ROOT_INSTANCE = SKIPPING_AUDIT_LOG + " Root process instance not yet available."; + public static final String SKIPPING_AUDIT_LOG_MISSING_USER_TASK = SKIPPING_AUDIT_LOG + " User task not yet available."; + public static final String UNSUPPORTED_AUDIT_LOG_ENTITY_TYPE = "Can't migrate Audit log for entity type: "; + public static final String UNSUPPORTED_AUDIT_LOG_OPERATION_TYPE = "Can't migrate Audit log for operation type: "; + public static final String MIGRATING_DECISION_REQUIREMENTS = "Migrating decision requirements"; public static final String MIGRATING_DECISION_REQUIREMENT = "Migrating decision requirements with C7 ID: [{}]"; public static final String MIGRATING_DECISION_REQUIREMENT_COMPLETED = "Migration of decision requirements with C7 ID [{}] completed."; @@ -314,4 +325,28 @@ public static void creatingDecisionRequirement(String c7DecisionId) { public static void creatingDecisionRequirementCompleted(String c7DecisionId) { LOGGER.debug(CREATING_DECISION_REQUIREMENT_COMPLETED, c7DecisionId); } + + public static void migratingHistoricAuditLogs() { + LOGGER.info(MIGRATING_AUDIT_LOGS); + } + + public static void migratingHistoricAuditLog(String c7AuditLogId) { + LOGGER.debug(MIGRATING_AUDIT_LOG, c7AuditLogId); + } + + public static void migratingHistoricAuditLogCompleted(String c7AuditLogId) { + LOGGER.debug(MIGRATING_AUDIT_LOG_COMPLETED, c7AuditLogId); + } + + public static void skippingHistoricAuditLog(String reason, String c7AuditLogId) { + LOGGER.debug(reason, c7AuditLogId); + } + + public static void skippingAuditLogDueToMissingDefinition(String c7AuditLogId) { + LOGGER.debug(SKIPPING_AUDIT_LOG_MISSING_DEFINITION, c7AuditLogId); + } + + public static void skippingAuditLogDueToMissingProcess(String c7AuditLogId) { + LOGGER.debug(SKIPPING_AUDIT_LOG_MISSING_PROCESS, c7AuditLogId); + } } diff --git a/data-migrator/core/src/main/java/io/camunda/migration/data/impl/persistence/IdKeyDbModel.java b/data-migrator/core/src/main/java/io/camunda/migration/data/impl/persistence/IdKeyDbModel.java index 70766769f..4fffbab27 100644 --- a/data-migrator/core/src/main/java/io/camunda/migration/data/impl/persistence/IdKeyDbModel.java +++ b/data-migrator/core/src/main/java/io/camunda/migration/data/impl/persistence/IdKeyDbModel.java @@ -14,7 +14,7 @@ public class IdKeyDbModel { protected String c7Id; - protected Long c8Key; + protected String c8Key; protected TYPE type; protected Date createTime; protected String skipReason; @@ -27,7 +27,7 @@ public IdKeyDbModel(String c7Id, Date createTime) { this.createTime = createTime; } - public Long getC8Key() { + public String getC8Key() { return c8Key; } @@ -47,7 +47,7 @@ public String getSkipReason() { return skipReason; } - public void setC8Key(Long c8Key) { + public void setC8Key(String c8Key) { this.c8Key = c8Key; } diff --git a/data-migrator/core/src/main/java/io/camunda/migration/data/impl/persistence/IdKeyMapper.java b/data-migrator/core/src/main/java/io/camunda/migration/data/impl/persistence/IdKeyMapper.java index e21dd300f..b6494613b 100644 --- a/data-migrator/core/src/main/java/io/camunda/migration/data/impl/persistence/IdKeyMapper.java +++ b/data-migrator/core/src/main/java/io/camunda/migration/data/impl/persistence/IdKeyMapper.java @@ -27,6 +27,7 @@ enum TYPE { HISTORY_DECISION_INSTANCE("Historic Decision Instance"), HISTORY_DECISION_DEFINITION("Historic Decision Definition"), HISTORY_DECISION_REQUIREMENT("Historic Decision Requirement"), + HISTORY_AUDIT_LOG("Historic Audit Log"), RUNTIME_PROCESS_INSTANCE("Process Instance"), diff --git a/data-migrator/core/src/main/resources/db/changelog/migrator/db.0.3.0.xml b/data-migrator/core/src/main/resources/db/changelog/migrator/db.0.3.0.xml new file mode 100644 index 000000000..4c200a71f --- /dev/null +++ b/data-migrator/core/src/main/resources/db/changelog/migrator/db.0.3.0.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + diff --git a/data-migrator/core/src/main/resources/db/changelog/migrator/db.changelog-master.yaml b/data-migrator/core/src/main/resources/db/changelog/migrator/db.changelog-master.yaml index 66748a212..ddcaf1489 100644 --- a/data-migrator/core/src/main/resources/db/changelog/migrator/db.changelog-master.yaml +++ b/data-migrator/core/src/main/resources/db/changelog/migrator/db.changelog-master.yaml @@ -1,3 +1,5 @@ databaseChangeLog: - include: file: db/changelog/migrator/db.0.1.0.xml + - include: + file: db/changelog/migrator/db.0.3.0.xml diff --git a/data-migrator/core/src/main/resources/mapper/IdKey.xml b/data-migrator/core/src/main/resources/mapper/IdKey.xml index 88809a16c..d32fc7a3d 100644 --- a/data-migrator/core/src/main/resources/mapper/IdKey.xml +++ b/data-migrator/core/src/main/resources/mapper/IdKey.xml @@ -11,7 +11,7 @@ - + @@ -58,7 +58,7 @@ UPDATE ${prefix}MIGRATION_MAPPING - SET C8_KEY = #{c8Key, jdbcType=BIGINT} + SET C8_KEY = #{c8Key, jdbcType=VARCHAR} WHERE C7_ID = #{c7Id, jdbcType=VARCHAR} AND TYPE = #{type, jdbcType=VARCHAR} @@ -100,7 +100,7 @@ parameterType="io.camunda.migration.data.impl.persistence.IdKeyDbModel" flushCache="true"> INSERT INTO ${prefix}MIGRATION_MAPPING (C7_ID, C8_KEY, CREATE_TIME, TYPE, SKIP_REASON) - VALUES (#{c7Id, jdbcType=VARCHAR}, #{c8Key, jdbcType=BIGINT}, #{createTime, jdbcType=TIMESTAMP}, #{type, jdbcType=VARCHAR}, #{skipReason, jdbcType=VARCHAR}) + VALUES (#{c7Id, jdbcType=VARCHAR}, #{c8Key, jdbcType=VARCHAR}, #{createTime, jdbcType=TIMESTAMP}, #{type, jdbcType=VARCHAR}, #{skipReason, jdbcType=VARCHAR}) diff --git a/data-migrator/plugins/cockpit/src/test/java/io/camunda/migration/data/plugin/cockpit/resources/MigratorResourceTest.java b/data-migrator/plugins/cockpit/src/test/java/io/camunda/migration/data/plugin/cockpit/resources/MigratorResourceTest.java index ec31869a6..e88275991 100644 --- a/data-migrator/plugins/cockpit/src/test/java/io/camunda/migration/data/plugin/cockpit/resources/MigratorResourceTest.java +++ b/data-migrator/plugins/cockpit/src/test/java/io/camunda/migration/data/plugin/cockpit/resources/MigratorResourceTest.java @@ -160,7 +160,7 @@ protected void assertIdKeyDbModelListsEqual(List expected, List searchHistoricProcessInstances(String process return searchHistoricProcessInstances(processDefinitionId, false); } + public List searchAuditLogs(String processDefinitionId) { + return rdbmsService.getAuditLogReader() + .search(AuditLogQuery.of(q -> q.filter(f -> + f.processDefinitionIds(prefixDefinitionId(processDefinitionId))))) + .items(); + } + + public List searchAuditLogsByCategory(String name) { + return rdbmsService.getAuditLogReader() + .search(AuditLogQuery.of(q -> q.filter(f -> + f.categories(name)))) + .items(); + } + + public List searchAuditLogss(String processDefinitionId) { + return rdbmsService.getAuditLogReader() + .search(AuditLogQuery.of(q -> q.filter(f -> + f.processInstanceKeys(123L)))) + .items(); + } + /** * When the built-in ProcessInstanceTransformer is disabled, the processDefinitionId * is NOT prefixed during migration. This method allows searching with or without prefixing. diff --git a/data-migrator/qa/integration-tests/src/test/java/io/camunda/migration/data/qa/history/HistoryMigrationListSkippedTest.java b/data-migrator/qa/integration-tests/src/test/java/io/camunda/migration/data/qa/history/HistoryMigrationListSkippedTest.java index 89d03455a..89b30131b 100644 --- a/data-migrator/qa/integration-tests/src/test/java/io/camunda/migration/data/qa/history/HistoryMigrationListSkippedTest.java +++ b/data-migrator/qa/integration-tests/src/test/java/io/camunda/migration/data/qa/history/HistoryMigrationListSkippedTest.java @@ -235,6 +235,7 @@ protected void verifySkippedEntitiesOutput(Map> skippedEnti IdKeyMapper.TYPE.HISTORY_FLOW_NODE.getDisplayName(), IdKeyMapper.TYPE.HISTORY_USER_TASK.getDisplayName(), IdKeyMapper.TYPE.HISTORY_VARIABLE.getDisplayName(), + IdKeyMapper.TYPE.HISTORY_AUDIT_LOG.getDisplayName(), IdKeyMapper.TYPE.HISTORY_INCIDENT.getDisplayName(), IdKeyMapper.TYPE.HISTORY_DECISION_DEFINITION.getDisplayName(), IdKeyMapper.TYPE.HISTORY_DECISION_REQUIREMENT.getDisplayName(), diff --git a/data-migrator/qa/integration-tests/src/test/java/io/camunda/migration/data/qa/history/IdKeyCreateTimeMappingTest.java b/data-migrator/qa/integration-tests/src/test/java/io/camunda/migration/data/qa/history/IdKeyCreateTimeMappingTest.java index a85c02910..12754942f 100644 --- a/data-migrator/qa/integration-tests/src/test/java/io/camunda/migration/data/qa/history/IdKeyCreateTimeMappingTest.java +++ b/data-migrator/qa/integration-tests/src/test/java/io/camunda/migration/data/qa/history/IdKeyCreateTimeMappingTest.java @@ -44,6 +44,6 @@ public void shouldCorrectlyMapCreateTimeDuringActualMigration() { assertThat(migratedInstance.getC7Id()).isEqualTo(processInstanceId); assertThat(migratedInstance.getCreateTime()).isNotNull().isBeforeOrEqualTo(beforeMigration); // Should be before we started the test - assertThat(migratedInstance.getC8Key()).isNotNull().isPositive(); + assertThat(Long.valueOf(migratedInstance.getC8Key())).isNotNull().isPositive(); } } diff --git a/data-migrator/qa/integration-tests/src/test/java/io/camunda/migration/data/qa/history/TransactionRollbackTest.java b/data-migrator/qa/integration-tests/src/test/java/io/camunda/migration/data/qa/history/TransactionRollbackTest.java index 85ab3a628..b2fbb704a 100644 --- a/data-migrator/qa/integration-tests/src/test/java/io/camunda/migration/data/qa/history/TransactionRollbackTest.java +++ b/data-migrator/qa/integration-tests/src/test/java/io/camunda/migration/data/qa/history/TransactionRollbackTest.java @@ -363,7 +363,7 @@ protected void configureSpyToFailOnFirstInsert(IdKeyMapper.TYPE entityType) { return invocation.callRealMethod(); }).when(dbClient).insert( anyString(), - anyLong(), + anyString(), any(Date.class), any(IdKeyMapper.TYPE.class), any()); diff --git a/data-migrator/qa/integration-tests/src/test/java/io/camunda/migration/data/qa/history/entity/HistoryAuditLogAdminTest.java b/data-migrator/qa/integration-tests/src/test/java/io/camunda/migration/data/qa/history/entity/HistoryAuditLogAdminTest.java new file mode 100644 index 000000000..de28e515e --- /dev/null +++ b/data-migrator/qa/integration-tests/src/test/java/io/camunda/migration/data/qa/history/entity/HistoryAuditLogAdminTest.java @@ -0,0 +1,410 @@ +/* + * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH under + * one or more contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright ownership. + * Licensed under the Camunda License 1.0. You may not use this file + * except in compliance with the Camunda License 1.0. + */ +package io.camunda.migration.data.qa.history.entity; + +import static io.camunda.migration.data.constants.MigratorConstants.C8_DEFAULT_TENANT; +import static org.assertj.core.api.Assertions.assertThat; + +import io.camunda.migration.data.qa.history.HistoryMigrationAbstractTest; +import io.camunda.search.entities.AuditLogEntity; +import java.util.List; +import org.camunda.bpm.engine.AuthorizationService; +import org.camunda.bpm.engine.IdentityService; +import org.camunda.bpm.engine.authorization.Authorization; +import org.camunda.bpm.engine.authorization.Permissions; +import org.camunda.bpm.engine.authorization.Resources; +import org.camunda.bpm.engine.history.UserOperationLogEntry; +import org.camunda.bpm.engine.identity.User; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +public class HistoryAuditLogAdminTest extends HistoryMigrationAbstractTest { + + @Autowired + protected IdentityService identityService; + + @Autowired + protected AuthorizationService authorizationService; + + @AfterEach + public void cleanupData() { + identityService.clearAuthentication(); + historyService.createUserOperationLogQuery().list().forEach(log -> + historyService.deleteUserOperationLogEntry(log.getId())); + identityService.createUserQuery().list().forEach(user -> + identityService.deleteUser(user.getId())); + identityService.createGroupQuery().list().forEach(group -> + identityService.deleteGroup(group.getId())); + identityService.createTenantQuery().list().forEach(tenant -> + identityService.deleteTenant(tenant.getId())); + authorizationService.createAuthorizationQuery().list().forEach(authorization -> + authorizationService.deleteAuthorization(authorization.getId())); + } + + @Test + public void shouldMigrateAuditLogsForUser() { + // given + identityService.setAuthenticatedUserId("demo"); + var user = identityService.newUser("newUserId"); + identityService.saveUser(user); + + long auditLogCount = historyService.createUserOperationLogQuery() + .count(); + assertThat(auditLogCount).isEqualTo(1); + String annotation = "anAnnotation"; + historyService.setAnnotationForOperationLogById(historyService.createUserOperationLogQuery().singleResult().getOperationId(), annotation); + + // when + historyMigrator.migrate(); + + // then + List logs = searchAuditLogsByCategory(AuditLogEntity.AuditLogOperationCategory.ADMIN.name()); + assertThat(logs).hasSize(1); + AuditLogEntity log = logs.getFirst(); + + assertThat(log.auditLogKey()).isNotNull(); + assertThat(log.processInstanceKey()).isNull(); + assertThat(log.rootProcessInstanceKey()).isNull(); + assertThat(log.processDefinitionKey()).isNull(); + assertThat(log.userTaskKey()).isNull(); + assertThat(log.timestamp()).isNotNull(); + assertThat(log.actorId()).isEqualTo("demo"); + assertThat(log.actorType()).isEqualTo(AuditLogEntity.AuditLogActorType.USER); + assertThat(log.processDefinitionId()).isNull(); + assertThat(log.annotation()).isEqualTo(annotation); + assertThat(log.tenantId()).isEqualTo(C8_DEFAULT_TENANT); + assertThat(log.tenantScope()).isEqualTo(AuditLogEntity.AuditLogTenantScope.GLOBAL); + assertThat(log.entityType()).isEqualTo(AuditLogEntity.AuditLogEntityType.USER); + assertThat(log.operationType()).isEqualTo(AuditLogEntity.AuditLogOperationType.CREATE); + assertThat(log.result()).isEqualTo(AuditLogEntity.AuditLogOperationResult.SUCCESS); + } + + @Test + public void shouldMigrateAuditLogsForCreateUser() { + // given + identityService.setAuthenticatedUserId("demo"); + var user = identityService.newUser("newUserId"); + identityService.saveUser(user); + + long auditLogCount = historyService.createUserOperationLogQuery() + .count(); + assertThat(auditLogCount).isEqualTo(1); + + // when + historyMigrator.migrate(); + + // then + List logs = searchAuditLogsByCategory(AuditLogEntity.AuditLogOperationCategory.ADMIN.name()); + assertThat(logs).hasSize(1); + + assertThat(logs).extracting(AuditLogEntity::entityType).containsOnly(AuditLogEntity.AuditLogEntityType.USER); + assertThat(logs).extracting(AuditLogEntity::operationType) + .containsOnly(AuditLogEntity.AuditLogOperationType.CREATE); + } + + @Test + public void shouldMigrateAuditLogsForUpdateUser() { + // given + var user = identityService.newUser("newUserId"); + user.setFirstName("John"); + identityService.saveUser(user); + + // Update the user + identityService.setAuthenticatedUserId("demo"); + user.setFirstName("Jane"); + identityService.saveUser(user); + + long auditLogCount = historyService.createUserOperationLogQuery().count(); + assertThat(auditLogCount).isEqualTo(1); + + // when + historyMigrator.migrate(); + + // then + List logs = searchAuditLogsByCategory(AuditLogEntity.AuditLogOperationCategory.ADMIN.name()); + assertThat(logs).hasSize(1); + + assertThat(logs).extracting(AuditLogEntity::entityType).containsOnly(AuditLogEntity.AuditLogEntityType.USER); + assertThat(logs).extracting(AuditLogEntity::operationType) + .containsOnly(AuditLogEntity.AuditLogOperationType.UPDATE); + } + + @Test + public void shouldMigrateAuditLogsForDeleteUser() { + // given + var user = identityService.newUser("newUserId"); + identityService.saveUser(user); + + // Delete the user + identityService.setAuthenticatedUserId("demo"); + identityService.deleteUser("newUserId"); + + long auditLogCount = historyService.createUserOperationLogQuery().count(); + assertThat(auditLogCount).isEqualTo(1); + + // when + historyMigrator.migrate(); + + // then + List logs = searchAuditLogsByCategory(AuditLogEntity.AuditLogOperationCategory.ADMIN.name()); + assertThat(logs).hasSize(1); + + assertThat(logs).extracting(AuditLogEntity::entityType).containsOnly(AuditLogEntity.AuditLogEntityType.USER); + assertThat(logs).extracting(AuditLogEntity::operationType) + .containsOnly(AuditLogEntity.AuditLogOperationType.DELETE); + } + + @Test + public void shouldMigrateAuditLogsForCreateGroup() { + // given + identityService.setAuthenticatedUserId("demo"); + var group = identityService.newGroup("newGroupId"); + group.setName("Test Group"); + identityService.saveGroup(group); + + long auditLogCount = historyService.createUserOperationLogQuery().count(); + assertThat(auditLogCount).isEqualTo(1); + + // when + historyMigrator.migrate(); + + // then + List logs = searchAuditLogsByCategory(AuditLogEntity.AuditLogOperationCategory.ADMIN.name()); + assertThat(logs).hasSize(1); + + assertThat(logs).extracting(AuditLogEntity::entityType).containsOnly(AuditLogEntity.AuditLogEntityType.GROUP); + assertThat(logs).extracting(AuditLogEntity::operationType).containsOnly(AuditLogEntity.AuditLogOperationType.CREATE); + } + + @Test + public void shouldMigrateAuditLogsForUpdateGroup() { + // given + var group = identityService.newGroup("newGroupId"); + group.setName("Test Group"); + identityService.saveGroup(group); + + // Update the group + identityService.setAuthenticatedUserId("demo"); + group.setName("Updated Group"); + identityService.saveGroup(group); + + long auditLogCount = historyService.createUserOperationLogQuery().count(); + assertThat(auditLogCount).isEqualTo(1); + + // when + historyMigrator.migrate(); + + // then + List logs = searchAuditLogsByCategory(AuditLogEntity.AuditLogOperationCategory.ADMIN.name()); + assertThat(logs).hasSize(1); + + assertThat(logs).extracting(AuditLogEntity::entityType).containsOnly(AuditLogEntity.AuditLogEntityType.GROUP); + assertThat(logs).extracting(AuditLogEntity::operationType) + .containsOnly(AuditLogEntity.AuditLogOperationType.UPDATE); + } + + @Test + public void shouldMigrateAuditLogsForDeleteGroup() { + // given + var group = identityService.newGroup("newGroupId"); + group.setName("Test Group"); + identityService.saveGroup(group); + + // Delete the group + identityService.setAuthenticatedUserId("demo"); + identityService.deleteGroup("newGroupId"); + + long auditLogCount = historyService.createUserOperationLogQuery().count(); + assertThat(auditLogCount).isEqualTo(1); + + // when + historyMigrator.migrate(); + + // then + List logs = searchAuditLogsByCategory(AuditLogEntity.AuditLogOperationCategory.ADMIN.name()); + assertThat(logs).hasSize(1); + + assertThat(logs).extracting(AuditLogEntity::entityType).containsOnly(AuditLogEntity.AuditLogEntityType.GROUP); + assertThat(logs).extracting(AuditLogEntity::operationType) + .containsOnly(AuditLogEntity.AuditLogOperationType.DELETE); + } + + @Test + public void shouldMigrateAuditLogsForCreateTenant() { + // given + identityService.setAuthenticatedUserId("demo"); + var tenant = identityService.newTenant("newTenantId"); + identityService.saveTenant(tenant); + + identityService.clearAuthentication(); + long auditLogCount = historyService.createUserOperationLogQuery() + .count(); + assertThat(auditLogCount).isEqualTo(1); + + // when + historyMigrator.migrate(); + + // then + List logs = searchAuditLogsByCategory(AuditLogEntity.AuditLogOperationCategory.ADMIN.name()); + assertThat(logs).hasSize(1); + + assertThat(logs).extracting(AuditLogEntity::entityType).containsOnly(AuditLogEntity.AuditLogEntityType.TENANT); + assertThat(logs).extracting(AuditLogEntity::operationType).containsOnly(AuditLogEntity.AuditLogOperationType.CREATE); + } + + @Test + public void shouldMigrateAuditLogsForUpdateTenant() { + // given + var tenant = identityService.newTenant("newTenantId"); + identityService.saveTenant(tenant); + + // Update the tenant + identityService.setAuthenticatedUserId("demo"); + tenant.setName("Updated Tenant"); + identityService.saveTenant(tenant); + identityService.clearAuthentication(); + + long auditLogCount = historyService.createUserOperationLogQuery().count(); + assertThat(auditLogCount).isEqualTo(1); + + // when + historyMigrator.migrate(); + + // then + List logs = searchAuditLogsByCategory(AuditLogEntity.AuditLogOperationCategory.ADMIN.name()); + assertThat(logs).hasSize(1); + + assertThat(logs).extracting(AuditLogEntity::entityType).containsOnly(AuditLogEntity.AuditLogEntityType.TENANT); + assertThat(logs).extracting(AuditLogEntity::operationType) + .containsOnly(AuditLogEntity.AuditLogOperationType.UPDATE); + } + + @Test + public void shouldMigrateAuditLogsForDeleteTenant() { + // given + var tenant = identityService.newTenant("newTenantId"); + identityService.saveTenant(tenant); + + // Delete the tenant + identityService.setAuthenticatedUserId("demo"); + identityService.deleteTenant("newTenantId"); + identityService.clearAuthentication(); + + long auditLogCount = historyService.createUserOperationLogQuery().count(); + assertThat(auditLogCount).isEqualTo(1); + + // when + historyMigrator.migrate(); + + // then + List logs = searchAuditLogsByCategory(AuditLogEntity.AuditLogOperationCategory.ADMIN.name()); + assertThat(logs).hasSize(1); + + assertThat(logs).extracting(AuditLogEntity::entityType).containsOnly(AuditLogEntity.AuditLogEntityType.TENANT); + assertThat(logs).extracting(AuditLogEntity::operationType) + .containsOnly(AuditLogEntity.AuditLogOperationType.DELETE); + } + + @Test + public void shouldMigrateAuditLogsForCreateAuthorization() { + // given + createUser(); + identityService.setAuthenticatedUserId("demo"); + Authorization authorization = authorizationService.createNewAuthorization(Authorization.AUTH_TYPE_GRANT); + authorization.setUserId("testUser"); + authorization.setResource(Resources.PROCESS_DEFINITION); + authorization.setResourceId("*"); + authorization.addPermission(Permissions.READ); + authorizationService.saveAuthorization(authorization); + + long auditLogCount = historyService.createUserOperationLogQuery() + .operationType(UserOperationLogEntry.OPERATION_TYPE_CREATE) + .count(); + assertThat(auditLogCount).isEqualTo(6); // there are 6 entries for the same log due to 6 properties + + // when + historyMigrator.migrate(); + + // then + List logs = searchAuditLogsByCategory(AuditLogEntity.AuditLogOperationCategory.ADMIN.name()); + assertThat(logs).hasSize(1); + + assertThat(logs).extracting(AuditLogEntity::entityType).containsOnly(AuditLogEntity.AuditLogEntityType.AUTHORIZATION); + assertThat(logs).extracting(AuditLogEntity::operationType) + .containsOnly(AuditLogEntity.AuditLogOperationType.CREATE); + } + + @Test + public void shouldMigrateAuditLogsForUpdateAuthorization() { + // given + createUser(); + Authorization authorization = authorizationService.createNewAuthorization(Authorization.AUTH_TYPE_GRANT); + authorization.setUserId("testUser"); + authorization.setResource(Resources.PROCESS_DEFINITION); + authorization.setResourceId("*"); + authorization.addPermission(Permissions.READ); + authorizationService.saveAuthorization(authorization); + + // Update the authorization + identityService.setAuthenticatedUserId("demo"); + authorization.addPermission(Permissions.UPDATE); + authorizationService.saveAuthorization(authorization); + + long auditLogCount = historyService.createUserOperationLogQuery().count(); + assertThat(auditLogCount).isEqualTo(6); // there are 6 entries for the same log due to 6 properties + + // when + historyMigrator.migrate(); + + // then + List logs = searchAuditLogsByCategory(AuditLogEntity.AuditLogOperationCategory.ADMIN.name()); + assertThat(logs).hasSize(1); + + assertThat(logs).extracting(AuditLogEntity::entityType).containsOnly(AuditLogEntity.AuditLogEntityType.AUTHORIZATION); + assertThat(logs).extracting(AuditLogEntity::operationType) + .containsOnly(AuditLogEntity.AuditLogOperationType.UPDATE); + } + + @Test + public void shouldMigrateAuditLogsForDeleteAuthorization() { + // given + createUser(); + Authorization authorization = authorizationService.createNewAuthorization(Authorization.AUTH_TYPE_GRANT); + authorization.setUserId("testUser"); + authorization.setResource(Resources.PROCESS_DEFINITION); + authorization.setResourceId("*"); + authorization.addPermission(Permissions.READ); + authorizationService.saveAuthorization(authorization); + + // Delete the authorization + identityService.setAuthenticatedUserId("demo"); + authorizationService.deleteAuthorization(authorization.getId()); + identityService.clearAuthentication(); + + long auditLogCount = historyService.createUserOperationLogQuery().count(); + assertThat(auditLogCount).isEqualTo(6); // there are 6 entries for the same log due to 6 properties + + // when + historyMigrator.migrate(); + + // then + List logs = searchAuditLogsByCategory(AuditLogEntity.AuditLogOperationCategory.ADMIN.name()); + assertThat(logs).hasSize(1); + + assertThat(logs).extracting(AuditLogEntity::entityType).containsOnly(AuditLogEntity.AuditLogEntityType.AUTHORIZATION); + assertThat(logs).extracting(AuditLogEntity::operationType) + .containsOnly(AuditLogEntity.AuditLogOperationType.DELETE); + } + + protected void createUser() { + User testUser = identityService.newUser("testUser"); + identityService.saveUser(testUser); + } + +} diff --git a/data-migrator/qa/integration-tests/src/test/java/io/camunda/migration/data/qa/history/entity/HistoryAuditLogTest.java b/data-migrator/qa/integration-tests/src/test/java/io/camunda/migration/data/qa/history/entity/HistoryAuditLogTest.java new file mode 100644 index 000000000..d6b6d294d --- /dev/null +++ b/data-migrator/qa/integration-tests/src/test/java/io/camunda/migration/data/qa/history/entity/HistoryAuditLogTest.java @@ -0,0 +1,520 @@ +/* + * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH under + * one or more contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright ownership. + * Licensed under the Camunda License 1.0. You may not use this file + * except in compliance with the Camunda License 1.0. + */ +package io.camunda.migration.data.qa.history.entity; + +import static io.camunda.migration.data.constants.MigratorConstants.C8_DEFAULT_TENANT; +import static io.camunda.migration.data.impl.util.ConverterUtil.prefixDefinitionId; +import static org.assertj.core.api.Assertions.assertThat; +import static org.camunda.bpm.engine.variable.Variables.createVariables; + +import io.camunda.migration.data.qa.history.HistoryMigrationAbstractTest; +import io.camunda.search.entities.AuditLogEntity; +import io.camunda.search.entities.ProcessInstanceEntity; +import java.util.List; +import org.camunda.bpm.engine.IdentityService; +import org.camunda.bpm.engine.history.UserOperationLogEntry; +import org.camunda.bpm.engine.repository.Deployment; +import org.camunda.bpm.engine.runtime.Incident; +import org.camunda.bpm.engine.runtime.ProcessInstance; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Integration test for audit log migration from Camunda 7 to Camunda 8. + *

+ * Tests the migration of user operation log entries (audit logs) that track + * user actions and operations performed on process instances. + *

+ */ +public class HistoryAuditLogTest extends HistoryMigrationAbstractTest { + + @Autowired + protected IdentityService identityService; + + @AfterEach + public void cleanupData() { + identityService.clearAuthentication(); + historyService.createUserOperationLogQuery().list().forEach(log -> + historyService.deleteUserOperationLogEntry(log.getId())); + } + + @Test + public void shouldMigrateAuditLogs() { + // given + deployer.deployCamunda7Process("simpleProcess.bpmn"); + identityService.setAuthenticatedUserId("demo"); + ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("simpleProcess"); + + // Verify audit logs exist in C7 + UserOperationLogEntry userOperationLogEntry = historyService.createUserOperationLogQuery() + .processInstanceId(processInstance.getId()) + .operationType(UserOperationLogEntry.OPERATION_TYPE_CREATE) + .singleResult(); + assertThat(userOperationLogEntry).isNotNull(); + String annotation = "anAnnotation"; + historyService.setAnnotationForOperationLogById(userOperationLogEntry.getOperationId(), annotation); + + // when + historyMigrator.migrate(); + + // then + List c8ProcessInstance = searchHistoricProcessInstances("simpleProcess"); + assertThat(c8ProcessInstance).hasSize(1); + List logs = searchAuditLogs("simpleProcess"); + assertThat(logs).hasSize(1); + AuditLogEntity log = logs.getFirst(); + + assertThat(log.auditLogKey()).isNotNull(); + assertThat(log.processInstanceKey()).isEqualTo(c8ProcessInstance.getFirst().processInstanceKey()); + assertThat(log.rootProcessInstanceKey()).isEqualTo(c8ProcessInstance.getFirst().processInstanceKey()); + assertThat(log.processDefinitionKey()).isNotNull(); + assertThat(log.userTaskKey()).isNull(); + + assertThat(log.timestamp()).isNotNull(); + assertThat(log.actorId()).isEqualTo("demo"); + assertThat(log.actorType()).isEqualTo(AuditLogEntity.AuditLogActorType.USER); + assertThat(log.processDefinitionId()).isEqualTo(prefixDefinitionId("simpleProcess")); + assertThat(log.annotation()).isEqualTo(annotation); + assertThat(log.tenantId()).isEqualTo(C8_DEFAULT_TENANT); + assertThat(log.tenantScope()).isEqualTo(AuditLogEntity.AuditLogTenantScope.GLOBAL); + assertThat(log.result()).isEqualTo(AuditLogEntity.AuditLogOperationResult.SUCCESS); + } + + @Test + public void shouldMigrateAuditLogsWithTenant() { + // given + deployer.deployCamunda7Process("simpleProcess.bpmn", "tenantA"); + identityService.setAuthentication("demo", null, List.of("tenantA")); + ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("simpleProcess"); + + // Verify audit logs exist in C7 + long auditLogCount = historyService.createUserOperationLogQuery() + .processInstanceId(processInstance.getId()) + .operationType(UserOperationLogEntry.OPERATION_TYPE_CREATE) + .count(); + assertThat(auditLogCount).isEqualTo(1); + + // when + historyMigrator.migrate(); + + // then + List c8ProcessInstance = searchHistoricProcessInstances("simpleProcess"); + assertThat(c8ProcessInstance).hasSize(1); + List logs = searchAuditLogs("simpleProcess"); + assertThat(logs).hasSize(1); + AuditLogEntity log = logs.getFirst(); + + assertThat(log.tenantId()).isEqualTo("tenantA"); + assertThat(log.tenantScope()).isEqualTo(AuditLogEntity.AuditLogTenantScope.TENANT); + } + + @Test + public void shouldMigrateAuditLogsForSetVariable() { + // given + deployer.deployCamunda7Process("simpleProcess.bpmn"); + ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("simpleProcess"); + + // Set variables to generate audit logs + identityService.setAuthenticatedUserId("demo"); + runtimeService.setVariable(processInstance.getId(), "testVar1", "value1"); + runtimeService.setVariable(processInstance.getId(), "testVar2", 123); + + // Verify audit logs exist in C7 + long auditLogCount = historyService.createUserOperationLogQuery() + .processInstanceId(processInstance.getId()) + .operationType(UserOperationLogEntry.OPERATION_TYPE_SET_VARIABLE) + .count(); + assertThat(auditLogCount).isEqualTo(2); + + // when + historyMigrator.migrate(); + + // then + List c8ProcessInstance = searchHistoricProcessInstances("simpleProcess"); + assertThat(c8ProcessInstance).hasSize(1); + List logs = searchAuditLogs("simpleProcess"); + assertThat(logs).hasSize(2); + assertThat(logs).extracting(AuditLogEntity::category).contains(AuditLogEntity.AuditLogOperationCategory.DEPLOYED_RESOURCES); + assertThat(logs).extracting(AuditLogEntity::entityType).containsOnly(AuditLogEntity.AuditLogEntityType.VARIABLE); + assertThat(logs).extracting(AuditLogEntity::operationType).containsOnly(AuditLogEntity.AuditLogOperationType.UPDATE); + } + + @Test + public void shouldMigrateAuditLogsForCreateProcessInstance() { + // given + deployer.deployCamunda7Process("simpleProcess.bpmn"); + identityService.setAuthenticatedUserId("demo"); + ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("simpleProcess"); + + // Verify audit logs exist in C7 + long auditLogCount = historyService.createUserOperationLogQuery() + .processInstanceId(processInstance.getId()) + .operationType(UserOperationLogEntry.OPERATION_TYPE_CREATE) + .count(); + assertThat(auditLogCount).isEqualTo(1); + + // when + historyMigrator.migrate(); + + // then + List c8ProcessInstance = searchHistoricProcessInstances("simpleProcess"); + assertThat(c8ProcessInstance).hasSize(1); + List logs = searchAuditLogs("simpleProcess"); + assertThat(logs).hasSize(1); + assertThat(logs).extracting(AuditLogEntity::category).contains(AuditLogEntity.AuditLogOperationCategory.DEPLOYED_RESOURCES); + assertThat(logs).extracting(AuditLogEntity::operationType).contains(AuditLogEntity.AuditLogOperationType.CREATE); + } + + @Test + public void shouldMigrateAuditLogsForModifyProcessInstance() { + // given + deployer.deployCamunda7Process("userTaskProcess.bpmn"); + ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("userTaskProcessId"); + + // Modify process instance to generate audit logs + identityService.setAuthenticatedUserId("demo"); + runtimeService.createProcessInstanceModification(processInstance.getId()) + .cancelAllForActivity("userTask") + .execute(); + + // Verify audit logs exist in C7 + long auditLogCount = historyService.createUserOperationLogQuery() + .processInstanceId(processInstance.getId()) + .operationType(UserOperationLogEntry.OPERATION_TYPE_MODIFY_PROCESS_INSTANCE) + .count(); + assertThat(auditLogCount).isEqualTo(1); + + // when + historyMigrator.migrate(); + + // then + List c8ProcessInstance = searchHistoricProcessInstances("userTaskProcessId"); + assertThat(c8ProcessInstance).hasSize(1); + List logs = searchAuditLogs("userTaskProcessId"); + assertThat(logs).hasSize(1); + assertThat(logs).extracting(AuditLogEntity::category).contains(AuditLogEntity.AuditLogOperationCategory.DEPLOYED_RESOURCES); + assertThat(logs).extracting(AuditLogEntity::operationType).contains(AuditLogEntity.AuditLogOperationType.MODIFY); + } + + @Test + public void shouldMigrateAuditLogsForMigrateProcessInstance() { + // given + deployer.deployCamunda7Process("simpleProcess.bpmn"); + ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("simpleProcess"); + + // Deploy a new version of the process + deployer.deployCamunda7Process("simpleProcess.bpmn"); + var targetProcessDefinition = repositoryService.createProcessDefinitionQuery() + .processDefinitionKey("simpleProcess") + .latestVersion() + .singleResult(); + + // Migrate process instance to generate audit logs + identityService.setAuthenticatedUserId("demo"); + var migrationPlan = runtimeService.createMigrationPlan(processInstance.getProcessDefinitionId(), targetProcessDefinition.getId()) + .mapEqualActivities() + .build(); + runtimeService.newMigration(migrationPlan) + .processInstanceIds(processInstance.getId()) + .execute(); + + // Verify audit logs exist in C7 + long auditLogCount = historyService.createUserOperationLogQuery() + .operationType(UserOperationLogEntry.OPERATION_TYPE_MIGRATE) + .count(); + assertThat(auditLogCount).isEqualTo(3); // 3 logs with same op log id for the 3 properties + + // when + historyMigrator.migrate(); + + // then + List c8ProcessInstance = searchHistoricProcessInstances("simpleProcess"); + assertThat(c8ProcessInstance).hasSize(1); + List logs = searchAuditLogs("simpleProcess"); + assertThat(logs).hasSize(1); + assertThat(logs).extracting(AuditLogEntity::category).contains(AuditLogEntity.AuditLogOperationCategory.DEPLOYED_RESOURCES); + assertThat(logs).extracting(AuditLogEntity::operationType).contains(AuditLogEntity.AuditLogOperationType.MIGRATE); + } + + @Test + public void shouldMigrateAuditLogsForDeleteProcessDefinition() { + // given + deployer.deployCamunda7Process("simpleProcess.bpmn"); + var processDefinition = repositoryService.createProcessDefinitionQuery() + .processDefinitionKey("simpleProcess") + .singleResult(); + historyMigrator.migrate(); // Migrate before deletion to ensure process definition is available in C8 + + identityService.setAuthenticatedUserId("demo"); + repositoryService.deleteProcessDefinition(processDefinition.getId()); + + // Verify audit logs exist in C7 + long auditLogCount = historyService.createUserOperationLogQuery() + .operationType(UserOperationLogEntry.OPERATION_TYPE_DELETE) + .count(); + assertThat(auditLogCount).isEqualTo(1); + + // when + historyMigrator.migrate(); + + // then + List logs = searchAuditLogsByCategory(AuditLogEntity.AuditLogOperationCategory.DEPLOYED_RESOURCES.name()); + assertThat(logs).hasSize(1); + assertThat(logs).extracting(AuditLogEntity::operationType).contains(AuditLogEntity.AuditLogOperationType.DELETE); + } + + @Test + public void shouldMigrateAuditLogsForEvaluateDecision() { + // given + deployer.deployCamunda7Decision("dish-decision.dmn"); + + identityService.setAuthenticatedUserId("demo"); + decisionService.evaluateDecisionByKey("Dish") + .variables(createVariables() + .putValue("season", "Winter") + .putValue("guestCount", 5)) + .evaluate(); + + // Verify audit logs exist in C7 + long auditLogCount = historyService.createUserOperationLogQuery() + .operationType(UserOperationLogEntry.OPERATION_TYPE_EVALUATE) + .count(); + assertThat(auditLogCount).isEqualTo(2); // two properties with same op log id + + // when + historyMigrator.migrate(); + + // then + List logs = searchAuditLogsByCategory(AuditLogEntity.AuditLogOperationCategory.DEPLOYED_RESOURCES.name()); + assertThat(logs).hasSize(1); + assertThat(logs).extracting(AuditLogEntity::entityType).contains(AuditLogEntity.AuditLogEntityType.DECISION); + assertThat(logs).extracting(AuditLogEntity::operationType).contains(AuditLogEntity.AuditLogOperationType.EVALUATE); + } + + @Test + public void shouldMigrateAuditLogsForDeleteDecisionHistory() { + // given + deployer.deployCamunda7Decision("dish-decision.dmn"); + + identityService.setAuthenticatedUserId("demo"); + decisionService.evaluateDecisionByKey("Dish") + .variables(createVariables() + .putValue("season", "Winter") + .putValue("guestCount", 5)) + .evaluate(); + + // Get the historic decision instance + var historicDecisionInstance = historyService.createHistoricDecisionInstanceQuery() + .decisionDefinitionKey("Dish") + .singleResult(); + + identityService.setAuthenticatedUserId("demo"); + historyService.deleteHistoricDecisionInstanceByInstanceId(historicDecisionInstance.getId()); + + // Verify audit logs exist in C7 + long auditLogCount = historyService.createUserOperationLogQuery() + .operationType(UserOperationLogEntry.OPERATION_TYPE_DELETE_HISTORY) + .count(); + assertThat(auditLogCount).isEqualTo(2); + + // when + historyMigrator.migrate(); + + // then + List logs = searchAuditLogsByCategory(AuditLogEntity.AuditLogOperationCategory.DEPLOYED_RESOURCES.name()); + assertThat(logs).hasSize(2); + assertThat(logs).extracting(AuditLogEntity::operationType).contains(AuditLogEntity.AuditLogOperationType.DELETE); + } + + @Test + public void shouldMigrateAuditLogsForCreateDeployment() { + // given + identityService.setAuthenticatedUserId("demo"); + deployer.deployCamunda7Process("simpleProcess.bpmn"); + + // Verify audit logs exist in C7 + long auditLogCount = historyService.createUserOperationLogQuery() + .operationType(UserOperationLogEntry.OPERATION_TYPE_CREATE) + .count(); + assertThat(auditLogCount).isEqualTo(1); + + // when + historyMigrator.migrate(); + + // then + List logs = searchAuditLogsByCategory(AuditLogEntity.AuditLogOperationCategory.DEPLOYED_RESOURCES.name()); + assertThat(logs).hasSize(1); + assertThat(logs).extracting(AuditLogEntity::entityType).contains(AuditLogEntity.AuditLogEntityType.RESOURCE); + assertThat(logs).extracting(AuditLogEntity::operationType).contains(AuditLogEntity.AuditLogOperationType.CREATE); + } + + @Test + public void shouldMigrateAuditLogsForDeleteDeployment() { + // given + deployer.deployCamunda7Process("simpleProcess.bpmn"); + Deployment deployment = repositoryService.createDeploymentQuery() + .singleResult(); + + identityService.setAuthenticatedUserId("demo"); + repositoryService.deleteDeployment(deployment.getId(), true); + + // Verify audit logs exist in C7 + long auditLogCount = historyService.createUserOperationLogQuery() + .operationType(UserOperationLogEntry.OPERATION_TYPE_DELETE) + .count(); + assertThat(auditLogCount).isEqualTo(1); + + // when + historyMigrator.migrate(); + + // then + List logs = searchAuditLogsByCategory(AuditLogEntity.AuditLogOperationCategory.DEPLOYED_RESOURCES.name()); + assertThat(logs).hasSize(1); + assertThat(logs).extracting(AuditLogEntity::operationType).contains(AuditLogEntity.AuditLogOperationType.DELETE); + } + + @Test + public void shouldMigrateAuditLogsForRemoveVariable() { + // given + deployer.deployCamunda7Process("simpleProcess.bpmn"); + ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("simpleProcess"); + runtimeService.setVariable(processInstance.getId(), "testVar", "value"); + + // Remove variable to generate audit logs + identityService.setAuthenticatedUserId("demo"); + runtimeService.removeVariable(processInstance.getId(), "testVar"); + + // Verify audit logs exist in C7 + long auditLogCount = historyService.createUserOperationLogQuery() + .processInstanceId(processInstance.getId()) + .operationType(UserOperationLogEntry.OPERATION_TYPE_REMOVE_VARIABLE) + .count(); + assertThat(auditLogCount).isEqualTo(1); + + // when + historyMigrator.migrate(); + + // then + List c8ProcessInstance = searchHistoricProcessInstances("simpleProcess"); + assertThat(c8ProcessInstance).hasSize(1); + List logs = searchAuditLogs("simpleProcess"); + assertThat(logs).hasSize(1); + assertThat(logs).extracting(AuditLogEntity::entityType).contains(AuditLogEntity.AuditLogEntityType.VARIABLE); + assertThat(logs).extracting(AuditLogEntity::operationType).contains(AuditLogEntity.AuditLogOperationType.DELETE); + } + + @Test + public void shouldMigrateAuditLogsForDeleteVariableHistory() { + // given + deployer.deployCamunda7Process("simpleProcess.bpmn"); + ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("simpleProcess"); + runtimeService.setVariable(processInstance.getId(), "testVar", "value"); + + // Delete variable history to generate audit logs + identityService.setAuthenticatedUserId("demo"); + historyService.deleteHistoricVariableInstance( + historyService.createHistoricVariableInstanceQuery() + .processInstanceId(processInstance.getId()) + .variableName("testVar") + .singleResult() + .getId()); + + // Verify audit logs exist in C7 + long auditLogCount = historyService.createUserOperationLogQuery() + .processInstanceId(processInstance.getId()) + .operationType(UserOperationLogEntry.OPERATION_TYPE_DELETE_HISTORY) + .count(); + assertThat(auditLogCount).isEqualTo(1); + + // when + historyMigrator.migrate(); + + // then + List c8ProcessInstance = searchHistoricProcessInstances("simpleProcess"); + assertThat(c8ProcessInstance).hasSize(1); + List logs = searchAuditLogs("simpleProcess"); + assertThat(logs).hasSize(1); + assertThat(logs).extracting(AuditLogEntity::entityType).contains(AuditLogEntity.AuditLogEntityType.VARIABLE); + assertThat(logs).extracting(AuditLogEntity::operationType).contains(AuditLogEntity.AuditLogOperationType.DELETE); + } + + @Test + public void shouldMigrateAuditLogsForIncidentResolution() { + // given + deployer.deployCamunda7Process("simpleProcess.bpmn"); + ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("simpleProcess"); + Incident incident = runtimeService.createIncident("foo", processInstance.getId(), "userTask1", "bar"); + identityService.setAuthenticatedUserId("demo"); + runtimeService.resolveIncident(incident.getId()); + assertThat(historyService.createUserOperationLogQuery().count()).isEqualTo(1L); + + // when + historyMigrator.migrate(); + + // then + List c8ProcessInstance = searchHistoricProcessInstances("simpleProcess"); + assertThat(c8ProcessInstance).hasSize(1); + List logs = searchAuditLogsByCategory(AuditLogEntity.AuditLogOperationCategory.DEPLOYED_RESOURCES.name()); + assertThat(logs).hasSize(1); + assertThat(logs).extracting(AuditLogEntity::entityType).contains(AuditLogEntity.AuditLogEntityType.INCIDENT); + assertThat(logs).extracting(AuditLogEntity::operationType).contains(AuditLogEntity.AuditLogOperationType.RESOLVE); + } + + @Test + public void shouldSkipAuditLogsForActivateSuspend() { + // given + deployer.deployCamunda7Process("simpleProcess.bpmn"); + ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("simpleProcess"); + + identityService.setAuthenticatedUserId("demo"); + // Perform operations that generate audit log entries + runtimeService.suspendProcessInstanceById(processInstance.getId()); + runtimeService.activateProcessInstanceById(processInstance.getId()); + + // Verify audit logs exist in C7 + long auditLogCount = historyService.createUserOperationLogQuery() + .processInstanceId(processInstance.getId()) + .count(); + assertThat(auditLogCount).isEqualTo(2); + + // when + historyMigrator.migrate(); + + // then + // Verify process instance was migrated + List c8ProcessInstance = searchHistoricProcessInstances("simpleProcess"); + assertThat(c8ProcessInstance).hasSize(1); + List logs = searchAuditLogs("simpleProcess"); + assertThat(logs).hasSize(0); + } + + @Test + public void shouldSkipMigrationOfAuditLogsWithoutProcessInstance() { + // given + deployer.deployCamunda7Process("simpleProcess.bpmn"); + ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("simpleProcess"); + + // Perform operation + identityService.setAuthenticatedUserId("demo"); + runtimeService.suspendProcessInstanceById(processInstance.getId()); + + // Get audit log entries before migration + long auditLogCountBefore = historyService.createUserOperationLogQuery() + .processInstanceId(processInstance.getId()) + .count(); + assertThat(auditLogCountBefore).isEqualTo(1); + + // when + historyMigrator.migrateAuditLogs(); + + // then + List logs = searchAuditLogsByCategory(AuditLogEntity.AuditLogOperationCategory.DEPLOYED_RESOURCES.name()); + assertThat(logs).hasSize(0); + } +} diff --git a/data-migrator/qa/integration-tests/src/test/java/io/camunda/migration/data/qa/history/entity/HistoryAuditLogUserTaskTest.java b/data-migrator/qa/integration-tests/src/test/java/io/camunda/migration/data/qa/history/entity/HistoryAuditLogUserTaskTest.java new file mode 100644 index 000000000..543334a61 --- /dev/null +++ b/data-migrator/qa/integration-tests/src/test/java/io/camunda/migration/data/qa/history/entity/HistoryAuditLogUserTaskTest.java @@ -0,0 +1,415 @@ +/* + * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH under + * one or more contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright ownership. + * Licensed under the Camunda License 1.0. You may not use this file + * except in compliance with the Camunda License 1.0. + */ +package io.camunda.migration.data.qa.history.entity; + +import static io.camunda.migration.data.constants.MigratorConstants.C8_DEFAULT_TENANT; +import static io.camunda.migration.data.impl.util.ConverterUtil.prefixDefinitionId; +import static org.assertj.core.api.Assertions.assertThat; + +import io.camunda.migration.data.qa.history.HistoryMigrationAbstractTest; +import io.camunda.search.entities.AuditLogEntity; +import io.camunda.search.entities.ProcessInstanceEntity; +import io.camunda.search.entities.UserTaskEntity; +import java.util.List; +import org.camunda.bpm.engine.IdentityService; +import org.camunda.bpm.engine.history.UserOperationLogEntry; +import org.camunda.bpm.engine.runtime.ProcessInstance; +import org.camunda.bpm.engine.task.Task; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Integration test for audit log migration from Camunda 7 to Camunda 8. + *

+ * Tests the migration of user operation log entries (audit logs) that track + * user actions and operations performed on process instances. + *

+ */ +public class HistoryAuditLogUserTaskTest extends HistoryMigrationAbstractTest { + + @Autowired + protected IdentityService identityService; + + @AfterEach + public void cleanupData() { + identityService.clearAuthentication(); + historyService.createUserOperationLogQuery().list().forEach(log -> + historyService.deleteUserOperationLogEntry(log.getId())); + } + + @Test + public void shouldMigrateAuditLogsForTask() { + // given + deployer.deployCamunda7Process("userTaskProcess.bpmn"); + ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("userTaskProcessId"); + + // Complete a user task to generate audit logs + identityService.setAuthenticatedUserId("demo"); + completeAllUserTasksWithDefaultUserTaskId(); + + // Verify audit logs exist in C7 + long auditLogCount = historyService.createUserOperationLogQuery() + .processInstanceId(processInstance.getId()) + .count(); + assertThat(auditLogCount).isEqualTo(1); + String annotation = "anAnnotation"; + historyService.setAnnotationForOperationLogById(historyService.createUserOperationLogQuery().singleResult().getOperationId(), annotation); + + // when + historyMigrator.migrate(); + + // then + List c8ProcessInstance = searchHistoricProcessInstances("userTaskProcessId"); + assertThat(c8ProcessInstance).hasSize(1); + List userTaskEntities = searchHistoricUserTasks(c8ProcessInstance.getFirst().processInstanceKey()); + assertThat(userTaskEntities).hasSize(1); + List logs = searchAuditLogs("userTaskProcessId"); + assertThat(logs).hasSize(1); + AuditLogEntity log = logs.getFirst(); + + assertThat(log.auditLogKey()).isNotNull(); + assertThat(log.processInstanceKey()).isEqualTo(c8ProcessInstance.getFirst().processInstanceKey()); + assertThat(log.rootProcessInstanceKey()).isEqualTo(c8ProcessInstance.getFirst().processInstanceKey()); + assertThat(log.processDefinitionKey()).isNotNull(); + assertThat(log.userTaskKey()).isNotNull(); + assertThat(log.elementInstanceKey()).isEqualTo(userTaskEntities.getFirst().elementInstanceKey()); + assertThat(log.timestamp()).isNotNull(); + assertThat(log.actorId()).isEqualTo("demo"); + assertThat(log.actorType()).isEqualTo(AuditLogEntity.AuditLogActorType.USER); + assertThat(log.processDefinitionId()).isEqualTo(prefixDefinitionId("userTaskProcessId")); + assertThat(log.annotation()).isEqualTo(annotation); + assertThat(log.tenantId()).isEqualTo(C8_DEFAULT_TENANT); + assertThat(log.tenantScope()).isEqualTo(AuditLogEntity.AuditLogTenantScope.GLOBAL); + assertThat(log.result()).isEqualTo(AuditLogEntity.AuditLogOperationResult.SUCCESS); + } + + @Test + public void shouldMigrateAuditLogsForTaskWithTenant() { + // given + deployer.deployCamunda7Process("userTaskProcess.bpmn", "tenantA"); + ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("userTaskProcessId"); + + // Complete a user task to generate audit logs + identityService.setAuthentication("demo", null, List.of("tenantA")); + completeAllUserTasksWithDefaultUserTaskId(); + + // Verify audit logs exist in C7 + long auditLogCount = historyService.createUserOperationLogQuery() + .processInstanceId(processInstance.getId()) + .count(); + assertThat(auditLogCount).isEqualTo(1); + + // when + historyMigrator.migrate(); + + // then + List c8ProcessInstance = searchHistoricProcessInstances("userTaskProcessId"); + assertThat(c8ProcessInstance).hasSize(1); + List logs = searchAuditLogs("userTaskProcessId"); + assertThat(logs).hasSize(1); + AuditLogEntity log = logs.getFirst(); + + assertThat(log.tenantId()).isEqualTo("tenantA"); + assertThat(log.tenantScope()).isEqualTo(AuditLogEntity.AuditLogTenantScope.TENANT); + } + + @Test + public void shouldMigrateAuditLogsForCompleteTask() { + // given + deployer.deployCamunda7Process("userTaskProcess.bpmn"); + ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("userTaskProcessId"); + + // Complete a user task to generate audit logs + identityService.setAuthenticatedUserId("demo"); + completeAllUserTasksWithDefaultUserTaskId(); + + // Verify audit logs exist in C7 + long auditLogCount = historyService.createUserOperationLogQuery() + .processInstanceId(processInstance.getId()) + .count(); + assertThat(auditLogCount).isEqualTo(1); + + // when + historyMigrator.migrate(); + + // then + List c8ProcessInstance = searchHistoricProcessInstances("userTaskProcessId"); + assertThat(c8ProcessInstance).hasSize(1); + List logs = searchAuditLogs("userTaskProcessId"); + assertThat(logs).hasSize(1); + assertAuditLogProperties(logs, AuditLogEntity.AuditLogOperationType.COMPLETE); + } + + @Test + public void shouldMigrateAuditLogsForAssignTask() { + // given + deployer.deployCamunda7Process("userTaskProcess.bpmn"); + ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("userTaskProcessId"); + + // Assign a user task to generate audit logs + identityService.setAuthenticatedUserId("demo"); + var task = taskService.createTaskQuery().processInstanceId(processInstance.getId()).singleResult(); + taskService.setAssignee(task.getId(), "assignedUser"); + + // Verify audit logs exist in C7 + long auditLogCount = historyService.createUserOperationLogQuery() + .processInstanceId(processInstance.getId()) + .operationType(UserOperationLogEntry.OPERATION_TYPE_ASSIGN) + .count(); + assertThat(auditLogCount).isEqualTo(1); + + // when + historyMigrator.migrate(); + + // then + List c8ProcessInstance = searchHistoricProcessInstances("userTaskProcessId"); + assertThat(c8ProcessInstance).hasSize(1); + List logs = searchAuditLogs("userTaskProcessId"); + assertThat(logs).hasSize(1); + assertAuditLogProperties(logs, AuditLogEntity.AuditLogOperationType.ASSIGN); + } + + @Test + public void shouldMigrateAuditLogsForClaimTask() { + // given + deployer.deployCamunda7Process("userTaskProcess.bpmn"); + ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("userTaskProcessId"); + + // Claim a user task to generate audit logs + identityService.setAuthenticatedUserId("demo"); + var task = taskService.createTaskQuery().processInstanceId(processInstance.getId()).singleResult(); + taskService.claim(task.getId(), "claimingUser"); + + // Verify audit logs exist in C7 + long auditLogCount = historyService.createUserOperationLogQuery() + .processInstanceId(processInstance.getId()) + .operationType(UserOperationLogEntry.OPERATION_TYPE_CLAIM) + .count(); + assertThat(auditLogCount).isEqualTo(1); // there are two UPDATE logs: for each property + + // when + historyMigrator.migrate(); + + // then + List c8ProcessInstance = searchHistoricProcessInstances("userTaskProcessId"); + assertThat(c8ProcessInstance).hasSize(1); + List logs = searchAuditLogs("userTaskProcessId"); + assertThat(logs).hasSize(1); + assertAuditLogProperties(logs, AuditLogEntity.AuditLogOperationType.ASSIGN); + } + + @Test + public void shouldMigrateAuditLogsForDelegateTask() { + // given + deployer.deployCamunda7Process("userTaskProcess.bpmn"); + ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("userTaskProcessId"); + + // Delegate a user task to generate audit logs + identityService.setAuthenticatedUserId("demo"); + var task = taskService.createTaskQuery().processInstanceId(processInstance.getId()).singleResult(); + taskService.delegateTask(task.getId(), "delegatedUser"); + + // Verify audit logs exist in C7 + long auditLogCount = historyService.createUserOperationLogQuery() + .processInstanceId(processInstance.getId()) + .operationType(UserOperationLogEntry.OPERATION_TYPE_DELEGATE) + .count(); + assertThat(auditLogCount).isEqualTo(2); + + // when + historyMigrator.migrate(); + + // then + List c8ProcessInstance = searchHistoricProcessInstances("userTaskProcessId"); + assertThat(c8ProcessInstance).hasSize(1); + List logs = searchAuditLogs("userTaskProcessId"); + assertThat(logs.size()).isEqualTo(1); + assertAuditLogProperties(logs, AuditLogEntity.AuditLogOperationType.ASSIGN); + } + + @Test + @Disabled + public void shouldMigrateAuditLogsForDeleteTask() { + // given + Task task = taskService.newTask(); + taskService.saveTask(task); + + // Delete a user task to generate audit logs + identityService.setAuthenticatedUserId("demo"); + taskService.deleteTask(task.getId()); + + // Verify audit logs exist in C7 + long auditLogCount = historyService.createUserOperationLogQuery() + .operationType("Delete") + .count(); + assertThat(auditLogCount).isEqualTo(1); + + // when + historyMigrator.migrate(); + + // then + List logs = searchAuditLogsByCategory(AuditLogEntity.AuditLogOperationCategory.USER_TASKS.name()); + assertThat(logs).hasSize(1); // result is 0 since task is not linked to a process instance and can't be migrated + assertAuditLogProperties(logs, AuditLogEntity.AuditLogOperationType.DELETE); + } + + @Test + public void shouldMigrateAuditLogsForResolveTask() { + // given + deployer.deployCamunda7Process("userTaskProcess.bpmn"); + ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("userTaskProcessId"); + + // Resolve task to generate audit logs + var task = taskService.createTaskQuery().processInstanceId(processInstance.getId()).singleResult(); + identityService.setAuthenticatedUserId("demo"); + taskService.resolveTask(task.getId()); + + // Verify audit logs exist in C7 + long auditLogCount = historyService.createUserOperationLogQuery() + .processInstanceId(processInstance.getId()) + .operationType(UserOperationLogEntry.OPERATION_TYPE_RESOLVE) + .count(); + assertThat(auditLogCount).isEqualTo(1); + + // when + historyMigrator.migrate(); + + // then + List c8ProcessInstance = searchHistoricProcessInstances("userTaskProcessId"); + assertThat(c8ProcessInstance).hasSize(1); + List logs = searchAuditLogs("userTaskProcessId"); + assertThat(logs.stream().filter(log -> log.operationType().equals(AuditLogEntity.AuditLogOperationType.UPDATE)).count()).isEqualTo(1); + assertThat(logs).extracting(AuditLogEntity::entityType).containsOnly(AuditLogEntity.AuditLogEntityType.USER_TASK); + assertThat(logs).extracting(AuditLogEntity::category).containsOnly(AuditLogEntity.AuditLogOperationCategory.USER_TASKS); + } + + @Test + public void shouldMigrateAuditLogsForSetOwnerTask() { + // given + deployer.deployCamunda7Process("userTaskProcess.bpmn"); + ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("userTaskProcessId"); + + // Set owner on a user task to generate audit logs + identityService.setAuthenticatedUserId("demo"); + var task = taskService.createTaskQuery().processInstanceId(processInstance.getId()).singleResult(); + taskService.setOwner(task.getId(), "taskOwner"); + + // Verify audit logs exist in C7 + long auditLogCount = historyService.createUserOperationLogQuery() + .processInstanceId(processInstance.getId()) + .operationType(UserOperationLogEntry.OPERATION_TYPE_SET_OWNER) + .count(); + assertThat(auditLogCount).isEqualTo(1); + + // when + historyMigrator.migrate(); + + // then + List c8ProcessInstance = searchHistoricProcessInstances("userTaskProcessId"); + assertThat(c8ProcessInstance).hasSize(1); + List logs = searchAuditLogs("userTaskProcessId"); + assertThat(logs).hasSize(1); + assertAuditLogProperties(logs, AuditLogEntity.AuditLogOperationType.UPDATE); + } + + @Test + public void shouldMigrateAuditLogsForSetPriorityTask() { + // given + deployer.deployCamunda7Process("userTaskProcess.bpmn"); + ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("userTaskProcessId"); + + // Set priority on a user task to generate audit logs + identityService.setAuthenticatedUserId("demo"); + var task = taskService.createTaskQuery().processInstanceId(processInstance.getId()).singleResult(); + taskService.setPriority(task.getId(), 100); + + // Verify audit logs exist in C7 + long auditLogCount = historyService.createUserOperationLogQuery() + .processInstanceId(processInstance.getId()) + .operationType(UserOperationLogEntry.OPERATION_TYPE_SET_PRIORITY) + .count(); + assertThat(auditLogCount).isEqualTo(1); + + // when + historyMigrator.migrate(); + + // then + List c8ProcessInstance = searchHistoricProcessInstances("userTaskProcessId"); + assertThat(c8ProcessInstance).hasSize(1); + List logs = searchAuditLogs("userTaskProcessId"); + assertThat(logs).hasSize(1); + assertAuditLogProperties(logs, AuditLogEntity.AuditLogOperationType.UPDATE); + } + + @Test + public void shouldMigrateAuditLogsForUpdateTask() { + // given + deployer.deployCamunda7Process("userTaskProcess.bpmn"); + ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("userTaskProcessId"); + + // Update a user task property to generate audit logs + identityService.setAuthenticatedUserId("demo"); + var task = taskService.createTaskQuery().processInstanceId(processInstance.getId()).singleResult(); + task.setDescription("Updated description"); + taskService.saveTask(task); + + // Verify audit logs exist in C7 + long auditLogCount = historyService.createUserOperationLogQuery() + .processInstanceId(processInstance.getId()) + .operationType(UserOperationLogEntry.OPERATION_TYPE_UPDATE) + .count(); + assertThat(auditLogCount).isEqualTo(1); + + // when + historyMigrator.migrate(); + + // then + List c8ProcessInstance = searchHistoricProcessInstances("userTaskProcessId"); + assertThat(c8ProcessInstance).hasSize(1); + List logs = searchAuditLogs("userTaskProcessId"); + assertThat(logs).hasSize(1); + assertAuditLogProperties(logs, AuditLogEntity.AuditLogOperationType.UPDATE); + } + + @Test + public void shouldSkipAuditLogsWhenUserTasksAreNotMigrated() { + // given + deployer.deployCamunda7Process("userTaskProcess.bpmn"); + ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("userTaskProcessId"); + + // Complete a user task to generate audit logs + identityService.setAuthenticatedUserId("demo"); + completeAllUserTasksWithDefaultUserTaskId(); + + // Verify audit logs exist in C7 + long auditLogCount = historyService.createUserOperationLogQuery() + .processInstanceId(processInstance.getId()) + .count(); + assertThat(auditLogCount).isEqualTo(1); + + // when user tasks are not migrated + historyMigrator.migrateProcessDefinitions(); + historyMigrator.migrateProcessInstances(); + historyMigrator.migrateAuditLogs(); + + // then + List c8ProcessInstance = searchHistoricProcessInstances("userTaskProcessId"); + assertThat(c8ProcessInstance).hasSize(1); + List logs = searchAuditLogs("userTaskProcessId"); + assertThat(logs).hasSize(0); + } + + protected void assertAuditLogProperties(List logs, AuditLogEntity.AuditLogOperationType opType) { + assertThat(logs.getFirst().category()).isEqualTo(AuditLogEntity.AuditLogOperationCategory.USER_TASKS); + assertThat(logs).extracting(AuditLogEntity::entityType).containsOnly(AuditLogEntity.AuditLogEntityType.USER_TASK); + assertThat(logs).extracting(AuditLogEntity::operationType).containsOnly(opType); + } + +} diff --git a/data-migrator/qa/integration-tests/src/test/java/io/camunda/migration/data/qa/history/entity/HistoryProcessInstanceTest.java b/data-migrator/qa/integration-tests/src/test/java/io/camunda/migration/data/qa/history/entity/HistoryProcessInstanceTest.java index 46765e23d..c59cbd18d 100644 --- a/data-migrator/qa/integration-tests/src/test/java/io/camunda/migration/data/qa/history/entity/HistoryProcessInstanceTest.java +++ b/data-migrator/qa/integration-tests/src/test/java/io/camunda/migration/data/qa/history/entity/HistoryProcessInstanceTest.java @@ -238,7 +238,7 @@ public void shouldSkipEntitiesWhenRootProcessInstanceNotMigrated() { historyMigrator.setMode(MigratorMode.MIGRATE); // Mark parent as skipped - dbClient.insert(parentInstance.getId(), null, new Date(), IdKeyMapper.TYPE.HISTORY_PROCESS_INSTANCE, "test skip"); + dbClient.insert(parentInstance.getId(), (Long) null, new Date(), IdKeyMapper.TYPE.HISTORY_PROCESS_INSTANCE, "test skip"); // when - attempt to migrate (sub should be skipped because parent is skipped) historyMigrator.migrate(); From 2b534318f9ef5f22cbe1470cb24556a395089f70 Mon Sep 17 00:00:00 2001 From: Yana Vasileva Date: Thu, 5 Feb 2026 20:21:41 +0100 Subject: [PATCH 2/3] impl the review hints --- .../migration/data/impl/clients/C7Client.java | 5 +- .../migration/data/impl/clients/DbClient.java | 6 +- .../data/impl/history/AuditLogMigrator.java | 87 +++++++-- .../history/entity/AuditLogTransformer.java | 170 +++++++++++++----- .../impl/logging/HistoryMigratorLogs.java | 4 +- .../resources/MigratorResourceTest.java | 2 +- .../history/HistoryMigrationAbstractTest.java | 7 - .../history/IdKeyCreateTimeMappingTest.java | 2 +- .../history/entity/HistoryAuditLogTest.java | 7 + .../entity/HistoryAuditLogUserTaskTest.java | 5 +- 10 files changed, 226 insertions(+), 69 deletions(-) diff --git a/data-migrator/core/src/main/java/io/camunda/migration/data/impl/clients/C7Client.java b/data-migrator/core/src/main/java/io/camunda/migration/data/impl/clients/C7Client.java index 686b40926..9dd09c629 100644 --- a/data-migrator/core/src/main/java/io/camunda/migration/data/impl/clients/C7Client.java +++ b/data-migrator/core/src/main/java/io/camunda/migration/data/impl/clients/C7Client.java @@ -503,7 +503,10 @@ public void fetchAndHandleHistoricFlowNodes(Consumer c public void fetchAndHandleUserOperationLogEntries(Consumer callback, Date timestampAfter) { UserOperationLogQueryImpl query = (UserOperationLogQueryImpl) historyService.createUserOperationLogQuery() .orderByTimestamp() - .asc(); + .asc() +// .orderbByOperationId() +// .asc() + ; if (timestampAfter != null) { query.afterTimestamp(timestampAfter); diff --git a/data-migrator/core/src/main/java/io/camunda/migration/data/impl/clients/DbClient.java b/data-migrator/core/src/main/java/io/camunda/migration/data/impl/clients/DbClient.java index 658425685..96d7057e3 100644 --- a/data-migrator/core/src/main/java/io/camunda/migration/data/impl/clients/DbClient.java +++ b/data-migrator/core/src/main/java/io/camunda/migration/data/impl/clients/DbClient.java @@ -122,7 +122,7 @@ public void updateSkipReason(String c7Id, TYPE type, String skipReason) { } /** - * Inserts a new process instance record into the mapping table. + * Inserts a new record of the given type into the mapping table. */ public void insert(String c7Id, Long c8Key, Date createTime, TYPE type) { insert(c7Id, c8Key, createTime, type, null); @@ -136,14 +136,14 @@ public void insert(String c7Id, Long c8Key, TYPE type) { } /** - * Inserts a new process instance record into the mapping table. + * Inserts a new record of the given type into the mapping table. */ public void insert(String c7Id, Long c8Key, Date createTime, TYPE type, String skipReason) { insert(c7Id, (c8Key == null) ? null : c8Key.toString(), createTime, type, skipReason); } /** - * Inserts a new process instance record into the mapping table. + * Inserts a new record of the given type into the mapping table. */ public void insert(String c7Id, String c8Key, Date createTime, TYPE type, String skipReason) { String finalSkipReason = properties.getSaveSkipReason() ? skipReason : null; diff --git a/data-migrator/core/src/main/java/io/camunda/migration/data/impl/history/AuditLogMigrator.java b/data-migrator/core/src/main/java/io/camunda/migration/data/impl/history/AuditLogMigrator.java index 1dd00dc4c..17b50f2fc 100644 --- a/data-migrator/core/src/main/java/io/camunda/migration/data/impl/history/AuditLogMigrator.java +++ b/data-migrator/core/src/main/java/io/camunda/migration/data/impl/history/AuditLogMigrator.java @@ -29,6 +29,7 @@ import io.camunda.migration.data.interceptor.property.EntityConversionContext; import io.camunda.search.entities.ProcessInstanceEntity; import java.util.Date; +import org.camunda.bpm.engine.EntityTypes; import org.camunda.bpm.engine.history.UserOperationLogEntry; import org.springframework.stereotype.Service; @@ -122,7 +123,9 @@ protected AuditLogDbModel.Builder configureAuditLogBuilder(UserOperationLogEntry String key = String.format("%s-%s", C7_HISTORY_PARTITION_ID, getNextKey()); builder.auditLogKey(key); - resolveEntityKeys(builder, c7AuditLog); + resolveProcessInstanceKeys(builder, c7AuditLog); + resolveProcessDefinitionKey(builder, c7AuditLog); + resolveUserTaskKey(builder, c7AuditLog); setHistoryCleanupDate(c7AuditLog, builder); @@ -130,55 +133,106 @@ protected AuditLogDbModel.Builder configureAuditLogBuilder(UserOperationLogEntry } /** - * Resolves and sets process instance and process definition keys on the builder. + * Resolves and sets process instance keys on the builder. *

- * This method looks up the Camunda 8 keys for related entities based on the + * This method looks up the Camunda 8 keys for process instances based on the * Camunda 7 IDs from the audit log entry: *

    *
  • processInstanceKey - from C7 process instance ID
  • *
  • rootProcessInstanceKey - from C7 root process instance ID
  • - *
  • processDefinitionKey - from C7 process definition ID
  • - *
  • userTaskKey - from C7 task ID
  • *
* Keys are only set if the corresponding entity has already been migrated. + * If the entity type is PROCESS_INSTANCE, also sets the entityKey. *

* * @param builder the audit log builder * @param c7AuditLog the user operation log entry from Camunda 7 */ - protected void resolveEntityKeys( + protected void resolveProcessInstanceKeys( AuditLogDbModel.Builder builder, UserOperationLogEntry c7AuditLog) { String c7ProcessInstanceId = c7AuditLog.getProcessInstanceId(); String c7RootProcessInstanceId = c7AuditLog.getRootProcessInstanceId(); if (c7ProcessInstanceId != null && isMigrated(c7ProcessInstanceId, HISTORY_PROCESS_INSTANCE)) { - ProcessInstanceEntity processInstance = findProcessInstanceByC7Id(c7ProcessInstanceId); - builder.processInstanceKey(processInstance.processInstanceKey()); + var processInstanceId = findProcessInstanceByC7Id(c7ProcessInstanceId).processInstanceKey(); + builder.processInstanceKey(processInstanceId); + if (EntityTypes.PROCESS_INSTANCE.equals(c7AuditLog.getEntityType())){ + builder.entityKey(String.valueOf(processInstanceId)); + } if (c7RootProcessInstanceId != null && isMigrated(c7RootProcessInstanceId, HISTORY_PROCESS_INSTANCE)) { ProcessInstanceEntity rootProcessInstance = findProcessInstanceByC7Id(c7RootProcessInstanceId); - if (rootProcessInstance != null && rootProcessInstance.processInstanceKey() != null) { - builder.rootProcessInstanceKey(rootProcessInstance.processInstanceKey()); - } + builder.rootProcessInstanceKey(rootProcessInstance.processInstanceKey()); } } + } + + /** + * Resolves and sets process definition key on the builder. + *

+ * This method looks up the Camunda 8 key for the process definition based on the + * Camunda 7 process definition ID from the audit log entry. + * The key is only set if the process definition has already been migrated. + * If the entity type is PROCESS_DEFINITION, also sets the entityKey. + *

+ * + * @param builder the audit log builder + * @param c7AuditLog the user operation log entry from Camunda 7 + */ + protected void resolveProcessDefinitionKey( + AuditLogDbModel.Builder builder, + UserOperationLogEntry c7AuditLog) { String c7ProcessDefinitionId = c7AuditLog.getProcessDefinitionId(); if (c7ProcessDefinitionId != null && isMigrated(c7ProcessDefinitionId, HISTORY_PROCESS_DEFINITION)) { Long processDefinitionKey = findProcessDefinitionKey(c7ProcessDefinitionId); builder.processDefinitionKey(processDefinitionKey); + if (EntityTypes.PROCESS_DEFINITION.equals(c7AuditLog.getEntityType())){ + builder.entityKey(String.valueOf(processDefinitionKey)); + } } + } + + /** + * Resolves and sets user task key on the builder. + *

+ * This method looks up the Camunda 8 key for the user task based on the + * Camunda 7 task ID from the audit log entry. + * The key is only set if the user task has already been migrated. + * Also retrieves the element instance key from the user task and sets it on the builder. + * If the entity type is TASK, also sets the entityKey. + *

+ * + * @param builder the audit log builder + * @param c7AuditLog the user operation log entry from Camunda 7 + */ + protected void resolveUserTaskKey( + AuditLogDbModel.Builder builder, + UserOperationLogEntry c7AuditLog) { String c7TaskId = c7AuditLog.getTaskId(); if (c7TaskId != null && isMigrated(c7TaskId, HISTORY_USER_TASK)) { Long taskKey = dbClient.findC8KeyByC7IdAndType(c7TaskId, HISTORY_USER_TASK); + if (EntityTypes.TASK.equals(c7AuditLog.getEntityType())){ + builder.entityKey(String.valueOf(taskKey)); + } UserTaskDbModel userTaskDbModel = searchUserTasksByKey(taskKey); builder.userTaskKey(taskKey) .elementInstanceKey(userTaskDbModel.elementInstanceKey()); } } + /** + * Searches for a user task in Camunda 8 by its task key. + *

+ * This method queries the Camunda 8 database to retrieve the user task details + * including the element instance key, which is needed for the audit log entry. + *

+ * + * @param taskKey the Camunda 8 user task key + * @return the user task database model, or null if not found + */ protected UserTaskDbModel searchUserTasksByKey(Long taskKey) { return c8Client.searchUserTasks(UserTaskDbQuery.of(b -> b.filter(f -> f.userTaskKeys(taskKey)))) .stream() @@ -188,6 +242,17 @@ protected UserTaskDbModel searchUserTasksByKey(Long taskKey) { /** * Validates dependencies and inserts the audit log or marks it as skipped. + *

+ * This method performs the following validations: + *

    + *
  • Checks if the process definition has been migrated when referenced
  • + *
  • Checks if the process instance has been migrated when referenced
  • + *
  • Checks if the root process instance has been migrated when referenced
  • + *
  • Checks if the user task has been migrated when referenced
  • + *
+ * If any required dependency is missing, the audit log is marked as skipped with + * an appropriate reason. Otherwise, it is inserted into Camunda 8. + *

* * @param c7AuditLog the user operation log entry from Camunda 7 * @param context the entity conversion context diff --git a/data-migrator/core/src/main/java/io/camunda/migration/data/impl/interceptor/history/entity/AuditLogTransformer.java b/data-migrator/core/src/main/java/io/camunda/migration/data/impl/interceptor/history/entity/AuditLogTransformer.java index e25cef1e0..1f1c4b2ec 100644 --- a/data-migrator/core/src/main/java/io/camunda/migration/data/impl/interceptor/history/entity/AuditLogTransformer.java +++ b/data-migrator/core/src/main/java/io/camunda/migration/data/impl/interceptor/history/entity/AuditLogTransformer.java @@ -43,6 +43,24 @@ public Set> getTypes() { return Set.of(UserOperationLogEntry.class); } + /** + * Executes the transformation of a Camunda 7 UserOperationLogEntry to Camunda 8 AuditLogDbModel. + *

+ * This method: + *

    + *
  • Converts entity type, operation type, and category to Camunda 8 equivalents
  • + *
  • Sets actor information (user ID and actor type)
  • + *
  • Maps tenant information and scope
  • + *
  • Preserves annotations and other metadata
  • + *
  • Handles special cases where entity types differ between C7 and C8
  • + *
+ * Note: Key properties like auditLogKey, processInstanceKey, processDefinitionKey, userTaskKey, + * timestamp, and historyCleanupDate are set externally by AuditLogMigrator. + *

+ * + * @param context the entity conversion context containing the C7 entity and C8 builder + * @throws EntityInterceptorException if the C8 builder is null or conversion fails + */ @Override public void execute(EntityConversionContext context) { UserOperationLogEntry userOperationLog = (UserOperationLogEntry) context.getC7Entity(); @@ -54,7 +72,7 @@ public void execute(EntityConversionContext context) { String tenantId = getTenantId(userOperationLog.getTenantId()); builder .entityType(convertEntityType(userOperationLog)) - .operationType(convertOperationType(userOperationLog, builder)) + .operationType(convertOperationType(userOperationLog)) .partitionId(C7_HISTORY_PARTITION_ID) .result(AuditLogEntity.AuditLogOperationResult.SUCCESS) .entityVersion(C7_AUDIT_LOG_ENTITY_VERSION) @@ -65,16 +83,27 @@ public void execute(EntityConversionContext context) { .tenantId(tenantId) .tenantScope(getAuditLogTenantScope(tenantId)) .category(convertCategory(userOperationLog.getCategory())); - // Note: auditLogKey, processInstanceKey, rootProcessInstanceKey, processDefinitionKey, userTaskKey, timestamp, historyCleanupDate + // Note: auditLogKey, processInstanceKey, rootProcessInstanceKey, processDefinitionKey, userTaskKey, entityKey, timestamp, historyCleanupDate // are set externally in AuditLogMigrator + // not setting entityValueType and entityOperationIntent as they internal properties meant for future-proofing purposes updateEntityTypesThatDontMatchBetweenC7andC8(userOperationLog, builder); } + /** + * Determines the audit log tenant scope based on the tenant ID. + *

+ * Returns {@link AuditLogEntity.AuditLogTenantScope#GLOBAL} if the tenant ID is the default tenant, + * otherwise returns {@link AuditLogEntity.AuditLogTenantScope#TENANT}. + *

+ * + * @param tenantId the tenant ID to evaluate + * @return the appropriate tenant scope for the audit log entry + */ protected AuditLogEntity.@NonNull AuditLogTenantScope getAuditLogTenantScope(String tenantId) { AuditLogEntity.AuditLogTenantScope tenantScope; - if ( tenantId.equals(C8_DEFAULT_TENANT)) { + if (tenantId.equals(C8_DEFAULT_TENANT)) { tenantScope = AuditLogEntity.AuditLogTenantScope.GLOBAL; } else { tenantScope = AuditLogEntity.AuditLogTenantScope.TENANT; @@ -82,6 +111,21 @@ public void execute(EntityConversionContext context) { return tenantScope; } + /** + * Converts a Camunda 7 operation category to a Camunda 8 AuditLogOperationCategory. + *

+ * Mapping: + *

    + *
  • CATEGORY_ADMIN → ADMIN
  • + *
  • CATEGORY_OPERATOR → DEPLOYED_RESOURCES
  • + *
  • CATEGORY_TASK_WORKER → USER_TASKS
  • + *
  • Other categories → UNKNOWN
  • + *
+ *

+ * + * @param category the Camunda 7 operation category + * @return the corresponding Camunda 8 audit log operation category + */ protected AuditLogEntity.AuditLogOperationCategory convertCategory(String category) { return switch (category) { case UserOperationLogEntry.CATEGORY_ADMIN -> AuditLogEntity.AuditLogOperationCategory.ADMIN; @@ -90,6 +134,33 @@ protected AuditLogEntity.AuditLogOperationCategory convertCategory(String catego default -> AuditLogEntity.AuditLogOperationCategory.UNKNOWN; }; } + /** + * Converts a Camunda 7 entity type to a Camunda 8 AuditLogEntityType. + *

+ * This method maps various Camunda 7 entity types to their Camunda 8 equivalents: + *

    + *
  • PROCESS_INSTANCE → PROCESS_INSTANCE
  • + *
  • VARIABLE → VARIABLE
  • + *
  • TASK → USER_TASK
  • + *
  • DECISION_INSTANCE, DECISION_DEFINITION, DECISION_REQUIREMENTS_DEFINITION → DECISION
  • + *
  • USER → USER
  • + *
  • GROUP → GROUP
  • + *
  • TENANT → TENANT
  • + *
  • AUTHORIZATION → AUTHORIZATION
  • + *
  • INCIDENT → INCIDENT
  • + *
  • PROCESS_DEFINITION, DEPLOYMENT → RESOURCE
  • + *
+ *

+ *

+ * Some Camunda 7 entity types are not currently converted (e.g., BATCH, IDENTITY_LINK, + * ATTACHMENT, JOB_DEFINITION, JOB, EXTERNAL_TASK, CASE_DEFINITION, CASE_INSTANCE, etc.). + * Attempting to convert unsupported types will throw an EntityInterceptorException. + *

+ * + * @param userOperationLog the Camunda 7 user operation log entry + * @return the corresponding Camunda 8 audit log entity type + * @throws EntityInterceptorException if the entity type is not supported + */ protected AuditLogEntity.AuditLogEntityType convertEntityType(UserOperationLogEntry userOperationLog) { return switch (userOperationLog.getEntityType()) { case EntityTypes.PROCESS_INSTANCE -> AuditLogEntity.AuditLogEntityType.PROCESS_INSTANCE; @@ -114,7 +185,44 @@ protected AuditLogEntity.AuditLogEntityType convertEntityType(UserOperationLogEn }; } - protected AuditLogEntity.AuditLogOperationType convertOperationType(UserOperationLogEntry userOperationLog, AuditLogDbModel.Builder builder) { + /** + * Converts a Camunda 7 operation type to a Camunda 8 AuditLogOperationType. + *

+ * This method handles the mapping of various operation types, with special handling + * for context-specific operations: + *

+ *

Task Operations:

+ *
    + *
  • ASSIGN, CLAIM, DELEGATE → ASSIGN
  • + *
  • COMPLETE → COMPLETE
  • + *
  • SET_PRIORITY, SET_OWNER, UPDATE → UPDATE
  • + *
+ *

Process Instance Operations:

+ *
    + *
  • CREATE → CREATE
  • + *
  • DELETE → CANCEL (for process instances) or DELETE (for other entities)
  • + *
  • MODIFY_PROCESS_INSTANCE → MODIFY
  • + *
  • MIGRATE → MIGRATE
  • + *
  • DELETE_HISTORY, REMOVE_VARIABLE → DELETE
  • + *
+ *

Variable Operations:

+ *
    + *
  • MODIFY_VARIABLE, SET_VARIABLE, SET_VARIABLES → UPDATE
  • + *
+ *

Decision Operations:

+ *
    + *
  • EVALUATE → EVALUATE
  • + *
+ *

Incident Operations:

+ *
    + *
  • RESOLVE → RESOLVE (for process instances) or UPDATE (for other entities)
  • + *
+ * + * @param userOperationLog the Camunda 7 user operation log entry + * @return the corresponding Camunda 8 audit log operation type + * @throws EntityInterceptorException if the operation type is not supported + */ + protected AuditLogEntity.AuditLogOperationType convertOperationType(UserOperationLogEntry userOperationLog) { String operationType = userOperationLog.getOperationType(); return switch (operationType) { @@ -160,7 +268,6 @@ protected AuditLogEntity.AuditLogOperationType convertOperationType(UserOperatio // Incident operations case UserOperationLogEntry.OPERATION_TYPE_RESOLVE -> { if (EntityTypes.PROCESS_INSTANCE.equals(userOperationLog.getEntityType())) { - builder.entityType(AuditLogEntity.AuditLogEntityType.INCIDENT); yield AuditLogEntity.AuditLogOperationType.RESOLVE; } else { yield AuditLogEntity.AuditLogOperationType.UPDATE; @@ -172,6 +279,23 @@ protected AuditLogEntity.AuditLogOperationType convertOperationType(UserOperatio }; } + /** + * Updates entity types for special cases where C7 and C8 handle them differently. + *

+ * This method corrects the entity type in cases where Camunda 7 and Camunda 8 use + * different entity types for the same operation: + *

+ *
    + *
  • RESOLVE operation on PROCESS_INSTANCE → entity type becomes INCIDENT + * (because resolving incidents in C7 is logged as a process instance operation, + * but in C8 it's an incident operation)
  • + *
  • SET_VARIABLE or SET_VARIABLES operations → entity type becomes VARIABLE + * (ensures variable operations are correctly typed as variable entities)
  • + *
+ * + * @param userOperationLog the Camunda 7 user operation log entry + * @param builder the audit log builder to update + */ protected void updateEntityTypesThatDontMatchBetweenC7andC8(UserOperationLogEntry userOperationLog, AuditLogDbModel.Builder builder) { if (UserOperationLogEntry.OPERATION_TYPE_RESOLVE.equals(userOperationLog.getOperationType()) && EntityTypes.PROCESS_INSTANCE.equals(userOperationLog.getEntityType())) { @@ -182,40 +306,4 @@ protected void updateEntityTypesThatDontMatchBetweenC7andC8(UserOperationLogEntr } } - - // entityValueType and entityOperationIntent are internal properties meant for future-proofing purposes - // protected void convertValueType(UserOperationLogEntry userOperationLog, AuditLogDbModel.Builder builder) { - // switch (userOperationLog.getEntityType()) { - // case EntityTypes.PROCESS_INSTANCE -> { - // switch (userOperationLog.getOperationType()) { - // case UserOperationLogEntry.OPERATION_TYPE_CREATE -> - // builder.entityValueType(ValueType.PROCESS_INSTANCE_CREATION.value()); - // case UserOperationLogEntry.OPERATION_TYPE_MODIFY_PROCESS_INSTANCE -> - // builder.entityValueType(ValueType.PROCESS_INSTANCE_MODIFICATION.value()); - // case UserOperationLogEntry.OPERATION_TYPE_MIGRATE -> - // builder.entityValueType(ValueType.PROCESS_INSTANCE_MIGRATION.value()); - // default -> builder.entityValueType(ValueType.PROCESS_INSTANCE.value()); - // } - // } - // case EntityTypes.VARIABLE -> builder.entityValueType(ValueType.VARIABLE.value()); - // case EntityTypes.TASK -> builder.entityValueType(ValueType.USER_TASK.value()); - // case EntityTypes.DECISION_INSTANCE, EntityTypes.DECISION_DEFINITION -> { - // if (userOperationLog.getOperationType().equals(UserOperationLogEntry.OPERATION_TYPE_EVALUATE)) { - // builder.entityValueType(ValueType.DECISION_EVALUATION.value()); - // } else { - // builder.entityValueType(ValueType.DECISION.value()); - // } - // } - // case EntityTypes.DECISION_REQUIREMENTS_DEFINITION -> - // builder.entityValueType(ValueType.DECISION_REQUIREMENTS.value()); - // case EntityTypes.USER -> builder.entityValueType(ValueType.USER.value()); - // case EntityTypes.GROUP -> builder.entityValueType(ValueType.GROUP.value()); - // case EntityTypes.TENANT -> builder.entityValueType(ValueType.TENANT.value()); - // case EntityTypes.AUTHORIZATION -> builder.entityValueType(ValueType.AUTHORIZATION.value()); - // case EntityTypes.PROCESS_DEFINITION -> builder.entityValueType(ValueType.PROCESS.value()); - // case EntityTypes.DEPLOYMENT -> builder.entityValueType(ValueType.RESOURCE.value()); - // case EntityTypes.INCIDENT -> builder.entityValueType(ValueType.INCIDENT.value()); - // default -> builder.entityValueType(ValueType.SBE_UNKNOWN.value()); - // } - // } } diff --git a/data-migrator/core/src/main/java/io/camunda/migration/data/impl/logging/HistoryMigratorLogs.java b/data-migrator/core/src/main/java/io/camunda/migration/data/impl/logging/HistoryMigratorLogs.java index 62eb3a6f1..15cd557ef 100644 --- a/data-migrator/core/src/main/java/io/camunda/migration/data/impl/logging/HistoryMigratorLogs.java +++ b/data-migrator/core/src/main/java/io/camunda/migration/data/impl/logging/HistoryMigratorLogs.java @@ -98,8 +98,8 @@ public class HistoryMigratorLogs { public static final String SKIPPING_AUDIT_LOG_MISSING_PROCESS = SKIPPING_AUDIT_LOG + " Process instance not yet available."; public static final String SKIPPING_AUDIT_LOG_MISSING_ROOT_INSTANCE = SKIPPING_AUDIT_LOG + " Root process instance not yet available."; public static final String SKIPPING_AUDIT_LOG_MISSING_USER_TASK = SKIPPING_AUDIT_LOG + " User task not yet available."; - public static final String UNSUPPORTED_AUDIT_LOG_ENTITY_TYPE = "Can't migrate Audit log for entity type: "; - public static final String UNSUPPORTED_AUDIT_LOG_OPERATION_TYPE = "Can't migrate Audit log for operation type: "; + public static final String UNSUPPORTED_AUDIT_LOG_ENTITY_TYPE = "Can't migrate audit log for entity type: "; + public static final String UNSUPPORTED_AUDIT_LOG_OPERATION_TYPE = "Can't migrate audit log for operation type: "; public static final String MIGRATING_DECISION_REQUIREMENTS = "Migrating decision requirements"; public static final String MIGRATING_DECISION_REQUIREMENT = "Migrating decision requirements with C7 ID: [{}]"; diff --git a/data-migrator/plugins/cockpit/src/test/java/io/camunda/migration/data/plugin/cockpit/resources/MigratorResourceTest.java b/data-migrator/plugins/cockpit/src/test/java/io/camunda/migration/data/plugin/cockpit/resources/MigratorResourceTest.java index e88275991..4d83204f3 100644 --- a/data-migrator/plugins/cockpit/src/test/java/io/camunda/migration/data/plugin/cockpit/resources/MigratorResourceTest.java +++ b/data-migrator/plugins/cockpit/src/test/java/io/camunda/migration/data/plugin/cockpit/resources/MigratorResourceTest.java @@ -182,7 +182,7 @@ protected static void insertTestData(IdKeyDbModel idKeyDbModel) { stmt.setString(4, String.valueOf(idKeyDbModel.getType())); stmt.setString(5, idKeyDbModel.getSkipReason()); if (idKeyDbModel.getC8Key() == null) { - stmt.setNull(2, java.sql.Types.BIGINT); + stmt.setNull(2, java.sql.Types.VARCHAR); } else { stmt.setString(2, idKeyDbModel.getC8Key()); } diff --git a/data-migrator/qa/integration-tests/src/test/java/io/camunda/migration/data/qa/history/HistoryMigrationAbstractTest.java b/data-migrator/qa/integration-tests/src/test/java/io/camunda/migration/data/qa/history/HistoryMigrationAbstractTest.java index a1e41cba2..4b7ced43e 100644 --- a/data-migrator/qa/integration-tests/src/test/java/io/camunda/migration/data/qa/history/HistoryMigrationAbstractTest.java +++ b/data-migrator/qa/integration-tests/src/test/java/io/camunda/migration/data/qa/history/HistoryMigrationAbstractTest.java @@ -155,13 +155,6 @@ public List searchAuditLogsByCategory(String name) { .items(); } - public List searchAuditLogss(String processDefinitionId) { - return rdbmsService.getAuditLogReader() - .search(AuditLogQuery.of(q -> q.filter(f -> - f.processInstanceKeys(123L)))) - .items(); - } - /** * When the built-in ProcessInstanceTransformer is disabled, the processDefinitionId * is NOT prefixed during migration. This method allows searching with or without prefixing. diff --git a/data-migrator/qa/integration-tests/src/test/java/io/camunda/migration/data/qa/history/IdKeyCreateTimeMappingTest.java b/data-migrator/qa/integration-tests/src/test/java/io/camunda/migration/data/qa/history/IdKeyCreateTimeMappingTest.java index 12754942f..e3da31046 100644 --- a/data-migrator/qa/integration-tests/src/test/java/io/camunda/migration/data/qa/history/IdKeyCreateTimeMappingTest.java +++ b/data-migrator/qa/integration-tests/src/test/java/io/camunda/migration/data/qa/history/IdKeyCreateTimeMappingTest.java @@ -44,6 +44,6 @@ public void shouldCorrectlyMapCreateTimeDuringActualMigration() { assertThat(migratedInstance.getC7Id()).isEqualTo(processInstanceId); assertThat(migratedInstance.getCreateTime()).isNotNull().isBeforeOrEqualTo(beforeMigration); // Should be before we started the test - assertThat(Long.valueOf(migratedInstance.getC8Key())).isNotNull().isPositive(); + assertThat(migratedInstance.getC8Key()).isNotNull().matches("^[1-9][0-9-]*$"); } } diff --git a/data-migrator/qa/integration-tests/src/test/java/io/camunda/migration/data/qa/history/entity/HistoryAuditLogTest.java b/data-migrator/qa/integration-tests/src/test/java/io/camunda/migration/data/qa/history/entity/HistoryAuditLogTest.java index d6b6d294d..8b9d0949c 100644 --- a/data-migrator/qa/integration-tests/src/test/java/io/camunda/migration/data/qa/history/entity/HistoryAuditLogTest.java +++ b/data-migrator/qa/integration-tests/src/test/java/io/camunda/migration/data/qa/history/entity/HistoryAuditLogTest.java @@ -71,6 +71,8 @@ public void shouldMigrateAuditLogs() { AuditLogEntity log = logs.getFirst(); assertThat(log.auditLogKey()).isNotNull(); + + assertThat(log.entityKey()).isEqualTo(String.valueOf(log.processInstanceKey())); assertThat(log.processInstanceKey()).isEqualTo(c8ProcessInstance.getFirst().processInstanceKey()); assertThat(log.rootProcessInstanceKey()).isEqualTo(c8ProcessInstance.getFirst().processInstanceKey()); assertThat(log.processDefinitionKey()).isNotNull(); @@ -267,6 +269,9 @@ public void shouldMigrateAuditLogsForDeleteProcessDefinition() { List logs = searchAuditLogsByCategory(AuditLogEntity.AuditLogOperationCategory.DEPLOYED_RESOURCES.name()); assertThat(logs).hasSize(1); assertThat(logs).extracting(AuditLogEntity::operationType).contains(AuditLogEntity.AuditLogOperationType.DELETE); + assertThat(logs).extracting(AuditLogEntity::entityType).contains(AuditLogEntity.AuditLogEntityType.RESOURCE); + assertThat(logs).extracting(AuditLogEntity::entityKey).contains( + String.valueOf(logs.getFirst().processDefinitionKey())); } @Test @@ -330,6 +335,7 @@ public void shouldMigrateAuditLogsForDeleteDecisionHistory() { List logs = searchAuditLogsByCategory(AuditLogEntity.AuditLogOperationCategory.DEPLOYED_RESOURCES.name()); assertThat(logs).hasSize(2); assertThat(logs).extracting(AuditLogEntity::operationType).contains(AuditLogEntity.AuditLogOperationType.DELETE); + assertThat(logs).extracting(AuditLogEntity::entityType).contains(AuditLogEntity.AuditLogEntityType.DECISION); } @Test @@ -377,6 +383,7 @@ public void shouldMigrateAuditLogsForDeleteDeployment() { List logs = searchAuditLogsByCategory(AuditLogEntity.AuditLogOperationCategory.DEPLOYED_RESOURCES.name()); assertThat(logs).hasSize(1); assertThat(logs).extracting(AuditLogEntity::operationType).contains(AuditLogEntity.AuditLogOperationType.DELETE); + assertThat(logs).extracting(AuditLogEntity::entityType).contains(AuditLogEntity.AuditLogEntityType.RESOURCE); } @Test diff --git a/data-migrator/qa/integration-tests/src/test/java/io/camunda/migration/data/qa/history/entity/HistoryAuditLogUserTaskTest.java b/data-migrator/qa/integration-tests/src/test/java/io/camunda/migration/data/qa/history/entity/HistoryAuditLogUserTaskTest.java index 543334a61..b091f1280 100644 --- a/data-migrator/qa/integration-tests/src/test/java/io/camunda/migration/data/qa/history/entity/HistoryAuditLogUserTaskTest.java +++ b/data-migrator/qa/integration-tests/src/test/java/io/camunda/migration/data/qa/history/entity/HistoryAuditLogUserTaskTest.java @@ -75,6 +75,7 @@ public void shouldMigrateAuditLogsForTask() { AuditLogEntity log = logs.getFirst(); assertThat(log.auditLogKey()).isNotNull(); + assertThat(log.entityKey()).isEqualTo(String.valueOf(log.userTaskKey())); assertThat(log.processInstanceKey()).isEqualTo(c8ProcessInstance.getFirst().processInstanceKey()); assertThat(log.rootProcessInstanceKey()).isEqualTo(c8ProcessInstance.getFirst().processInstanceKey()); assertThat(log.processDefinitionKey()).isNotNull(); @@ -192,7 +193,7 @@ public void shouldMigrateAuditLogsForClaimTask() { .processInstanceId(processInstance.getId()) .operationType(UserOperationLogEntry.OPERATION_TYPE_CLAIM) .count(); - assertThat(auditLogCount).isEqualTo(1); // there are two UPDATE logs: for each property + assertThat(auditLogCount).isEqualTo(1); // when historyMigrator.migrate(); @@ -256,7 +257,7 @@ public void shouldMigrateAuditLogsForDeleteTask() { // then List logs = searchAuditLogsByCategory(AuditLogEntity.AuditLogOperationCategory.USER_TASKS.name()); - assertThat(logs).hasSize(1); // result is 0 since task is not linked to a process instance and can't be migrated + assertThat(logs).hasSize(1); assertAuditLogProperties(logs, AuditLogEntity.AuditLogOperationType.DELETE); } From d0acfa6ceb2f53bc78a1eb22e38c58c18bf08373 Mon Sep 17 00:00:00 2001 From: Yana Vasileva Date: Fri, 6 Feb 2026 11:19:53 +0100 Subject: [PATCH 3/3] c7 change --- .../io/camunda/migration/data/impl/clients/C7Client.java | 5 ++--- pom.xml | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/data-migrator/core/src/main/java/io/camunda/migration/data/impl/clients/C7Client.java b/data-migrator/core/src/main/java/io/camunda/migration/data/impl/clients/C7Client.java index 9dd09c629..16ccec7b9 100644 --- a/data-migrator/core/src/main/java/io/camunda/migration/data/impl/clients/C7Client.java +++ b/data-migrator/core/src/main/java/io/camunda/migration/data/impl/clients/C7Client.java @@ -504,9 +504,8 @@ public void fetchAndHandleUserOperationLogEntries(Consumer21 21 - 7.24.3-SNAPSHOT + 7.24.4-SNAPSHOT 8.9.0-SNAPSHOT 8.19.6 4.0.2