Java tutorial
/** * Copyright 2016-2018 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 com.datastax.driver.core.utils.UUIDs; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.collect.Lists; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import org.thingsboard.server.common.data.BaseData; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.audit.ActionStatus; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.audit.AuditLog; import org.thingsboard.server.common.data.id.AuditLogId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UUIDBased; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.page.TimePageData; import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.dao.audit.sink.AuditLogSink; import org.thingsboard.server.dao.entity.EntityService; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; import java.io.PrintWriter; import java.io.StringWriter; import java.util.List; import static org.thingsboard.server.dao.service.Validator.validateEntityId; import static org.thingsboard.server.dao.service.Validator.validateId; @Slf4j @Service @ConditionalOnProperty(prefix = "audit_log", value = "enabled", havingValue = "true") public class AuditLogServiceImpl implements AuditLogService { private static final ObjectMapper objectMapper = new ObjectMapper(); private static final String INCORRECT_TENANT_ID = "Incorrect tenantId "; private static final int INSERTS_PER_ENTRY = 3; @Autowired private AuditLogLevelFilter auditLogLevelFilter; @Autowired private AuditLogDao auditLogDao; @Autowired private EntityService entityService; @Autowired private AuditLogSink auditLogSink; @Override public TimePageData<AuditLog> findAuditLogsByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, TimePageLink pageLink) { log.trace("Executing findAuditLogsByTenantIdAndCustomerId [{}], [{}], [{}]", tenantId, customerId, pageLink); validateId(tenantId, INCORRECT_TENANT_ID + tenantId); validateId(customerId, "Incorrect customerId " + customerId); List<AuditLog> auditLogs = auditLogDao.findAuditLogsByTenantIdAndCustomerId(tenantId.getId(), customerId, pageLink); return new TimePageData<>(auditLogs, pageLink); } @Override public TimePageData<AuditLog> findAuditLogsByTenantIdAndUserId(TenantId tenantId, UserId userId, TimePageLink pageLink) { log.trace("Executing findAuditLogsByTenantIdAndUserId [{}], [{}], [{}]", tenantId, userId, pageLink); validateId(tenantId, INCORRECT_TENANT_ID + tenantId); validateId(userId, "Incorrect userId" + userId); List<AuditLog> auditLogs = auditLogDao.findAuditLogsByTenantIdAndUserId(tenantId.getId(), userId, pageLink); return new TimePageData<>(auditLogs, pageLink); } @Override public TimePageData<AuditLog> findAuditLogsByTenantIdAndEntityId(TenantId tenantId, EntityId entityId, TimePageLink pageLink) { log.trace("Executing findAuditLogsByTenantIdAndEntityId [{}], [{}], [{}]", tenantId, entityId, pageLink); validateId(tenantId, INCORRECT_TENANT_ID + tenantId); validateEntityId(entityId, INCORRECT_TENANT_ID + entityId); List<AuditLog> auditLogs = auditLogDao.findAuditLogsByTenantIdAndEntityId(tenantId.getId(), entityId, pageLink); return new TimePageData<>(auditLogs, pageLink); } @Override public TimePageData<AuditLog> findAuditLogsByTenantId(TenantId tenantId, TimePageLink pageLink) { log.trace("Executing findAuditLogs [{}]", pageLink); validateId(tenantId, INCORRECT_TENANT_ID + tenantId); List<AuditLog> auditLogs = auditLogDao.findAuditLogsByTenantId(tenantId.getId(), pageLink); return new TimePageData<>(auditLogs, pageLink); } @Override public <E extends HasName, I extends EntityId> ListenableFuture<List<Void>> logEntityAction(TenantId tenantId, CustomerId customerId, UserId userId, String userName, I entityId, E entity, ActionType actionType, Exception e, Object... additionalInfo) { if (canLog(entityId.getEntityType(), actionType)) { JsonNode actionData = constructActionData(entityId, entity, actionType, additionalInfo); ActionStatus actionStatus = ActionStatus.SUCCESS; String failureDetails = ""; String entityName = ""; if (entity != null) { entityName = entity.getName(); } else { try { entityName = entityService.fetchEntityNameAsync(tenantId, entityId).get(); } catch (Exception ex) { } } if (e != null) { actionStatus = ActionStatus.FAILURE; failureDetails = getFailureStack(e); } if (actionType == ActionType.RPC_CALL) { String rpcErrorString = extractParameter(String.class, additionalInfo); if (!StringUtils.isEmpty(rpcErrorString)) { actionStatus = ActionStatus.FAILURE; failureDetails = rpcErrorString; } } return logAction(tenantId, entityId, entityName, customerId, userId, userName, actionType, actionData, actionStatus, failureDetails); } else { return null; } } private <E extends HasName, I extends EntityId> JsonNode constructActionData(I entityId, E entity, ActionType actionType, Object... additionalInfo) { ObjectNode actionData = objectMapper.createObjectNode(); switch (actionType) { case ADDED: case UPDATED: case ALARM_ACK: case ALARM_CLEAR: case RELATIONS_DELETED: if (entity != null) { ObjectNode entityNode = objectMapper.valueToTree(entity); if (entityId.getEntityType() == EntityType.DASHBOARD) { entityNode.put("configuration", ""); } actionData.set("entity", entityNode); } if (entityId.getEntityType() == EntityType.RULE_CHAIN) { RuleChainMetaData ruleChainMetaData = extractParameter(RuleChainMetaData.class, additionalInfo); if (ruleChainMetaData != null) { ObjectNode ruleChainMetaDataNode = objectMapper.valueToTree(ruleChainMetaData); actionData.set("metadata", ruleChainMetaDataNode); } } break; case DELETED: case ACTIVATED: case SUSPENDED: case CREDENTIALS_READ: String strEntityId = extractParameter(String.class, additionalInfo); actionData.put("entityId", strEntityId); break; case ATTRIBUTES_UPDATED: actionData.put("entityId", entityId.toString()); String scope = extractParameter(String.class, 0, additionalInfo); List<AttributeKvEntry> attributes = extractParameter(List.class, 1, additionalInfo); actionData.put("scope", scope); ObjectNode attrsNode = objectMapper.createObjectNode(); if (attributes != null) { for (AttributeKvEntry attr : attributes) { attrsNode.put(attr.getKey(), attr.getValueAsString()); } } actionData.set("attributes", attrsNode); break; case ATTRIBUTES_DELETED: case ATTRIBUTES_READ: actionData.put("entityId", entityId.toString()); scope = extractParameter(String.class, 0, additionalInfo); actionData.put("scope", scope); List<String> keys = extractParameter(List.class, 1, additionalInfo); ArrayNode attrsArrayNode = actionData.putArray("attributes"); if (keys != null) { keys.forEach(attrsArrayNode::add); } break; case RPC_CALL: actionData.put("entityId", entityId.toString()); Boolean oneWay = extractParameter(Boolean.class, 1, additionalInfo); String method = extractParameter(String.class, 2, additionalInfo); String params = extractParameter(String.class, 3, additionalInfo); actionData.put("oneWay", oneWay); actionData.put("method", method); actionData.put("params", params); break; case CREDENTIALS_UPDATED: actionData.put("entityId", entityId.toString()); DeviceCredentials deviceCredentials = extractParameter(DeviceCredentials.class, additionalInfo); actionData.set("credentials", objectMapper.valueToTree(deviceCredentials)); break; case ASSIGNED_TO_CUSTOMER: strEntityId = extractParameter(String.class, 0, additionalInfo); String strCustomerId = extractParameter(String.class, 1, additionalInfo); String strCustomerName = extractParameter(String.class, 2, additionalInfo); actionData.put("entityId", strEntityId); actionData.put("assignedCustomerId", strCustomerId); actionData.put("assignedCustomerName", strCustomerName); break; case UNASSIGNED_FROM_CUSTOMER: strEntityId = extractParameter(String.class, 0, additionalInfo); strCustomerId = extractParameter(String.class, 1, additionalInfo); strCustomerName = extractParameter(String.class, 2, additionalInfo); actionData.put("entityId", strEntityId); actionData.put("unassignedCustomerId", strCustomerId); actionData.put("unassignedCustomerName", strCustomerName); break; case RELATION_ADD_OR_UPDATE: case RELATION_DELETED: EntityRelation relation = extractParameter(EntityRelation.class, 0, additionalInfo); actionData.set("relation", objectMapper.valueToTree(relation)); break; } return actionData; } private <T> T extractParameter(Class<T> clazz, Object... additionalInfo) { return extractParameter(clazz, 0, additionalInfo); } private <T> T extractParameter(Class<T> clazz, int index, Object... additionalInfo) { T result = null; if (additionalInfo != null && additionalInfo.length > index) { Object paramObject = additionalInfo[index]; if (clazz.isInstance(paramObject)) { result = clazz.cast(paramObject); } } return result; } private String getFailureStack(Exception e) { StringWriter sw = new StringWriter(); e.printStackTrace(new PrintWriter(sw)); return sw.toString(); } private boolean canLog(EntityType entityType, ActionType actionType) { return auditLogLevelFilter.logEnabled(entityType, actionType); } private AuditLog createAuditLogEntry(TenantId tenantId, EntityId entityId, String entityName, CustomerId customerId, UserId userId, String userName, ActionType actionType, JsonNode actionData, ActionStatus actionStatus, String actionFailureDetails) { AuditLog result = new AuditLog(); result.setId(new AuditLogId(UUIDs.timeBased())); result.setTenantId(tenantId); result.setEntityId(entityId); result.setEntityName(entityName); result.setCustomerId(customerId); result.setUserId(userId); result.setUserName(userName); result.setActionType(actionType); result.setActionData(actionData); result.setActionStatus(actionStatus); result.setActionFailureDetails(actionFailureDetails); return result; } private ListenableFuture<List<Void>> logAction(TenantId tenantId, EntityId entityId, String entityName, CustomerId customerId, UserId userId, String userName, ActionType actionType, JsonNode actionData, ActionStatus actionStatus, String actionFailureDetails) { AuditLog auditLogEntry = createAuditLogEntry(tenantId, entityId, entityName, customerId, userId, userName, actionType, actionData, actionStatus, actionFailureDetails); log.trace("Executing logAction [{}]", auditLogEntry); auditLogValidator.validate(auditLogEntry, AuditLog::getTenantId); List<ListenableFuture<Void>> futures = Lists.newArrayListWithExpectedSize(INSERTS_PER_ENTRY); futures.add(auditLogDao.savePartitionsByTenantId(auditLogEntry)); futures.add(auditLogDao.saveByTenantId(auditLogEntry)); futures.add(auditLogDao.saveByTenantIdAndEntityId(auditLogEntry)); futures.add(auditLogDao.saveByTenantIdAndCustomerId(auditLogEntry)); futures.add(auditLogDao.saveByTenantIdAndUserId(auditLogEntry)); auditLogSink.logAction(auditLogEntry); return Futures.allAsList(futures); } private DataValidator<AuditLog> auditLogValidator = new DataValidator<AuditLog>() { @Override protected void validateDataImpl(TenantId tenantId, AuditLog auditLog) { if (auditLog.getEntityId() == null) { throw new DataValidationException("Entity Id should be specified!"); } if (auditLog.getTenantId() == null) { throw new DataValidationException("Tenant Id should be specified!"); } if (auditLog.getUserId() == null) { throw new DataValidationException("User Id should be specified!"); } } }; }