diff --git a/application/src/main/java/org/thingsboard/server/controller/AuditLogController.java b/application/src/main/java/org/thingsboard/server/controller/AuditLogController.java index b79359b44f..4d1d50ecab 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AuditLogController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AuditLogController.java @@ -18,20 +18,64 @@ package org.thingsboard.server.controller; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; import org.thingsboard.server.common.data.audit.AuditLog; +import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.page.TimePageData; import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.exception.ThingsboardException; +import java.util.UUID; + @RestController @RequestMapping("/api") public class AuditLogController extends BaseController { @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/audit/logs/{entityType}/{entityId}", params = {"limit"}, method = RequestMethod.GET) + @RequestMapping(value = "/audit/logs/customer/{customerId}", params = {"limit"}, method = RequestMethod.GET) @ResponseBody - public TimePageData getAuditLogs( + public TimePageData getAuditLogsByCustomerId( + @PathVariable("customerId") String strCustomerId, + @RequestParam int limit, + @RequestParam(required = false) Long startTime, + @RequestParam(required = false) Long endTime, + @RequestParam(required = false, defaultValue = "false") boolean ascOrder, + @RequestParam(required = false) String offset) throws ThingsboardException { + try { + checkParameter("CustomerId", strCustomerId); + TenantId tenantId = getCurrentUser().getTenantId(); + TimePageLink pageLink = createPageLink(limit, startTime, endTime, ascOrder, offset); + return checkNotNull(auditLogService.findAuditLogsByTenantIdAndCustomerId(tenantId, new CustomerId(UUID.fromString(strCustomerId)), pageLink)); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/audit/logs/user/{userId}", params = {"limit"}, method = RequestMethod.GET) + @ResponseBody + public TimePageData getAuditLogsByUserId( + @PathVariable("userId") String strUserId, + @RequestParam int limit, + @RequestParam(required = false) Long startTime, + @RequestParam(required = false) Long endTime, + @RequestParam(required = false, defaultValue = "false") boolean ascOrder, + @RequestParam(required = false) String offset) throws ThingsboardException { + try { + checkParameter("UserId", strUserId); + TenantId tenantId = getCurrentUser().getTenantId(); + TimePageLink pageLink = createPageLink(limit, startTime, endTime, ascOrder, offset); + return checkNotNull(auditLogService.findAuditLogsByTenantIdAndUserId(tenantId, new UserId(UUID.fromString(strUserId)), pageLink)); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/audit/logs/entity/{entityType}/{entityId}", params = {"limit"}, method = RequestMethod.GET) + @ResponseBody + public TimePageData getAuditLogsByEntityId( @PathVariable("entityType") String strEntityType, @PathVariable("entityId") String strEntityId, @RequestParam int limit, diff --git a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java index 8d08706993..0844ce0bf4 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java @@ -85,12 +85,16 @@ public class DeviceController extends BaseController { savedDevice.getName(), savedDevice.getType()); - // TODO: refactor to ANNOTATION usage - if (device.getId() == null) { - logDeviceAction(savedDevice, ActionType.ADDED); - } else { - logDeviceAction(savedDevice, ActionType.UPDATED); - } + auditLogService.logEntityAction( + getCurrentUser(), + savedDevice.getId(), + savedDevice.getName(), + savedDevice.getCustomerId(), + device.getId() == null ? ActionType.ADDED : ActionType.UPDATED, + null, + ActionStatus.SUCCESS, + null); + return savedDevice; } catch (Exception e) { @@ -107,8 +111,15 @@ public class DeviceController extends BaseController { DeviceId deviceId = new DeviceId(toUUID(strDeviceId)); Device device = checkDeviceId(deviceId); deviceService.deleteDevice(deviceId); - // TODO: refactor to ANNOTATION usage - logDeviceAction(device, ActionType.DELETED); + auditLogService.logEntityAction( + getCurrentUser(), + device.getId(), + device.getName(), + device.getCustomerId(), + ActionType.DELETED, + null, + ActionStatus.SUCCESS, + null); } catch (Exception e) { throw handleException(e); } @@ -189,8 +200,15 @@ public class DeviceController extends BaseController { Device device = checkDeviceId(deviceCredentials.getDeviceId()); DeviceCredentials result = checkNotNull(deviceCredentialsService.updateDeviceCredentials(deviceCredentials)); actorService.onCredentialsUpdate(getCurrentUser().getTenantId(), deviceCredentials.getDeviceId()); - // TODO: refactor to ANNOTATION usage - logDeviceAction(device, ActionType.CREDENTIALS_UPDATED); + auditLogService.logEntityAction( + getCurrentUser(), + device.getId(), + device.getName(), + device.getCustomerId(), + ActionType.CREDENTIALS_UPDATED, + null, + ActionStatus.SUCCESS, + null); return result; } catch (Exception e) { throw handleException(e); @@ -321,19 +339,4 @@ public class DeviceController extends BaseController { throw handleException(e); } } - - // TODO: refactor to ANNOTATION usage - private void logDeviceAction(Device device, ActionType actionType) throws ThingsboardException { - auditLogService.logAction( - getCurrentUser().getTenantId(), - device.getId(), - device.getName(), - device.getCustomerId(), - getCurrentUser().getId(), - getCurrentUser().getName(), - actionType, - null, - ActionStatus.SUCCESS, - null); - } } diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 83630ce56a..3d8501fd0a 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -244,7 +244,6 @@ spring: username: "${SPRING_DATASOURCE_USERNAME:sa}" password: "${SPRING_DATASOURCE_PASSWORD:}" - # PostgreSQL DAO Configuration #spring: # data: @@ -260,3 +259,8 @@ spring: # url: "${SPRING_DATASOURCE_URL:jdbc:postgresql://localhost:5432/thingsboard}" # username: "${SPRING_DATASOURCE_USERNAME:postgres}" # password: "${SPRING_DATASOURCE_PASSWORD:postgres}" + +# Audit log parameters +audit_log: + # Enable/disable audit log functionality. + enabled: "${AUDIT_LOG_ENABLED:true}" \ No newline at end of file diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseAuditLogControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseAuditLogControllerTest.java index 5e74416868..a826ee85d1 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseAuditLogControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseAuditLogControllerTest.java @@ -27,6 +27,7 @@ import org.thingsboard.server.common.data.audit.AuditLog; import org.thingsboard.server.common.data.page.TimePageData; import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.data.security.Authority; +import org.thingsboard.server.dao.model.ModelConstants; import java.util.ArrayList; import java.util.List; @@ -36,6 +37,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. public abstract class BaseAuditLogControllerTest extends AbstractControllerTest { private Tenant savedTenant; + private User tenantAdmin; @Before public void beforeTest() throws Exception { @@ -46,14 +48,14 @@ public abstract class BaseAuditLogControllerTest extends AbstractControllerTest savedTenant = doPost("/api/tenant", tenant, Tenant.class); Assert.assertNotNull(savedTenant); - User tenantAdmin = new User(); + tenantAdmin = new User(); tenantAdmin.setAuthority(Authority.TENANT_ADMIN); tenantAdmin.setTenantId(savedTenant.getId()); tenantAdmin.setEmail("tenant2@thingsboard.org"); tenantAdmin.setFirstName("Joe"); tenantAdmin.setLastName("Downs"); - createUserAndLogin(tenantAdmin, "testPassword1"); + tenantAdmin = createUserAndLogin(tenantAdmin, "testPassword1"); } @After @@ -65,7 +67,7 @@ public abstract class BaseAuditLogControllerTest extends AbstractControllerTest } @Test - public void testSaveDeviceAuditLogs() throws Exception { + public void testAuditLogs() throws Exception { for (int i = 0; i < 178; i++) { Device device = new Device(); device.setName("Device" + i); @@ -87,10 +89,38 @@ public abstract class BaseAuditLogControllerTest extends AbstractControllerTest } while (pageData.hasNext()); Assert.assertEquals(178, loadedAuditLogs.size()); + + loadedAuditLogs = new ArrayList<>(); + pageLink = new TimePageLink(23); + do { + pageData = doGetTypedWithTimePageLink("/api/audit/logs/customer/" + ModelConstants.NULL_UUID + "?", + new TypeReference>() { + }, pageLink); + loadedAuditLogs.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageData.getNextPageLink(); + } + } while (pageData.hasNext()); + + Assert.assertEquals(178, loadedAuditLogs.size()); + + loadedAuditLogs = new ArrayList<>(); + pageLink = new TimePageLink(23); + do { + pageData = doGetTypedWithTimePageLink("/api/audit/logs/user/" + tenantAdmin.getId().getId().toString() + "?", + new TypeReference>() { + }, pageLink); + loadedAuditLogs.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageData.getNextPageLink(); + } + } while (pageData.hasNext()); + + Assert.assertEquals(178, loadedAuditLogs.size()); } @Test - public void testUpdateDeviceAuditLogs() throws Exception { + public void testAuditLogs_byTenantIdAndEntityId() throws Exception { Device device = new Device(); device.setName("Device name"); device.setType("default"); @@ -104,7 +134,7 @@ public abstract class BaseAuditLogControllerTest extends AbstractControllerTest TimePageLink pageLink = new TimePageLink(23); TimePageData pageData; do { - pageData = doGetTypedWithTimePageLink("/api/audit/logs/DEVICE/" + savedDevice.getId().getId() + "?", + pageData = doGetTypedWithTimePageLink("/api/audit/logs/entity/DEVICE/" + savedDevice.getId().getId() + "?", new TypeReference>() { }, pageLink); loadedAuditLogs.addAll(pageData.getData()); diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogDao.java b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogDao.java index f401ad1ac6..8562b6a5ab 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogDao.java @@ -17,7 +17,9 @@ package org.thingsboard.server.dao.audit; import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.audit.AuditLog; +import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.page.TimePageLink; import java.util.List; @@ -29,9 +31,17 @@ public interface AuditLogDao { ListenableFuture saveByTenantIdAndEntityId(AuditLog auditLog); + ListenableFuture saveByTenantIdAndCustomerId(AuditLog auditLog); + + ListenableFuture saveByTenantIdAndUserId(AuditLog auditLog); + ListenableFuture savePartitionsByTenantId(AuditLog auditLog); List findAuditLogsByTenantIdAndEntityId(UUID tenantId, EntityId entityId, TimePageLink pageLink); + List findAuditLogsByTenantIdAndCustomerId(UUID tenantId, CustomerId customerId, TimePageLink pageLink); + + List findAuditLogsByTenantIdAndUserId(UUID tenantId, UserId userId, TimePageLink pageLink); + List findAuditLogsByTenantId(UUID tenantId, TimePageLink pageLink); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogQueryCursor.java b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogQueryCursor.java new file mode 100644 index 0000000000..78b043b727 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogQueryCursor.java @@ -0,0 +1,70 @@ +/** + * Copyright © 2016-2017 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 lombok.Getter; +import org.thingsboard.server.common.data.page.TimePageLink; +import org.thingsboard.server.dao.model.nosql.AuditLogEntity; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +public class AuditLogQueryCursor { + @Getter + private final UUID tenantId; + @Getter + private final List data; + @Getter + private final TimePageLink pageLink; + + private final List partitions; + + private int partitionIndex; + private int currentLimit; + + public AuditLogQueryCursor(UUID tenantId, TimePageLink pageLink, List partitions) { + this.tenantId = tenantId; + this.partitions = partitions; + this.partitionIndex = partitions.size() - 1; + this.data = new ArrayList<>(); + this.currentLimit = pageLink.getLimit(); + this.pageLink = pageLink; + } + + public boolean hasNextPartition() { + return partitionIndex >= 0; + } + + public boolean isFull() { + return currentLimit <= 0; + } + + public long getNextPartition() { + long partition = partitions.get(partitionIndex); + partitionIndex--; + return partition; + } + + public int getCurrentLimit() { + return currentLimit; + } + + public void addData(List newData) { + currentLimit -= newData.size(); + data.addAll(newData); + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogService.java b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogService.java index 5593edee9d..d6e61abcc2 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogService.java @@ -17,6 +17,7 @@ package org.thingsboard.server.dao.audit; import com.fasterxml.jackson.databind.JsonNode; import com.google.common.util.concurrent.ListenableFuture; +import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.audit.ActionStatus; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.audit.AuditLog; @@ -31,20 +32,21 @@ import java.util.List; public interface AuditLogService { + TimePageData findAuditLogsByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, TimePageLink pageLink); + + TimePageData findAuditLogsByTenantIdAndUserId(TenantId tenantId, UserId userId, TimePageLink pageLink); + TimePageData findAuditLogsByTenantIdAndEntityId(TenantId tenantId, EntityId entityId, TimePageLink pageLink); TimePageData findAuditLogsByTenantId(TenantId tenantId, TimePageLink pageLink); - ListenableFuture> logAction(TenantId tenantId, - EntityId entityId, - String entityName, - CustomerId customerId, - UserId userId, - String userName, - ActionType actionType, - JsonNode actionData, - ActionStatus actionStatus, - String actionFailureDetails); - + ListenableFuture> logEntityAction(User user, + EntityId entityId, + String entityName, + CustomerId customerId, + ActionType actionType, + JsonNode actionData, + ActionStatus actionStatus, + String actionFailureDetails); } 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 1baf573155..1ee26e5dd8 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 @@ -15,20 +15,20 @@ */ package org.thingsboard.server.dao.audit; +import com.datastax.driver.core.utils.UUIDs; import com.fasterxml.jackson.databind.JsonNode; 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.thingsboard.server.common.data.User; 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.CustomerId; -import org.thingsboard.server.common.data.id.EntityId; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.id.*; import org.thingsboard.server.common.data.page.TimePageData; import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.dao.exception.DataValidationException; @@ -41,6 +41,7 @@ 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 String INCORRECT_TENANT_ID = "Incorrect tenantId "; @@ -49,6 +50,24 @@ public class AuditLogServiceImpl implements AuditLogService { @Autowired private AuditLogDao auditLogDao; + @Override + public TimePageData 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 auditLogs = auditLogDao.findAuditLogsByTenantIdAndCustomerId(tenantId.getId(), customerId, pageLink); + return new TimePageData<>(auditLogs, pageLink); + } + + @Override + public TimePageData 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 auditLogs = auditLogDao.findAuditLogsByTenantIdAndUserId(tenantId.getId(), userId, pageLink); + return new TimePageData<>(auditLogs, pageLink); + } + @Override public TimePageData findAuditLogsByTenantIdAndEntityId(TenantId tenantId, EntityId entityId, TimePageLink pageLink) { log.trace("Executing findAuditLogsByTenantIdAndEntityId [{}], [{}], [{}]", tenantId, entityId, pageLink); @@ -66,6 +85,28 @@ public class AuditLogServiceImpl implements AuditLogService { return new TimePageData<>(auditLogs, pageLink); } + @Override + public ListenableFuture> logEntityAction(User user, + EntityId entityId, + String entityName, + CustomerId customerId, + ActionType actionType, + JsonNode actionData, + ActionStatus actionStatus, + String actionFailureDetails) { + return logAction( + user.getTenantId(), + entityId, + entityName, + customerId, + user.getId(), + user.getName(), + actionType, + actionData, + actionStatus, + actionFailureDetails); + } + private AuditLog createAuditLogEntry(TenantId tenantId, EntityId entityId, String entityName, @@ -77,6 +118,7 @@ public class AuditLogServiceImpl implements AuditLogService { ActionStatus actionStatus, String actionFailureDetails) { AuditLog result = new AuditLog(); + result.setId(new AuditLogId(UUIDs.timeBased())); result.setTenantId(tenantId); result.setEntityId(entityId); result.setEntityName(entityName); @@ -90,17 +132,16 @@ public class AuditLogServiceImpl implements AuditLogService { return result; } - @Override - public ListenableFuture> logAction(TenantId tenantId, - EntityId entityId, - String entityName, - CustomerId customerId, - UserId userId, - String userName, - ActionType actionType, - JsonNode actionData, - ActionStatus actionStatus, - String actionFailureDetails) { + private ListenableFuture> 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); @@ -109,6 +150,8 @@ public class AuditLogServiceImpl implements AuditLogService { 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)); return Futures.allAsList(futures); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/CassandraAuditLogDao.java b/dao/src/main/java/org/thingsboard/server/dao/audit/CassandraAuditLogDao.java index 2fcb73281d..8262933711 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/audit/CassandraAuditLogDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/audit/CassandraAuditLogDao.java @@ -19,7 +19,8 @@ import com.datastax.driver.core.BoundStatement; import com.datastax.driver.core.PreparedStatement; import com.datastax.driver.core.ResultSet; import com.datastax.driver.core.ResultSetFuture; -import com.datastax.driver.core.utils.UUIDs; +import com.datastax.driver.core.querybuilder.QueryBuilder; +import com.datastax.driver.core.querybuilder.Select; import com.google.common.base.Function; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; @@ -29,8 +30,9 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.core.env.Environment; import org.springframework.stereotype.Component; 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.UserId; import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.model.ModelConstants; @@ -52,6 +54,7 @@ import java.util.Optional; import java.util.UUID; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.stream.Collectors; import static com.datastax.driver.core.querybuilder.QueryBuilder.eq; import static org.thingsboard.server.dao.model.ModelConstants.*; @@ -82,7 +85,11 @@ public class CassandraAuditLogDao extends CassandraAbstractSearchTimeDao saveByTenantId(AuditLog auditLog) { log.debug("Save saveByTenantId [{}] ", auditLog); - AuditLogId auditLogId = new AuditLogId(UUIDs.timeBased()); - long partition = toPartitionTs(LocalDate.now().atStartOfDay().toInstant(ZoneOffset.UTC).toEpochMilli()); BoundStatement stmt = getSaveByTenantStmt().bind(); - stmt.setUUID(0, auditLogId.getId()) + stmt = stmt.setUUID(0, auditLog.getId().getId()) .setUUID(1, auditLog.getTenantId().getId()) .setUUID(2, auditLog.getEntityId().getId()) .setString(3, auditLog.getEntityId().getEntityType().name()) @@ -140,17 +145,44 @@ public class CassandraAuditLogDao extends CassandraAbstractSearchTimeDao saveByTenantIdAndEntityId(AuditLog auditLog) { log.debug("Save saveByTenantIdAndEntityId [{}] ", auditLog); - AuditLogId auditLogId = new AuditLogId(UUIDs.timeBased()); - BoundStatement stmt = getSaveByTenantIdAndEntityIdStmt().bind(); - stmt.setUUID(0, auditLogId.getId()) - .setUUID(1, auditLog.getTenantId().getId()) - .setUUID(2, auditLog.getEntityId().getId()) - .setString(3, auditLog.getEntityId().getEntityType().name()) - .setString(4, auditLog.getActionType().name()); + stmt = setSaveStmtVariables(stmt, auditLog); return getFuture(executeAsyncWrite(stmt), rs -> null); } + @Override + public ListenableFuture saveByTenantIdAndCustomerId(AuditLog auditLog) { + log.debug("Save saveByTenantIdAndCustomerId [{}] ", auditLog); + + BoundStatement stmt = getSaveByTenantIdAndCustomerIdStmt().bind(); + stmt = setSaveStmtVariables(stmt, auditLog); + return getFuture(executeAsyncWrite(stmt), rs -> null); + } + + @Override + public ListenableFuture saveByTenantIdAndUserId(AuditLog auditLog) { + log.debug("Save saveByTenantIdAndUserId [{}] ", auditLog); + + BoundStatement stmt = getSaveByTenantIdAndUserIdStmt().bind(); + stmt = setSaveStmtVariables(stmt, auditLog); + return getFuture(executeAsyncWrite(stmt), rs -> null); + } + + private BoundStatement setSaveStmtVariables(BoundStatement stmt, AuditLog auditLog) { + return stmt.setUUID(0, auditLog.getId().getId()) + .setUUID(1, auditLog.getTenantId().getId()) + .setUUID(2, auditLog.getCustomerId().getId()) + .setUUID(3, auditLog.getEntityId().getId()) + .setString(4, auditLog.getEntityId().getEntityType().name()) + .setString(5, auditLog.getEntityName()) + .setUUID(6, auditLog.getUserId().getId()) + .setString(7, auditLog.getUserName()) + .setString(8, auditLog.getActionType().name()) + .setString(9, auditLog.getActionData() != null ? auditLog.getActionData().toString() : null) + .setString(10, auditLog.getActionStatus().name()) + .setString(11, auditLog.getActionFailureDetails()); + } + @Override public ListenableFuture savePartitionsByTenantId(AuditLog auditLog) { log.debug("Save savePartitionsByTenantId [{}] ", auditLog); @@ -163,35 +195,66 @@ public class CassandraAuditLogDao extends CassandraAbstractSearchTimeDao null); } - private PreparedStatement getPartitionInsertStmt() { - // TODO: ADD CACHE LOGIC - return getSession().prepare(INSERT_INTO + ModelConstants.AUDIT_LOG_BY_TENANT_ID_PARTITIONS_CF + - "(" + ModelConstants.AUDIT_LOG_TENANT_ID_PROPERTY + - "," + ModelConstants.AUDIT_LOG_PARTITION_PROPERTY + ")" + - " VALUES(?, ?)"); + private PreparedStatement getSaveByTenantIdAndEntityIdStmt() { + if (saveByTenantIdAndEntityIdStmt == null) { + saveByTenantIdAndEntityIdStmt = getSaveByTenantIdAndCFName(ModelConstants.AUDIT_LOG_BY_ENTITY_ID_CF); + } + return saveByTenantIdAndEntityIdStmt; } - private PreparedStatement getSaveByTenantIdAndEntityIdStmt() { - // TODO: ADD CACHE LOGIC - return getSession().prepare(INSERT_INTO + ModelConstants.AUDIT_LOG_BY_ENTITY_ID_CF + + private PreparedStatement getSaveByTenantIdAndCustomerIdStmt() { + if (saveByTenantIdAndCustomerIdStmt == null) { + saveByTenantIdAndCustomerIdStmt = getSaveByTenantIdAndCFName(ModelConstants.AUDIT_LOG_BY_CUSTOMER_ID_CF); + } + return saveByTenantIdAndCustomerIdStmt; + } + + private PreparedStatement getSaveByTenantIdAndUserIdStmt() { + if (saveByTenantIdAndUserIdStmt == null) { + saveByTenantIdAndUserIdStmt = getSaveByTenantIdAndCFName(ModelConstants.AUDIT_LOG_BY_USER_ID_CF); + } + return saveByTenantIdAndUserIdStmt; + } + + private PreparedStatement getSaveByTenantIdAndCFName(String cfName) { + return getSession().prepare(INSERT_INTO + cfName + "(" + ModelConstants.AUDIT_LOG_ID_PROPERTY + "," + ModelConstants.AUDIT_LOG_TENANT_ID_PROPERTY + + "," + ModelConstants.AUDIT_LOG_CUSTOMER_ID_PROPERTY + "," + ModelConstants.AUDIT_LOG_ENTITY_ID_PROPERTY + "," + ModelConstants.AUDIT_LOG_ENTITY_TYPE_PROPERTY + - "," + ModelConstants.AUDIT_LOG_ACTION_TYPE_PROPERTY + ")" + - " VALUES(?, ?, ?, ?, ?)"); + "," + ModelConstants.AUDIT_LOG_ENTITY_NAME_PROPERTY + + "," + ModelConstants.AUDIT_LOG_USER_ID_PROPERTY + + "," + ModelConstants.AUDIT_LOG_USER_NAME_PROPERTY + + "," + ModelConstants.AUDIT_LOG_ACTION_TYPE_PROPERTY + + "," + ModelConstants.AUDIT_LOG_ACTION_DATA_PROPERTY + + "," + ModelConstants.AUDIT_LOG_ACTION_STATUS_PROPERTY + + "," + ModelConstants.AUDIT_LOG_ACTION_FAILURE_DETAILS_PROPERTY + ")" + + " VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"); + } + + private PreparedStatement getPartitionInsertStmt() { + if (partitionInsertStmt == null) { + partitionInsertStmt = getSession().prepare(INSERT_INTO + ModelConstants.AUDIT_LOG_BY_TENANT_ID_PARTITIONS_CF + + "(" + ModelConstants.AUDIT_LOG_TENANT_ID_PROPERTY + + "," + ModelConstants.AUDIT_LOG_PARTITION_PROPERTY + ")" + + " VALUES(?, ?)"); + } + return partitionInsertStmt; } private PreparedStatement getSaveByTenantStmt() { - // TODO: ADD CACHE LOGIC - return getSession().prepare(INSERT_INTO + ModelConstants.AUDIT_LOG_BY_TENANT_ID_CF + - "(" + ModelConstants.AUDIT_LOG_ID_PROPERTY + - "," + ModelConstants.AUDIT_LOG_TENANT_ID_PROPERTY + - "," + ModelConstants.AUDIT_LOG_ENTITY_ID_PROPERTY + - "," + ModelConstants.AUDIT_LOG_ENTITY_TYPE_PROPERTY + - "," + ModelConstants.AUDIT_LOG_ACTION_TYPE_PROPERTY + - "," + ModelConstants.AUDIT_LOG_PARTITION_PROPERTY + ")" + - " VALUES(?, ?, ?, ?, ?, ?)"); + if (saveByTenantStmt == null) { + saveByTenantStmt = getSession().prepare(INSERT_INTO + ModelConstants.AUDIT_LOG_BY_TENANT_ID_CF + + "(" + ModelConstants.AUDIT_LOG_ID_PROPERTY + + "," + ModelConstants.AUDIT_LOG_TENANT_ID_PROPERTY + + "," + ModelConstants.AUDIT_LOG_ENTITY_ID_PROPERTY + + "," + ModelConstants.AUDIT_LOG_ENTITY_TYPE_PROPERTY + + "," + ModelConstants.AUDIT_LOG_ACTION_TYPE_PROPERTY + + "," + ModelConstants.AUDIT_LOG_PARTITION_PROPERTY + ")" + + " VALUES(?, ?, ?, ?, ?, ?)"); + } + return saveByTenantStmt; } private long toPartitionTs(long ts) { @@ -199,7 +262,6 @@ public class CassandraAuditLogDao extends CassandraAbstractSearchTimeDao findAuditLogsByTenantIdAndEntityId(UUID tenantId, EntityId entityId, TimePageLink pageLink) { log.trace("Try to find audit logs by tenant [{}], entity [{}] and pageLink [{}]", tenantId, entityId, pageLink); @@ -212,31 +274,76 @@ public class CassandraAuditLogDao extends CassandraAbstractSearchTimeDao findAuditLogsByTenantIdAndCustomerId(UUID tenantId, CustomerId customerId, TimePageLink pageLink) { + log.trace("Try to find audit logs by tenant [{}], customer [{}] and pageLink [{}]", tenantId, customerId, pageLink); + List entities = findPageWithTimeSearch(AUDIT_LOG_BY_CUSTOMER_ID_CF, + Arrays.asList(eq(ModelConstants.AUDIT_LOG_TENANT_ID_PROPERTY, tenantId), + eq(ModelConstants.AUDIT_LOG_CUSTOMER_ID_PROPERTY, customerId.getId())), + pageLink); + log.trace("Found audit logs by tenant [{}], customer [{}] and pageLink [{}]", tenantId, customerId, pageLink); + return DaoUtil.convertDataList(entities); + } + + @Override + public List findAuditLogsByTenantIdAndUserId(UUID tenantId, UserId userId, TimePageLink pageLink) { + log.trace("Try to find audit logs by tenant [{}], user [{}] and pageLink [{}]", tenantId, userId, pageLink); + List entities = findPageWithTimeSearch(AUDIT_LOG_BY_USER_ID_CF, + Arrays.asList(eq(ModelConstants.AUDIT_LOG_TENANT_ID_PROPERTY, tenantId), + eq(ModelConstants.AUDIT_LOG_USER_ID_PROPERTY, userId.getId())), + pageLink); + log.trace("Found audit logs by tenant [{}], user [{}] and pageLink [{}]", tenantId, userId, pageLink); + return DaoUtil.convertDataList(entities); + } + @Override public List findAuditLogsByTenantId(UUID tenantId, TimePageLink pageLink) { log.trace("Try to find audit logs by tenant [{}] and pageLink [{}]", tenantId, pageLink); - // TODO: ADD AUDIT LOG PARTITION CURSOR LOGIC - long minPartition; - long maxPartition; - if (pageLink.getStartTime() != null && pageLink.getStartTime() != 0) { minPartition = toPartitionTs(pageLink.getStartTime()); } else { minPartition = toPartitionTs(LocalDate.now().minusMonths(1).atStartOfDay().toInstant(ZoneOffset.UTC).toEpochMilli()); } + long maxPartition; if (pageLink.getEndTime() != null && pageLink.getEndTime() != 0) { maxPartition = toPartitionTs(pageLink.getEndTime()); } else { maxPartition = toPartitionTs(LocalDate.now().atStartOfDay().toInstant(ZoneOffset.UTC).toEpochMilli()); } - List entities = findPageWithTimeSearch(AUDIT_LOG_BY_TENANT_ID_CF, - Arrays.asList(eq(ModelConstants.AUDIT_LOG_TENANT_ID_PROPERTY, tenantId), - eq(ModelConstants.AUDIT_LOG_PARTITION_PROPERTY, maxPartition)), - pageLink); + + List partitions = fetchPartitions(tenantId, minPartition, maxPartition) + .all() + .stream() + .map(row -> row.getLong(ModelConstants.PARTITION_COLUMN)) + .collect(Collectors.toList()); + + AuditLogQueryCursor cursor = new AuditLogQueryCursor(tenantId, pageLink, partitions); + List entities = fetchSequentiallyWithLimit(cursor); log.trace("Found audit logs by tenant [{}] and pageLink [{}]", tenantId, pageLink); return DaoUtil.convertDataList(entities); } + + private List fetchSequentiallyWithLimit(AuditLogQueryCursor cursor) { + if (cursor.isFull() || !cursor.hasNextPartition()) { + return cursor.getData(); + } else { + cursor.addData(findPageWithTimeSearch(AUDIT_LOG_BY_TENANT_ID_CF, + Arrays.asList(eq(ModelConstants.AUDIT_LOG_TENANT_ID_PROPERTY, cursor.getTenantId()), + eq(ModelConstants.AUDIT_LOG_PARTITION_PROPERTY, cursor.getNextPartition())), + cursor.getPageLink())); + return fetchSequentiallyWithLimit(cursor); + } + } + + private ResultSet fetchPartitions(UUID tenantId, long minPartition, long maxPartition) { + Select.Where select = QueryBuilder.select(ModelConstants.AUDIT_LOG_PARTITION_PROPERTY).from(ModelConstants.AUDIT_LOG_BY_TENANT_ID_PARTITIONS_CF) + .where(eq(ModelConstants.AUDIT_LOG_TENANT_ID_PROPERTY, tenantId)); + select.and(QueryBuilder.gte(ModelConstants.PARTITION_COLUMN, minPartition)); + select.and(QueryBuilder.lte(ModelConstants.PARTITION_COLUMN, maxPartition)); + return getSession().execute(select); + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/DummyAuditLogServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/audit/DummyAuditLogServiceImpl.java new file mode 100644 index 0000000000..c6107694e1 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/audit/DummyAuditLogServiceImpl.java @@ -0,0 +1,61 @@ +/** + * Copyright © 2016-2017 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.fasterxml.jackson.databind.JsonNode; +import com.google.common.util.concurrent.ListenableFuture; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.thingsboard.server.common.data.User; +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.CustomerId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.page.TimePageData; +import org.thingsboard.server.common.data.page.TimePageLink; + +import java.util.List; + +@ConditionalOnProperty(prefix = "audit_log", value = "enabled", havingValue = "false") +public class DummyAuditLogServiceImpl implements AuditLogService { + + @Override + public TimePageData findAuditLogsByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, TimePageLink pageLink) { + return null; + } + + @Override + public TimePageData findAuditLogsByTenantIdAndUserId(TenantId tenantId, UserId userId, TimePageLink pageLink) { + return null; + } + + @Override + public TimePageData findAuditLogsByTenantIdAndEntityId(TenantId tenantId, EntityId entityId, TimePageLink pageLink) { + return null; + } + + @Override + public TimePageData findAuditLogsByTenantId(TenantId tenantId, TimePageLink pageLink) { + return null; + } + + @Override + public ListenableFuture> logEntityAction(User user, EntityId entityId, String entityName, CustomerId customerId, ActionType actionType, JsonNode actionData, ActionStatus actionStatus, String actionFailureDetails) { + return null; + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java index 58c8d2f1f7..66e03b0629 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java @@ -147,6 +147,8 @@ public class ModelConstants { public static final String AUDIT_LOG_COLUMN_FAMILY_NAME = "audit_log"; public static final String AUDIT_LOG_BY_ENTITY_ID_CF = "audit_log_by_entity_id"; + public static final String AUDIT_LOG_BY_CUSTOMER_ID_CF = "audit_log_by_customer_id"; + public static final String AUDIT_LOG_BY_USER_ID_CF = "audit_log_by_user_id"; public static final String AUDIT_LOG_BY_TENANT_ID_CF = "audit_log_by_tenant_id"; public static final String AUDIT_LOG_BY_TENANT_ID_PARTITIONS_CF = "audit_log_by_tenant_id_partitions"; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/audit/AuditLogRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/audit/AuditLogRepository.java index 7e4fdc1ce2..bac44c5121 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/audit/AuditLogRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/audit/AuditLogRepository.java @@ -41,4 +41,20 @@ public interface AuditLogRepository extends CrudRepository :idOffset ORDER BY al.id") + List findByTenantIdAndCustomerId(@Param("tenantId") String tenantId, + @Param("customerId") String customerId, + @Param("idOffset") String idOffset, + Pageable pageable); + + @Query("SELECT al FROM AuditLogEntity al WHERE al.tenantId = :tenantId " + + "AND al.userId = :userId " + + "AND al.id > :idOffset ORDER BY al.id") + List findByTenantIdAndUserId(@Param("tenantId") String tenantId, + @Param("userId") String userId, + @Param("idOffset") String idOffset, + Pageable pageable); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/audit/JpaAuditLogDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/audit/JpaAuditLogDao.java index fe7edf8d95..b4abf07416 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/audit/JpaAuditLogDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/audit/JpaAuditLogDao.java @@ -23,7 +23,9 @@ import org.springframework.data.domain.PageRequest; import org.springframework.data.repository.CrudRepository; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.audit.AuditLog; +import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.audit.AuditLogDao; @@ -76,6 +78,16 @@ public class JpaAuditLogDao extends JpaAbstractDao imp return insertService.submit(() -> null); } + @Override + public ListenableFuture saveByTenantIdAndCustomerId(AuditLog auditLog) { + return insertService.submit(() -> null); + } + + @Override + public ListenableFuture saveByTenantIdAndUserId(AuditLog auditLog) { + return insertService.submit(() -> null); + } + @Override public ListenableFuture savePartitionsByTenantId(AuditLog auditLog) { return insertService.submit(() -> null); @@ -92,6 +104,26 @@ public class JpaAuditLogDao extends JpaAbstractDao imp new PageRequest(0, pageLink.getLimit()))); } + @Override + public List findAuditLogsByTenantIdAndCustomerId(UUID tenantId, CustomerId customerId, TimePageLink pageLink) { + return DaoUtil.convertDataList( + auditLogRepository.findByTenantIdAndCustomerId( + fromTimeUUID(tenantId), + fromTimeUUID(customerId.getId()), + pageLink.getIdOffset() == null ? NULL_UUID_STR : fromTimeUUID(pageLink.getIdOffset()), + new PageRequest(0, pageLink.getLimit()))); + } + + @Override + public List findAuditLogsByTenantIdAndUserId(UUID tenantId, UserId userId, TimePageLink pageLink) { + return DaoUtil.convertDataList( + auditLogRepository.findByTenantIdAndUserId( + fromTimeUUID(tenantId), + fromTimeUUID(userId.getId()), + pageLink.getIdOffset() == null ? NULL_UUID_STR : fromTimeUUID(pageLink.getIdOffset()), + new PageRequest(0, pageLink.getLimit()))); + } + @Override public List findAuditLogsByTenantId(UUID tenantId, TimePageLink pageLink) { return DaoUtil.convertDataList( diff --git a/dao/src/main/resources/cassandra/schema.cql b/dao/src/main/resources/cassandra/schema.cql index 6dc2becca9..bc7884dfbe 100644 --- a/dao/src/main/resources/cassandra/schema.cql +++ b/dao/src/main/resources/cassandra/schema.cql @@ -566,6 +566,40 @@ CREATE TABLE IF NOT EXISTS thingsboard.audit_log_by_entity_id ( PRIMARY KEY ((tenant_id, entity_id, entity_type), id) ); +CREATE TABLE IF NOT EXISTS thingsboard.audit_log_by_customer_id ( + tenant_id timeuuid, + id timeuuid, + customer_id timeuuid, + entity_id timeuuid, + entity_type text, + entity_name text, + user_id timeuuid, + user_name text, + action_type text, + action_data text, + action_status text, + action_failure_details text, + PRIMARY KEY ((tenant_id, customer_id), id) +); + +CREATE TABLE IF NOT EXISTS thingsboard.audit_log_by_user_id ( + tenant_id timeuuid, + id timeuuid, + customer_id timeuuid, + entity_id timeuuid, + entity_type text, + entity_name text, + user_id timeuuid, + user_name text, + action_type text, + action_data text, + action_status text, + action_failure_details text, + PRIMARY KEY ((tenant_id, user_id), id) +); + + + CREATE TABLE IF NOT EXISTS thingsboard.audit_log_by_tenant_id ( tenant_id timeuuid, id timeuuid, diff --git a/dao/src/test/resources/application-test.properties b/dao/src/test/resources/application-test.properties index d87a181d0a..cbca287d66 100644 --- a/dao/src/test/resources/application-test.properties +++ b/dao/src/test/resources/application-test.properties @@ -7,4 +7,6 @@ zk.enabled=false zk.url=localhost:2181 zk.zk_dir=/thingsboard -updates.enabled=false \ No newline at end of file +updates.enabled=false + +audit_log.enabled=true \ No newline at end of file