Added get by user id and customer id

Improved partitions query
This commit is contained in:
Volodymyr Babak 2018-02-14 19:51:23 +02:00
parent ead30d700b
commit 23b21b2912
15 changed files with 562 additions and 102 deletions

View File

@ -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<AuditLog> getAuditLogs(
public TimePageData<AuditLog> 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<AuditLog> 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<AuditLog> getAuditLogsByEntityId(
@PathVariable("entityType") String strEntityType,
@PathVariable("entityId") String strEntityId,
@RequestParam int limit,

View File

@ -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);
}
}

View File

@ -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}"

View File

@ -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<TimePageData<AuditLog>>() {
}, 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<TimePageData<AuditLog>>() {
}, 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<AuditLog> pageData;
do {
pageData = doGetTypedWithTimePageLink("/api/audit/logs/DEVICE/" + savedDevice.getId().getId() + "?",
pageData = doGetTypedWithTimePageLink("/api/audit/logs/entity/DEVICE/" + savedDevice.getId().getId() + "?",
new TypeReference<TimePageData<AuditLog>>() {
}, pageLink);
loadedAuditLogs.addAll(pageData.getData());

View File

@ -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<Void> saveByTenantIdAndEntityId(AuditLog auditLog);
ListenableFuture<Void> saveByTenantIdAndCustomerId(AuditLog auditLog);
ListenableFuture<Void> saveByTenantIdAndUserId(AuditLog auditLog);
ListenableFuture<Void> savePartitionsByTenantId(AuditLog auditLog);
List<AuditLog> findAuditLogsByTenantIdAndEntityId(UUID tenantId, EntityId entityId, TimePageLink pageLink);
List<AuditLog> findAuditLogsByTenantIdAndCustomerId(UUID tenantId, CustomerId customerId, TimePageLink pageLink);
List<AuditLog> findAuditLogsByTenantIdAndUserId(UUID tenantId, UserId userId, TimePageLink pageLink);
List<AuditLog> findAuditLogsByTenantId(UUID tenantId, TimePageLink pageLink);
}

View File

@ -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<AuditLogEntity> data;
@Getter
private final TimePageLink pageLink;
private final List<Long> partitions;
private int partitionIndex;
private int currentLimit;
public AuditLogQueryCursor(UUID tenantId, TimePageLink pageLink, List<Long> 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<AuditLogEntity> newData) {
currentLimit -= newData.size();
data.addAll(newData);
}
}

View File

@ -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<AuditLog> findAuditLogsByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, TimePageLink pageLink);
TimePageData<AuditLog> findAuditLogsByTenantIdAndUserId(TenantId tenantId, UserId userId, TimePageLink pageLink);
TimePageData<AuditLog> findAuditLogsByTenantIdAndEntityId(TenantId tenantId, EntityId entityId, TimePageLink pageLink);
TimePageData<AuditLog> findAuditLogsByTenantId(TenantId tenantId, TimePageLink pageLink);
ListenableFuture<List<Void>> logAction(TenantId tenantId,
EntityId entityId,
String entityName,
CustomerId customerId,
UserId userId,
String userName,
ActionType actionType,
JsonNode actionData,
ActionStatus actionStatus,
String actionFailureDetails);
ListenableFuture<List<Void>> logEntityAction(User user,
EntityId entityId,
String entityName,
CustomerId customerId,
ActionType actionType,
JsonNode actionData,
ActionStatus actionStatus,
String actionFailureDetails);
}

View File

@ -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<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);
@ -66,6 +85,28 @@ public class AuditLogServiceImpl implements AuditLogService {
return new TimePageData<>(auditLogs, pageLink);
}
@Override
public ListenableFuture<List<Void>> 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<List<Void>> logAction(TenantId tenantId,
EntityId entityId,
String entityName,
CustomerId customerId,
UserId userId,
String userName,
ActionType actionType,
JsonNode actionData,
ActionStatus actionStatus,
String actionFailureDetails) {
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);
@ -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);
}

View File

@ -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<AuditLo
private String partitioning;
private TsPartitionDate tsFormat;
private PreparedStatement[] saveStmts;
private PreparedStatement partitionInsertStmt;
private PreparedStatement saveByTenantStmt;
private PreparedStatement saveByTenantIdAndUserIdStmt;
private PreparedStatement saveByTenantIdAndEntityIdStmt;
private PreparedStatement saveByTenantIdAndCustomerIdStmt;
private boolean isInstall() {
return environment.acceptsProfiles("install");
@ -123,11 +130,9 @@ public class CassandraAuditLogDao extends CassandraAbstractSearchTimeDao<AuditLo
public ListenableFuture<Void> 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<AuditLo
public ListenableFuture<Void> 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<Void> 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<Void> 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<Void> savePartitionsByTenantId(AuditLog auditLog) {
log.debug("Save savePartitionsByTenantId [{}] ", auditLog);
@ -163,35 +195,66 @@ public class CassandraAuditLogDao extends CassandraAbstractSearchTimeDao<AuditLo
return getFuture(executeAsyncWrite(stmt), rs -> 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<AuditLo
return tsFormat.truncatedTo(time).toInstant(ZoneOffset.UTC).toEpochMilli();
}
@Override
public List<AuditLog> 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<AuditLo
return DaoUtil.convertDataList(entities);
}
@Override
public List<AuditLog> findAuditLogsByTenantIdAndCustomerId(UUID tenantId, CustomerId customerId, TimePageLink pageLink) {
log.trace("Try to find audit logs by tenant [{}], customer [{}] and pageLink [{}]", tenantId, customerId, pageLink);
List<AuditLogEntity> 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<AuditLog> findAuditLogsByTenantIdAndUserId(UUID tenantId, UserId userId, TimePageLink pageLink) {
log.trace("Try to find audit logs by tenant [{}], user [{}] and pageLink [{}]", tenantId, userId, pageLink);
List<AuditLogEntity> 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<AuditLog> 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<AuditLogEntity> 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<Long> 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<AuditLogEntity> entities = fetchSequentiallyWithLimit(cursor);
log.trace("Found audit logs by tenant [{}] and pageLink [{}]", tenantId, pageLink);
return DaoUtil.convertDataList(entities);
}
private List<AuditLogEntity> 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);
}
}

View File

@ -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<AuditLog> findAuditLogsByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, TimePageLink pageLink) {
return null;
}
@Override
public TimePageData<AuditLog> findAuditLogsByTenantIdAndUserId(TenantId tenantId, UserId userId, TimePageLink pageLink) {
return null;
}
@Override
public TimePageData<AuditLog> findAuditLogsByTenantIdAndEntityId(TenantId tenantId, EntityId entityId, TimePageLink pageLink) {
return null;
}
@Override
public TimePageData<AuditLog> findAuditLogsByTenantId(TenantId tenantId, TimePageLink pageLink) {
return null;
}
@Override
public ListenableFuture<List<Void>> logEntityAction(User user, EntityId entityId, String entityName, CustomerId customerId, ActionType actionType, JsonNode actionData, ActionStatus actionStatus, String actionFailureDetails) {
return null;
}
}

View File

@ -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";

View File

@ -41,4 +41,20 @@ public interface AuditLogRepository extends CrudRepository<AuditLogEntity, Strin
@Param("entityType") EntityType entityType,
@Param("idOffset") String idOffset,
Pageable pageable);
@Query("SELECT al FROM AuditLogEntity al WHERE al.tenantId = :tenantId " +
"AND al.customerId = :customerId " +
"AND al.id > :idOffset ORDER BY al.id")
List<AuditLogEntity> 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<AuditLogEntity> findByTenantIdAndUserId(@Param("tenantId") String tenantId,
@Param("userId") String userId,
@Param("idOffset") String idOffset,
Pageable pageable);
}

View File

@ -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<AuditLogEntity, AuditLog> imp
return insertService.submit(() -> null);
}
@Override
public ListenableFuture<Void> saveByTenantIdAndCustomerId(AuditLog auditLog) {
return insertService.submit(() -> null);
}
@Override
public ListenableFuture<Void> saveByTenantIdAndUserId(AuditLog auditLog) {
return insertService.submit(() -> null);
}
@Override
public ListenableFuture<Void> savePartitionsByTenantId(AuditLog auditLog) {
return insertService.submit(() -> null);
@ -92,6 +104,26 @@ public class JpaAuditLogDao extends JpaAbstractDao<AuditLogEntity, AuditLog> imp
new PageRequest(0, pageLink.getLimit())));
}
@Override
public List<AuditLog> 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<AuditLog> 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<AuditLog> findAuditLogsByTenantId(UUID tenantId, TimePageLink pageLink) {
return DaoUtil.convertDataList(

View File

@ -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,

View File

@ -7,4 +7,6 @@ zk.enabled=false
zk.url=localhost:2181
zk.zk_dir=/thingsboard
updates.enabled=false
updates.enabled=false
audit_log.enabled=true