diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java b/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java index db56acb8b2..3a70dacf65 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java @@ -22,52 +22,68 @@ import java.util.Optional; public enum ActionType { - ADDED(false, TbMsgType.ENTITY_CREATED), // log entity - DELETED(false, TbMsgType.ENTITY_DELETED), // log string id - UPDATED(false, TbMsgType.ENTITY_UPDATED), // log entity - ATTRIBUTES_UPDATED(false, TbMsgType.ATTRIBUTES_UPDATED), // log attributes/values - ATTRIBUTES_DELETED(false, TbMsgType.ATTRIBUTES_DELETED), // log attributes - TIMESERIES_UPDATED(false, TbMsgType.TIMESERIES_UPDATED), // log timeseries update - TIMESERIES_DELETED(false, TbMsgType.TIMESERIES_DELETED), // log timeseries - RPC_CALL(false, null), // log method and params - CREDENTIALS_UPDATED(false, null), // log new credentials - ASSIGNED_TO_CUSTOMER(false, TbMsgType.ENTITY_ASSIGNED), // log customer name - UNASSIGNED_FROM_CUSTOMER(false, TbMsgType.ENTITY_UNASSIGNED), // log customer name - ACTIVATED(false, null), // log string id - SUSPENDED(false, null), // log string id - CREDENTIALS_READ(true, null), // log device id - ATTRIBUTES_READ(true, null), // log attributes - RELATION_ADD_OR_UPDATE(false, TbMsgType.RELATION_ADD_OR_UPDATE), - RELATION_DELETED(false, TbMsgType.RELATION_DELETED), - RELATIONS_DELETED(false, TbMsgType.RELATIONS_DELETED), - REST_API_RULE_ENGINE_CALL(false, null), // log call to rule engine from REST API - ALARM_ACK(false, TbMsgType.ALARM_ACK), - ALARM_CLEAR(false, TbMsgType.ALARM_CLEAR), - ALARM_DELETE(false, TbMsgType.ALARM_DELETE), - ALARM_ASSIGNED(false, TbMsgType.ALARM_ASSIGNED), - ALARM_UNASSIGNED(false, TbMsgType.ALARM_UNASSIGNED), - LOGIN(false, null), - LOGOUT(false, null), - LOCKOUT(false, null), - ASSIGNED_FROM_TENANT(false, TbMsgType.ENTITY_ASSIGNED_FROM_TENANT), - ASSIGNED_TO_TENANT(false, TbMsgType.ENTITY_ASSIGNED_TO_TENANT), - PROVISION_SUCCESS(false, TbMsgType.PROVISION_SUCCESS), - PROVISION_FAILURE(false, TbMsgType.PROVISION_FAILURE), - ASSIGNED_TO_EDGE(false, TbMsgType.ENTITY_ASSIGNED_TO_EDGE), // log edge name - UNASSIGNED_FROM_EDGE(false, TbMsgType.ENTITY_UNASSIGNED_FROM_EDGE), - ADDED_COMMENT(false, TbMsgType.COMMENT_CREATED), - UPDATED_COMMENT(false, TbMsgType.COMMENT_UPDATED), - DELETED_COMMENT(false, null), - SMS_SENT(false, null); + ADDED(TbMsgType.ENTITY_CREATED), // log entity + DELETED(TbMsgType.ENTITY_DELETED), // log string id + UPDATED(TbMsgType.ENTITY_UPDATED), // log entity + ATTRIBUTES_UPDATED(TbMsgType.ATTRIBUTES_UPDATED), // log attributes/values + ATTRIBUTES_DELETED(TbMsgType.ATTRIBUTES_DELETED), // log attributes + TIMESERIES_UPDATED(TbMsgType.TIMESERIES_UPDATED), // log timeseries update + TIMESERIES_DELETED(TbMsgType.TIMESERIES_DELETED), // log timeseries + RPC_CALL(null), // log method and params + CREDENTIALS_UPDATED(null), // log new credentials + ASSIGNED_TO_CUSTOMER(TbMsgType.ENTITY_ASSIGNED), // log customer name + UNASSIGNED_FROM_CUSTOMER(TbMsgType.ENTITY_UNASSIGNED), // log customer name + ACTIVATED(null), // log string id + SUSPENDED(null), // log string id + CREDENTIALS_READ(true), // log device id + ATTRIBUTES_READ(true), // log attributes + RELATION_ADD_OR_UPDATE(TbMsgType.RELATION_ADD_OR_UPDATE), + RELATION_DELETED(TbMsgType.RELATION_DELETED), + RELATIONS_DELETED(TbMsgType.RELATIONS_DELETED), + REST_API_RULE_ENGINE_CALL(null), // log call to rule engine from REST API + ALARM_ACK(TbMsgType.ALARM_ACK, true), + ALARM_CLEAR(TbMsgType.ALARM_CLEAR, true), + ALARM_DELETE(TbMsgType.ALARM_DELETE, true), + ALARM_ASSIGNED(TbMsgType.ALARM_ASSIGNED, true), + ALARM_UNASSIGNED(TbMsgType.ALARM_UNASSIGNED, true), + LOGIN(null), + LOGOUT(null), + LOCKOUT(null), + ASSIGNED_FROM_TENANT(TbMsgType.ENTITY_ASSIGNED_FROM_TENANT), + ASSIGNED_TO_TENANT(TbMsgType.ENTITY_ASSIGNED_TO_TENANT), + PROVISION_SUCCESS(TbMsgType.PROVISION_SUCCESS), + PROVISION_FAILURE(TbMsgType.PROVISION_FAILURE), + ASSIGNED_TO_EDGE(TbMsgType.ENTITY_ASSIGNED_TO_EDGE), // log edge name + UNASSIGNED_FROM_EDGE(TbMsgType.ENTITY_UNASSIGNED_FROM_EDGE), + ADDED_COMMENT(TbMsgType.COMMENT_CREATED), + UPDATED_COMMENT(TbMsgType.COMMENT_UPDATED), + DELETED_COMMENT(null), + SMS_SENT(null); @Getter private final boolean isRead; private final TbMsgType ruleEngineMsgType; - ActionType(boolean isRead, TbMsgType ruleEngineMsgType) { + @Getter + private final boolean alarmAction; + + ActionType(boolean isRead) { this.isRead = isRead; + this.ruleEngineMsgType = null; + this.alarmAction = false; + } + + ActionType(TbMsgType ruleEngineMsgType) { + this.isRead = false; + this.ruleEngineMsgType = ruleEngineMsgType; + this.alarmAction = false; + } + + ActionType(TbMsgType ruleEngineMsgType, boolean isAlarmAction) { + this.isRead = false; this.ruleEngineMsgType = ruleEngineMsgType; + this.alarmAction = isAlarmAction; } public Optional getRuleEngineMsgType() { diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/audit/ActionTypeTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/audit/ActionTypeTest.java index 6a5ed54896..ddfe068937 100644 --- a/common/data/src/test/java/org/thingsboard/server/common/data/audit/ActionTypeTest.java +++ b/common/data/src/test/java/org/thingsboard/server/common/data/audit/ActionTypeTest.java @@ -21,6 +21,11 @@ import java.util.List; import static org.assertj.core.api.Assertions.assertThat; import static org.thingsboard.server.common.data.audit.ActionType.ACTIVATED; +import static org.thingsboard.server.common.data.audit.ActionType.ALARM_ACK; +import static org.thingsboard.server.common.data.audit.ActionType.ALARM_ASSIGNED; +import static org.thingsboard.server.common.data.audit.ActionType.ALARM_CLEAR; +import static org.thingsboard.server.common.data.audit.ActionType.ALARM_DELETE; +import static org.thingsboard.server.common.data.audit.ActionType.ALARM_UNASSIGNED; import static org.thingsboard.server.common.data.audit.ActionType.ATTRIBUTES_READ; import static org.thingsboard.server.common.data.audit.ActionType.CREDENTIALS_READ; import static org.thingsboard.server.common.data.audit.ActionType.CREDENTIALS_UPDATED; @@ -35,7 +40,7 @@ import static org.thingsboard.server.common.data.audit.ActionType.SUSPENDED; class ActionTypeTest { - private static final List typesWithNullRuleEngineMsgType = List.of( + private final List typesWithNullRuleEngineMsgType = List.of( RPC_CALL, CREDENTIALS_UPDATED, ACTIVATED, @@ -50,6 +55,10 @@ class ActionTypeTest { REST_API_RULE_ENGINE_CALL ); + private final List alarmActionTypes = List.of( + ALARM_ACK, ALARM_CLEAR, ALARM_DELETE, ALARM_ASSIGNED, ALARM_UNASSIGNED + ); + // backward-compatibility tests @Test @@ -64,4 +73,16 @@ class ActionTypeTest { } } + @Test + void isAlarmActionTest() { + var types = ActionType.values(); + for (var type : types) { + if (alarmActionTypes.contains(type)) { + assertThat(type.isAlarmAction()).isTrue(); + } else { + assertThat(type.isAlarmAction()).isFalse(); + } + } + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java index c67a105cae..933909e7e9 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java @@ -29,8 +29,8 @@ import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.StringUtils; -import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmComment; +import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.audit.ActionStatus; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.audit.AuditLog; @@ -124,13 +124,17 @@ public class AuditLogServiceImpl implements AuditLogService { ActionStatus actionStatus = ActionStatus.SUCCESS; String failureDetails = ""; String entityName = "N/A"; - if (entity != null && !(entity instanceof Alarm)) { - entityName = entity.getName(); - } else { - try { - entityName = entityService.fetchEntityName(tenantId, entityId).orElse(entityName); - } catch (Exception ignored) { + if (entity != null) { + if (actionType.isAlarmAction()) { + entityName = (entity instanceof AlarmInfo alarmInfo) + ? alarmInfo.getOriginatorName() + : entityService.fetchEntityName(tenantId, entityId).orElse(entityName); + } else { + entityName = entity.getName(); } + } else try { + entityName = entityService.fetchEntityName(tenantId, entityId).orElse(entityName); + } catch (Exception ignored) { } if (e != null) { actionStatus = ActionStatus.FAILURE; diff --git a/dao/src/test/java/org/thingsboard/server/dao/audit/AuditLogServiceImplTest.java b/dao/src/test/java/org/thingsboard/server/dao/audit/AuditLogServiceImplTest.java new file mode 100644 index 0000000000..2a876c58db --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/audit/AuditLogServiceImplTest.java @@ -0,0 +1,141 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.audit; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmComment; +import org.thingsboard.server.common.data.alarm.AlarmInfo; +import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.audit.AuditLog; +import org.thingsboard.server.common.data.id.AlarmId; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.dao.audit.sink.AuditLogSink; +import org.thingsboard.server.dao.entity.EntityService; +import org.thingsboard.server.dao.service.validator.AuditLogDataValidator; +import org.thingsboard.server.dao.sql.JpaExecutorService; + +import java.util.UUID; +import java.util.concurrent.Callable; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.never; +import static org.mockito.BDDMockito.then; + +@ExtendWith(MockitoExtension.class) +public class AuditLogServiceImplTest { + + private final TenantId TENANT_ID = TenantId.fromUUID(UUID.fromString("9114e9ac-6c28-4019-a2a7-b948cb9500d5")); + private final CustomerId CUSTOMER_ID = new CustomerId(UUID.fromString("d15822ef-09eb-49a6-9068-21b9c8ae3356")); + private final UserId USER_ID = new UserId(UUID.fromString("47a2c904-3a47-4530-91bb-51068a4610a7")); + private final DeviceId DEVICE_ID = new DeviceId(UUID.fromString("b913c12a-9942-4cbd-9481-c42dad4831b0")); + + private final String USER_NAME = "Test User"; + + @InjectMocks + private AuditLogServiceImpl auditLogService; + @Mock + private EntityService entityService; + @Mock + private AuditLogLevelFilter auditLogLevelFilter; + @Mock + private AuditLogDataValidator auditLogDataValidator; + @Mock + private JpaExecutorService executor; + @Mock + private AuditLogDao auditLogDao; + @Mock + private AuditLogSink auditLogSink; + + @Test + public void givenEntityIsNull_whenLogEntityAction_thenShouldFetchEntityName() { + // GIVEN + given(auditLogLevelFilter.logEnabled(any(), any())).willReturn(true); + + // WHEN + auditLogService.logEntityAction(TENANT_ID, CUSTOMER_ID, USER_ID, USER_NAME, DEVICE_ID, null, ActionType.ADDED, null); + + // THEN + then(entityService).should().fetchEntityName(TENANT_ID, DEVICE_ID); + } + + @Test + public void givenActionTypeIsAlarmActionAndEntityIsAlarm_whenLogEntityAction_thenShouldGetEntityName() throws Exception { + // GIVEN + given(auditLogLevelFilter.logEnabled(any(), any())).willReturn(true); + Alarm alarm = new Alarm(new AlarmId(UUID.fromString("55f577b3-6ef5-4b99-92dc-70eb78b2a970"))); + alarm.setType("Test Alarm"); + AlarmComment comment = new AlarmComment(); + comment.setComment(JacksonUtil.toJsonNode("{\"comment\": \"test\"}")); + + // WHEN + auditLogService.logEntityAction(TENANT_ID, CUSTOMER_ID, USER_ID, USER_NAME, alarm.getId(), alarm, ActionType.ADDED_COMMENT, null, comment); + + // THEN + then(entityService).should(never()).fetchEntityName(any(), any()); + ArgumentCaptor submitTask = ArgumentCaptor.forClass(Callable.class); + then(executor).should().submit(submitTask.capture()); + submitTask.getValue().call(); + ArgumentCaptor auditLogEntry = ArgumentCaptor.forClass(AuditLog.class); + then(auditLogDao).should().save(eq(TENANT_ID), auditLogEntry.capture()); + assertThat(auditLogEntry.getValue().getEntityName()).isEqualTo("Test Alarm"); + } + + @Test + public void givenActionTypeIsAlarmActionAndEntityIsAlarmInfo_whenLogEntityAction_thenShouldGetEntityOriginatorName() throws Exception { + // GIVEN + given(auditLogLevelFilter.logEnabled(any(), any())).willReturn(true); + AlarmInfo alarmInfo = new AlarmInfo(); + alarmInfo.setOriginatorName("Test Device"); + + // WHEN + auditLogService.logEntityAction(TENANT_ID, CUSTOMER_ID, USER_ID, USER_NAME, DEVICE_ID, alarmInfo, ActionType.ALARM_ASSIGNED, null); + + // THEN + then(entityService).should(never()).fetchEntityName(any(), any()); + ArgumentCaptor submitTask = ArgumentCaptor.forClass(Callable.class); + then(executor).should().submit(submitTask.capture()); + submitTask.getValue().call(); + ArgumentCaptor auditLogEntry = ArgumentCaptor.forClass(AuditLog.class); + then(auditLogDao).should().save(eq(TENANT_ID), auditLogEntry.capture()); + assertThat(auditLogEntry.getValue().getEntityName()).isEqualTo("Test Device"); + } + + @Test + public void givenActionTypeIsAlarmActionAndEntityIsAlarm_whenLogEntityAction_thenShouldFetchEntityName() { + // GIVEN + given(auditLogLevelFilter.logEnabled(any(), any())).willReturn(true); + + // WHEN + auditLogService.logEntityAction(TENANT_ID, CUSTOMER_ID, USER_ID, USER_NAME, DEVICE_ID, new Alarm(), ActionType.ALARM_DELETE, null); + + // THEN + then(entityService).should().fetchEntityName(TENANT_ID, DEVICE_ID); + } + +}