Merge pull request #11421 from irynamatveieva/fix/audit-logs-alarm-deleted

Fix null entityId in audit log when alarm is deleted
This commit is contained in:
Viacheslav Klimov 2024-08-28 18:48:55 +03:00 committed by GitHub
commit e0244c2970
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 278 additions and 60 deletions

View File

@ -196,7 +196,7 @@ public class DefaultTbAlarmService extends AbstractTbEntityService implements Tb
public Boolean delete(Alarm alarm, User user) {
TenantId tenantId = alarm.getTenantId();
logEntityActionService.logEntityAction(tenantId, alarm.getOriginator(), alarm, alarm.getCustomerId(),
ActionType.ALARM_DELETE, user);
ActionType.ALARM_DELETE, user, alarm.getId());
return alarmSubscriptionService.deleteAlarm(tenantId, alarm.getId());
}

View File

@ -260,7 +260,7 @@ public class AlarmControllerTest extends AbstractControllerTest {
doDelete("/api/alarm/" + alarm.getId()).andExpect(status().isOk());
testNotifyEntityAllOneTime(new Alarm(alarm), alarm.getId(), alarm.getOriginator(),
tenantId, customerId, customerUserId, CUSTOMER_USER_EMAIL, ActionType.ALARM_DELETE);
tenantId, customerId, customerUserId, CUSTOMER_USER_EMAIL, ActionType.ALARM_DELETE, alarm.getId());
}
@Test
@ -273,7 +273,7 @@ public class AlarmControllerTest extends AbstractControllerTest {
doDelete("/api/alarm/" + alarm.getId()).andExpect(status().isOk());
testNotifyEntityAllOneTime(new Alarm(alarm), alarm.getId(), alarm.getOriginator(),
tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_DELETE);
tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_DELETE, alarm.getId());
}
@Test

View File

@ -129,7 +129,7 @@ public class DefaultTbAlarmServiceTest {
public void testDelete() {
service.delete(new Alarm(), new User());
verify(logEntityActionService, times(1)).logEntityAction(any(), any(), any(), any(), eq(ActionType.ALARM_DELETE), any());
verify(logEntityActionService, times(1)).logEntityAction(any(), any(), any(), any(), eq(ActionType.ALARM_DELETE), any(), any());
verify(alarmSubscriptionService, times(1)).deleteAlarm(any(), any());
}

View File

@ -22,52 +22,72 @@ 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, // log method and params
CREDENTIALS_UPDATED, // log new credentials
ASSIGNED_TO_CUSTOMER(TbMsgType.ENTITY_ASSIGNED), // log customer name
UNASSIGNED_FROM_CUSTOMER(TbMsgType.ENTITY_UNASSIGNED), // log customer name
ACTIVATED, // log string id
SUSPENDED, // 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, // 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,
LOGOUT,
LOCKOUT,
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,
SMS_SENT;
@Getter
private final boolean isRead;
private final boolean read;
private final TbMsgType ruleEngineMsgType;
ActionType(boolean isRead, TbMsgType ruleEngineMsgType) {
this.isRead = isRead;
@Getter
private final boolean alarmAction;
ActionType() {
this(false, null, false);
}
ActionType(boolean read) {
this(read, null, false);
}
ActionType(TbMsgType ruleEngineMsgType) {
this(false, ruleEngineMsgType, false);
}
ActionType(TbMsgType ruleEngineMsgType, boolean isAlarmAction) {
this(false, ruleEngineMsgType, isAlarmAction);
}
ActionType(boolean read, TbMsgType ruleEngineMsgType, boolean alarmAction) {
this.read = read;
this.ruleEngineMsgType = ruleEngineMsgType;
this.alarmAction = alarmAction;
}
public Optional<TbMsgType> getRuleEngineMsgType() {

View File

@ -17,10 +17,16 @@ package org.thingsboard.server.common.data.audit;
import org.junit.jupiter.api.Test;
import java.util.List;
import java.util.EnumSet;
import java.util.Set;
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 +41,7 @@ import static org.thingsboard.server.common.data.audit.ActionType.SUSPENDED;
class ActionTypeTest {
private static final List<ActionType> typesWithNullRuleEngineMsgType = List.of(
private final Set<ActionType> typesWithNullRuleEngineMsgType = EnumSet.of(
RPC_CALL,
CREDENTIALS_UPDATED,
ACTIVATED,
@ -50,6 +56,12 @@ class ActionTypeTest {
REST_API_RULE_ENGINE_CALL
);
private final Set<ActionType> alarmActionTypes = EnumSet.of(
ALARM_ACK, ALARM_CLEAR, ALARM_DELETE, ALARM_ASSIGNED, ALARM_UNASSIGNED
);
private final Set<ActionType> readActionTypes = EnumSet.of(CREDENTIALS_READ, ATTRIBUTES_READ);
// backward-compatibility tests
@Test
@ -64,4 +76,28 @@ 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();
}
}
}
@Test
void isReadActionTypeTest() {
var types = ActionType.values();
for (var type : types) {
if (readActionTypes.contains(type)) {
assertThat(type.isRead()).isTrue();
} else {
assertThat(type.isRead()).isFalse();
}
}
}
}

View File

@ -30,6 +30,7 @@ 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.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;
@ -122,15 +123,7 @@ public class AuditLogServiceImpl implements AuditLogService {
JsonNode actionData = constructActionData(entityId, entity, actionType, additionalInfo);
ActionStatus actionStatus = ActionStatus.SUCCESS;
String failureDetails = "";
String entityName = "N/A";
if (entity != null) {
entityName = entity.getName();
} else {
try {
entityName = entityService.fetchEntityName(tenantId, entityId).orElse(entityName);
} catch (Exception ignored) {
}
}
String entityName = getEntityName(tenantId, entityId, entity, actionType);
if (e != null) {
actionStatus = ActionStatus.FAILURE;
failureDetails = getFailureStack(e);
@ -157,6 +150,27 @@ public class AuditLogServiceImpl implements AuditLogService {
}
}
private <E extends HasName, I extends EntityId> String getEntityName(TenantId tenantId, I entityId, E entity, ActionType actionType) {
if (entity == null) {
return fetchEntityName(tenantId, entityId);
}
if (!actionType.isAlarmAction()) {
return entity.getName();
}
if (entity instanceof AlarmInfo alarmInfo) {
return alarmInfo.getOriginatorName();
}
return fetchEntityName(tenantId, entityId);
}
private <I extends EntityId> String fetchEntityName(TenantId tenantId, I entityId) {
try {
return entityService.fetchEntityName(tenantId, entityId).orElse("N/A");
} catch (Exception ignored) {
return "N/A";
}
}
private <E extends HasName, I extends EntityId> JsonNode constructActionData(I entityId, E entity,
ActionType actionType,
Object... additionalInfo) {
@ -192,6 +206,10 @@ public class AuditLogServiceImpl implements AuditLogService {
actionData.set("comment", comment.getComment());
break;
case ALARM_DELETE:
EntityId alarmId = extractParameter(EntityId.class, additionalInfo);
actionData.put("alarmId", alarmId != null ? alarmId.toString() : null);
actionData.put("originatorId", entityId.toString());
break;
case DELETED:
case ACTIVATED:
case SUSPENDED:

View File

@ -0,0 +1,144 @@
/**
* 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.Optional;
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.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() throws Exception {
// GIVEN
given(auditLogLevelFilter.logEnabled(any(), any())).willReturn(true);
given(entityService.fetchEntityName(any(), any())).willReturn(Optional.of("Test device"));
// 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);
verifyEntityName("Test device");
}
@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
verifyEntityName("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
verifyEntityName("Test device");
}
@Test
public void givenActionTypeIsAlarmActionAndEntityIsAlarm_whenLogEntityAction_thenShouldFetchEntityName() throws Exception {
// GIVEN
given(auditLogLevelFilter.logEnabled(any(), any())).willReturn(true);
given(entityService.fetchEntityName(any(), any())).willReturn(Optional.of("Test alarm"));
// 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);
verifyEntityName("Test alarm");
}
private void verifyEntityName(String entityName) throws Exception {
then(auditLogDataValidator).should().validate(any(AuditLog.class), any());
ArgumentCaptor<Callable> submitTask = ArgumentCaptor.forClass(Callable.class);
then(executor).should().submit(submitTask.capture());
submitTask.getValue().call();
ArgumentCaptor<AuditLog> auditLogEntry = ArgumentCaptor.forClass(AuditLog.class);
then(auditLogDao).should().save(eq(TENANT_ID), auditLogEntry.capture());
assertThat(auditLogEntry.getValue().getEntityName()).isEqualTo(entityName);
then(auditLogSink).should().logAction(any());
}
}

View File

@ -958,11 +958,11 @@
"type-relation-add-or-update": "Relation updated",
"type-relation-delete": "Relation deleted",
"type-relations-delete": "All relation deleted",
"type-alarm-ack": "Acknowledged",
"type-alarm-clear": "Cleared",
"type-alarm-delete": "Deleted",
"type-alarm-assign": "Assigned",
"type-alarm-unassign": "Unassigned",
"type-alarm-ack": "Alarm acknowledged",
"type-alarm-clear": "Alarm cleared",
"type-alarm-delete": "Alarm deleted",
"type-alarm-assign": "Alarm assigned",
"type-alarm-unassign": "Alarm unassigned",
"type-added-comment": "Added comment",
"type-updated-comment": "Updated comment",
"type-deleted-comment": "Deleted comment",