Implement Audit Logs

This commit is contained in:
Igor Kulikov 2018-02-21 20:05:03 +02:00
parent 2a74985e4b
commit a5f44729e5
74 changed files with 2280 additions and 269 deletions

View File

@ -24,8 +24,8 @@ CREATE TABLE IF NOT EXISTS audit_log (
user_id varchar(31),
user_name varchar(255),
action_type varchar(255),
action_data varchar(255),
action_data varchar(1000000),
action_status varchar(255),
action_failure_details varchar
action_failure_details varchar(1000000)
);

View File

@ -40,6 +40,7 @@ import org.thingsboard.server.controller.plugin.PluginWebSocketMsgEndpoint;
import org.thingsboard.server.dao.alarm.AlarmService;
import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.audit.AuditLogService;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.event.EventService;
@ -113,6 +114,9 @@ public class ActorSystemContext {
@Autowired
@Getter private RelationService relationService;
@Autowired
@Getter private AuditLogService auditLogService;
@Autowired
@Getter @Setter private PluginWebSocketMsgEndpoint wsMsgEndpoint;

View File

@ -26,6 +26,7 @@ import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.id.*;
import org.thingsboard.server.common.data.kv.AttributeKey;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
@ -41,9 +42,7 @@ import org.thingsboard.server.extensions.api.device.DeviceAttributesEventNotific
import org.thingsboard.server.extensions.api.plugins.PluginApiCallSecurityContext;
import org.thingsboard.server.extensions.api.plugins.PluginCallback;
import org.thingsboard.server.extensions.api.plugins.PluginContext;
import org.thingsboard.server.extensions.api.plugins.msg.PluginToRuleMsg;
import org.thingsboard.server.extensions.api.plugins.msg.TimeoutMsg;
import org.thingsboard.server.extensions.api.plugins.msg.ToDeviceRpcRequest;
import org.thingsboard.server.extensions.api.plugins.msg.*;
import org.thingsboard.server.extensions.api.plugins.rpc.PluginRpcMsg;
import org.thingsboard.server.extensions.api.plugins.rpc.RpcMsg;
import org.thingsboard.server.extensions.api.plugins.ws.PluginWebsocketSessionRef;
@ -196,6 +195,52 @@ public final class PluginProcessingContext implements PluginContext {
}));
}
@Override
public void logAttributesUpdated(PluginApiCallSecurityContext ctx, EntityId entityId, String attributeType,
List<AttributeKvEntry> attributes, Exception e) {
pluginCtx.auditLogService.logEntityAction(
ctx.getTenantId(),
ctx.getCustomerId(),
ctx.getUserId(),
ctx.getUserName(),
(UUIDBased & EntityId)entityId,
null,
ActionType.ATTRIBUTES_UPDATED,
e,
attributeType,
attributes);
}
@Override
public void logAttributesDeleted(PluginApiCallSecurityContext ctx, EntityId entityId, String attributeType, List<String> keys, Exception e) {
pluginCtx.auditLogService.logEntityAction(
ctx.getTenantId(),
ctx.getCustomerId(),
ctx.getUserId(),
ctx.getUserName(),
(UUIDBased & EntityId)entityId,
null,
ActionType.ATTRIBUTES_DELETED,
e,
attributeType,
keys);
}
@Override
public void logAttributesRead(PluginApiCallSecurityContext ctx, EntityId entityId, String attributeType, List<String> keys, Exception e) {
pluginCtx.auditLogService.logEntityAction(
ctx.getTenantId(),
ctx.getCustomerId(),
ctx.getUserId(),
ctx.getUserName(),
(UUIDBased & EntityId)entityId,
null,
ActionType.ATTRIBUTES_READ,
e,
attributeType,
keys);
}
@Override
public void loadLatestTimeseries(final EntityId entityId, final Collection<String> keys, final PluginCallback<List<TsKvEntry>> callback) {
validate(entityId, new ValidationCallback(callback, ctx -> {
@ -460,6 +505,29 @@ public final class PluginProcessingContext implements PluginContext {
pluginCtx.sendRpcRequest(msg);
}
@Override
public void logRpcRequest(PluginApiCallSecurityContext ctx, DeviceId deviceId, ToDeviceRpcRequestBody body, boolean oneWay, Optional<RpcError> rpcError, Exception e) {
String rpcErrorStr = "";
if (rpcError.isPresent()) {
rpcErrorStr = "RPC Error: " + rpcError.get().name();
}
String method = body.getMethod();
String params = body.getParams();
pluginCtx.auditLogService.logEntityAction(
ctx.getTenantId(),
ctx.getCustomerId(),
ctx.getUserId(),
ctx.getUserName(),
deviceId,
null,
ActionType.RPC_CALL,
e,
rpcErrorStr,
new Boolean(oneWay),
method,
params);
}
@Override
public void scheduleTimeoutMsg(TimeoutMsg msg) {
pluginCtx.scheduleTimeoutMsg(msg);

View File

@ -27,6 +27,7 @@ import org.thingsboard.server.controller.plugin.PluginWebSocketMsgEndpoint;
import org.thingsboard.server.common.data.id.PluginId;
import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.audit.AuditLogService;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.plugin.PluginService;
@ -63,6 +64,7 @@ public final class SharedPluginProcessingContext {
final ClusterRpcService rpcService;
final ClusterRoutingService routingService;
final RelationService relationService;
final AuditLogService auditLogService;
final PluginId pluginId;
final TenantId tenantId;
@ -86,6 +88,7 @@ public final class SharedPluginProcessingContext {
this.customerService = sysContext.getCustomerService();
this.tenantService = sysContext.getTenantService();
this.relationService = sysContext.getRelationService();
this.auditLogService = sysContext.getAuditLogService();
}
public PluginId getPluginId() {

View File

@ -148,7 +148,7 @@ public class BasicRpcSessionListener implements GrpcSessionListener {
DeviceId deviceId = new DeviceId(toUUID(msg.getDeviceId()));
ToDeviceRpcRequestBody requestBody = new ToDeviceRpcRequestBody(msg.getMethod(), msg.getParams());
ToDeviceRpcRequest request = new ToDeviceRpcRequest(toUUID(msg.getMsgId()), deviceTenantId, deviceId, msg.getOneway(), msg.getExpTime(), requestBody);
ToDeviceRpcRequest request = new ToDeviceRpcRequest(toUUID(msg.getMsgId()), null, deviceTenantId, deviceId, msg.getOneway(), msg.getExpTime(), requestBody);
return new ToDeviceRpcRequestPluginMsg(serverAddress, pluginId, pluginTenantId, request);
}

View File

@ -0,0 +1,43 @@
/**
* 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.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.audit.ActionType;
import java.util.HashMap;
import java.util.Map;
@Configuration
@ConfigurationProperties(prefix = "audit_log.logging_level")
public class AuditLogLevelProperties {
private Map<String, String> mask = new HashMap<>();
public AuditLogLevelProperties() {
super();
}
public void setMask(Map<String, String> mask) {
this.mask = mask;
}
public Map<String, String> getMask() {
return this.mask;
}
}

View File

@ -40,6 +40,7 @@ import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.web.cors.CorsUtils;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.thingsboard.server.dao.audit.AuditLogLevelFilter;
import org.thingsboard.server.exception.ThingsboardErrorResponseHandler;
import org.thingsboard.server.service.security.auth.rest.RestAuthenticationProvider;
import org.thingsboard.server.service.security.auth.rest.RestLoginProcessingFilter;
@ -198,4 +199,9 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt
return new CorsFilter(source);
}
}
@Bean
public AuditLogLevelFilter auditLogLevelFilter(@Autowired AuditLogLevelProperties auditLogLevelProperties) {
return new AuditLogLevelFilter(auditLogLevelProperties.getMask());
}
}

View File

@ -21,7 +21,9 @@ import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId;
@ -73,8 +75,16 @@ public class AssetController extends BaseController {
checkCustomerId(asset.getCustomerId());
}
}
return checkNotNull(assetService.saveAsset(asset));
Asset savedAsset = checkNotNull(assetService.saveAsset(asset));
logEntityAction(savedAsset.getId(), savedAsset,
savedAsset.getCustomerId(),
asset.getId() == null ? ActionType.ADDED : ActionType.UPDATED, null);
return savedAsset;
} catch (Exception e) {
logEntityAction(emptyId(EntityType.ASSET), asset,
null, asset.getId() == null ? ActionType.ADDED : ActionType.UPDATED, e);
throw handleException(e);
}
}
@ -86,9 +96,18 @@ public class AssetController extends BaseController {
checkParameter(ASSET_ID, strAssetId);
try {
AssetId assetId = new AssetId(toUUID(strAssetId));
checkAssetId(assetId);
Asset asset = checkAssetId(assetId);
assetService.deleteAsset(assetId);
logEntityAction(assetId, asset,
asset.getCustomerId(),
ActionType.DELETED, null, strAssetId);
} catch (Exception e) {
logEntityAction(emptyId(EntityType.ASSET),
null,
null,
ActionType.DELETED, e, strAssetId);
throw handleException(e);
}
}
@ -102,13 +121,24 @@ public class AssetController extends BaseController {
checkParameter(ASSET_ID, strAssetId);
try {
CustomerId customerId = new CustomerId(toUUID(strCustomerId));
checkCustomerId(customerId);
Customer customer = checkCustomerId(customerId);
AssetId assetId = new AssetId(toUUID(strAssetId));
checkAssetId(assetId);
return checkNotNull(assetService.assignAssetToCustomer(assetId, customerId));
Asset savedAsset = checkNotNull(assetService.assignAssetToCustomer(assetId, customerId));
logEntityAction(assetId, savedAsset,
savedAsset.getCustomerId(),
ActionType.ASSIGNED_TO_CUSTOMER, null, strAssetId, strCustomerId, customer.getName());
return savedAsset;
} catch (Exception e) {
logEntityAction(emptyId(EntityType.ASSET), null,
null,
ActionType.ASSIGNED_TO_CUSTOMER, e, strAssetId, strCustomerId);
throw handleException(e);
}
}
@ -124,8 +154,22 @@ public class AssetController extends BaseController {
if (asset.getCustomerId() == null || asset.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) {
throw new IncorrectParameterException("Asset isn't assigned to any customer!");
}
return checkNotNull(assetService.unassignAssetFromCustomer(assetId));
Customer customer = checkCustomerId(asset.getCustomerId());
Asset savedAsset = checkNotNull(assetService.unassignAssetFromCustomer(assetId));
logEntityAction(assetId, asset,
asset.getCustomerId(),
ActionType.UNASSIGNED_FROM_CUSTOMER, null, strAssetId, customer.getId().toString(), customer.getName());
return savedAsset;
} catch (Exception e) {
logEntityAction(emptyId(EntityType.ASSET), null,
null,
ActionType.UNASSIGNED_FROM_CUSTOMER, e, strAssetId);
throw handleException(e);
}
}
@ -139,8 +183,19 @@ public class AssetController extends BaseController {
AssetId assetId = new AssetId(toUUID(strAssetId));
Asset asset = checkAssetId(assetId);
Customer publicCustomer = customerService.findOrCreatePublicCustomer(asset.getTenantId());
return checkNotNull(assetService.assignAssetToCustomer(assetId, publicCustomer.getId()));
Asset savedAsset = checkNotNull(assetService.assignAssetToCustomer(assetId, publicCustomer.getId()));
logEntityAction(assetId, savedAsset,
savedAsset.getCustomerId(),
ActionType.ASSIGNED_TO_CUSTOMER, null, strAssetId, publicCustomer.getId().toString(), publicCustomer.getName());
return savedAsset;
} catch (Exception e) {
logEntityAction(emptyId(EntityType.ASSET), null,
null,
ActionType.ASSIGNED_TO_CUSTOMER, e, strAssetId);
throw handleException(e);
}
}

View File

@ -79,9 +79,6 @@ public abstract class BaseController {
public static final String INCORRECT_TENANT_ID = "Incorrect tenantId ";
public static final String YOU_DON_T_HAVE_PERMISSION_TO_PERFORM_THIS_OPERATION = "You don't have permission to perform this operation!";
@Value("${audit_log.exceptions.enabled}")
private boolean auditLogExceptionsEnabled;
@Autowired
private ThingsboardErrorResponseHandler errorResponseHandler;
@ -130,11 +127,6 @@ public abstract class BaseController {
@Autowired
protected AuditLogService auditLogService;
@ExceptionHandler(Exception.class)
public void handleException(Exception ex, HttpServletResponse response) {
errorResponseHandler.handle(ex, response);
}
@ExceptionHandler(ThingsboardException.class)
public void handleThingsboardException(ThingsboardException ex, HttpServletResponse response) {
errorResponseHandler.handle(ex, response);
@ -144,11 +136,6 @@ public abstract class BaseController {
return handleException(exception, true);
}
ThingsboardException handleException(Exception exception, ActionType actionType, String actionData) {
logExceptionToAuditLog(exception, actionType, actionData);
return handleException(exception, true);
}
private ThingsboardException handleException(Exception exception, boolean logException) {
if (logException) {
log.error("Error [{}]", exception.getMessage());
@ -171,36 +158,6 @@ public abstract class BaseController {
}
}
private void logExceptionToAuditLog(Exception exception, ActionType actionType, String actionData) {
try {
if (auditLogExceptionsEnabled) {
SecurityUser currentUser = getCurrentUser();
EntityId entityId;
CustomerId customerId;
if (!currentUser.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) {
entityId = currentUser.getCustomerId();
customerId = currentUser.getCustomerId();
} else {
entityId = currentUser.getTenantId();
customerId = new CustomerId(ModelConstants.NULL_UUID);
}
JsonNode actionDataNode = new ObjectMapper().createObjectNode().put("actionData", actionData);
auditLogService.logEntityAction(currentUser,
entityId,
null,
customerId,
actionType,
actionDataNode,
ActionStatus.FAILURE,
exception.getMessage());
}
} catch (Exception e) {
log.error("Exception happend during saving to audit log", e);
}
}
<T> T checkNotNull(T reference) throws ThingsboardException {
if (reference == null) {
throw new ThingsboardException("Requested item wasn't found!", ThingsboardErrorCode.ITEM_NOT_FOUND);
@ -594,23 +551,19 @@ public abstract class BaseController {
return baseUrl;
}
protected void logEntityDeleted(EntityId entityId, String entityName, CustomerId customerId) throws ThingsboardException {
logEntitySuccess(entityId, entityName, customerId, ActionType.DELETED);
protected <I extends UUIDBased & EntityId> I emptyId(EntityType entityType) {
return (I)EntityIdFactory.getByTypeAndUuid(entityType, ModelConstants.NULL_UUID);
}
protected void logEntityAddedOrUpdated(EntityId entityId, String entityName, CustomerId customerId, boolean isAddAction) throws ThingsboardException {
logEntitySuccess(entityId, entityName, customerId, isAddAction ? ActionType.ADDED : ActionType.UPDATED);
protected <E extends BaseData<I> & HasName,
I extends UUIDBased & EntityId> void logEntityAction(I entityId, E entity, CustomerId customerId,
ActionType actionType, Exception e, Object... additionalInfo) throws ThingsboardException {
User user = getCurrentUser();
if (customerId == null || customerId.isNullUid()) {
customerId = user.getCustomerId();
}
auditLogService.logEntityAction(user.getTenantId(), customerId, user.getId(), user.getName(), entityId, entity, actionType, e, additionalInfo);
}
protected void logEntitySuccess(EntityId entityId, String entityName, CustomerId customerId, ActionType actionType) throws ThingsboardException {
auditLogService.logEntityAction(
getCurrentUser(),
entityId,
entityName,
customerId,
actionType,
null,
ActionStatus.SUCCESS,
null);
}
}

View File

@ -22,6 +22,8 @@ import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.TextPageData;
@ -86,8 +88,18 @@ public class CustomerController extends BaseController {
public Customer saveCustomer(@RequestBody Customer customer) throws ThingsboardException {
try {
customer.setTenantId(getCurrentUser().getTenantId());
return checkNotNull(customerService.saveCustomer(customer));
Customer savedCustomer = checkNotNull(customerService.saveCustomer(customer));
logEntityAction(savedCustomer.getId(), savedCustomer,
savedCustomer.getId(),
customer.getId() == null ? ActionType.ADDED : ActionType.UPDATED, null);
return savedCustomer;
} catch (Exception e) {
logEntityAction(emptyId(EntityType.CUSTOMER), customer,
null, customer.getId() == null ? ActionType.ADDED : ActionType.UPDATED, e);
throw handleException(e);
}
}
@ -99,9 +111,20 @@ public class CustomerController extends BaseController {
checkParameter(CUSTOMER_ID, strCustomerId);
try {
CustomerId customerId = new CustomerId(toUUID(strCustomerId));
checkCustomerId(customerId);
Customer customer = checkCustomerId(customerId);
customerService.deleteCustomer(customerId);
logEntityAction(customerId, customer,
customer.getId(),
ActionType.DELETED, null, strCustomerId);
} catch (Exception e) {
logEntityAction(emptyId(EntityType.CUSTOMER),
null,
null,
ActionType.DELETED, e, strCustomerId);
throw handleException(e);
}
}

View File

@ -21,6 +21,8 @@ import org.springframework.web.bind.annotation.*;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.common.data.DashboardInfo;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.TenantId;
@ -75,8 +77,17 @@ public class DashboardController extends BaseController {
public Dashboard saveDashboard(@RequestBody Dashboard dashboard) throws ThingsboardException {
try {
dashboard.setTenantId(getCurrentUser().getTenantId());
return checkNotNull(dashboardService.saveDashboard(dashboard));
Dashboard savedDashboard = checkNotNull(dashboardService.saveDashboard(dashboard));
logEntityAction(savedDashboard.getId(), savedDashboard,
savedDashboard.getCustomerId(),
dashboard.getId() == null ? ActionType.ADDED : ActionType.UPDATED, null);
return savedDashboard;
} catch (Exception e) {
logEntityAction(emptyId(EntityType.DASHBOARD), dashboard,
null, dashboard.getId() == null ? ActionType.ADDED : ActionType.UPDATED, e);
throw handleException(e);
}
}
@ -88,9 +99,20 @@ public class DashboardController extends BaseController {
checkParameter(DASHBOARD_ID, strDashboardId);
try {
DashboardId dashboardId = new DashboardId(toUUID(strDashboardId));
checkDashboardId(dashboardId);
Dashboard dashboard = checkDashboardId(dashboardId);
dashboardService.deleteDashboard(dashboardId);
logEntityAction(dashboardId, dashboard,
dashboard.getCustomerId(),
ActionType.DELETED, null, strDashboardId);
} catch (Exception e) {
logEntityAction(emptyId(EntityType.DASHBOARD),
null,
null,
ActionType.DELETED, e, strDashboardId);
throw handleException(e);
}
}
@ -104,13 +126,25 @@ public class DashboardController extends BaseController {
checkParameter(DASHBOARD_ID, strDashboardId);
try {
CustomerId customerId = new CustomerId(toUUID(strCustomerId));
checkCustomerId(customerId);
Customer customer = checkCustomerId(customerId);
DashboardId dashboardId = new DashboardId(toUUID(strDashboardId));
checkDashboardId(dashboardId);
return checkNotNull(dashboardService.assignDashboardToCustomer(dashboardId, customerId));
Dashboard savedDashboard = checkNotNull(dashboardService.assignDashboardToCustomer(dashboardId, customerId));
logEntityAction(dashboardId, savedDashboard,
savedDashboard.getCustomerId(),
ActionType.ASSIGNED_TO_CUSTOMER, null, strDashboardId, strCustomerId, customer.getName());
return savedDashboard;
} catch (Exception e) {
logEntityAction(emptyId(EntityType.DASHBOARD), null,
null,
ActionType.ASSIGNED_TO_CUSTOMER, e, strDashboardId, strCustomerId);
throw handleException(e);
}
}
@ -126,8 +160,22 @@ public class DashboardController extends BaseController {
if (dashboard.getCustomerId() == null || dashboard.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) {
throw new IncorrectParameterException("Dashboard isn't assigned to any customer!");
}
return checkNotNull(dashboardService.unassignDashboardFromCustomer(dashboardId));
Customer customer = checkCustomerId(dashboard.getCustomerId());
Dashboard savedDashboard = checkNotNull(dashboardService.unassignDashboardFromCustomer(dashboardId));
logEntityAction(dashboardId, dashboard,
dashboard.getCustomerId(),
ActionType.UNASSIGNED_FROM_CUSTOMER, null, strDashboardId, customer.getId().toString(), customer.getName());
return savedDashboard;
} catch (Exception e) {
logEntityAction(emptyId(EntityType.DASHBOARD), null,
null,
ActionType.UNASSIGNED_FROM_CUSTOMER, e, strDashboardId);
throw handleException(e);
}
}
@ -141,8 +189,19 @@ public class DashboardController extends BaseController {
DashboardId dashboardId = new DashboardId(toUUID(strDashboardId));
Dashboard dashboard = checkDashboardId(dashboardId);
Customer publicCustomer = customerService.findOrCreatePublicCustomer(dashboard.getTenantId());
return checkNotNull(dashboardService.assignDashboardToCustomer(dashboardId, publicCustomer.getId()));
Dashboard savedDashboard = checkNotNull(dashboardService.assignDashboardToCustomer(dashboardId, publicCustomer.getId()));
logEntityAction(dashboardId, savedDashboard,
savedDashboard.getCustomerId(),
ActionType.ASSIGNED_TO_CUSTOMER, null, strDashboardId, publicCustomer.getId().toString(), publicCustomer.getName());
return savedDashboard;
} catch (Exception e) {
logEntityAction(emptyId(EntityType.DASHBOARD), null,
null,
ActionType.ASSIGNED_TO_CUSTOMER, e, strDashboardId);
throw handleException(e);
}
}

View File

@ -22,6 +22,7 @@ import org.springframework.web.bind.annotation.*;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.audit.ActionStatus;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.device.DeviceSearchQuery;
@ -85,11 +86,15 @@ public class DeviceController extends BaseController {
savedDevice.getName(),
savedDevice.getType());
logEntityAddedOrUpdated(savedDevice.getId(), savedDevice.getName(), savedDevice.getCustomerId(), device.getId() == null);
logEntityAction(savedDevice.getId(), savedDevice,
savedDevice.getCustomerId(),
device.getId() == null ? ActionType.ADDED : ActionType.UPDATED, null);
return savedDevice;
} catch (Exception e) {
throw handleException(e, device.getId() == null ? ActionType.ADDED : ActionType.UPDATED, "addDevice(" + device + ")");
logEntityAction(emptyId(EntityType.DEVICE), device,
null, device.getId() == null ? ActionType.ADDED : ActionType.UPDATED, e);
throw handleException(e);
}
}
@ -102,9 +107,17 @@ public class DeviceController extends BaseController {
DeviceId deviceId = new DeviceId(toUUID(strDeviceId));
Device device = checkDeviceId(deviceId);
deviceService.deleteDevice(deviceId);
logEntityDeleted(device.getId(), device.getName(), device.getCustomerId());
logEntityAction(deviceId, device,
device.getCustomerId(),
ActionType.DELETED, null, strDeviceId);
} catch (Exception e) {
throw handleException(e, ActionType.DELETED, "deleteDevice(" + strDeviceId + ")");
logEntityAction(emptyId(EntityType.DEVICE),
null,
null,
ActionType.DELETED, e, strDeviceId);
throw handleException(e);
}
}
@ -117,13 +130,22 @@ public class DeviceController extends BaseController {
checkParameter(DEVICE_ID, strDeviceId);
try {
CustomerId customerId = new CustomerId(toUUID(strCustomerId));
checkCustomerId(customerId);
Customer customer = checkCustomerId(customerId);
DeviceId deviceId = new DeviceId(toUUID(strDeviceId));
checkDeviceId(deviceId);
return checkNotNull(deviceService.assignDeviceToCustomer(deviceId, customerId));
Device savedDevice = checkNotNull(deviceService.assignDeviceToCustomer(deviceId, customerId));
logEntityAction(deviceId, savedDevice,
savedDevice.getCustomerId(),
ActionType.ASSIGNED_TO_CUSTOMER, null, strDeviceId, strCustomerId, customer.getName());
return savedDevice;
} catch (Exception e) {
logEntityAction(emptyId(EntityType.DEVICE), null,
null,
ActionType.ASSIGNED_TO_CUSTOMER, e, strDeviceId, strCustomerId);
throw handleException(e);
}
}
@ -139,8 +161,19 @@ public class DeviceController extends BaseController {
if (device.getCustomerId() == null || device.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) {
throw new IncorrectParameterException("Device isn't assigned to any customer!");
}
return checkNotNull(deviceService.unassignDeviceFromCustomer(deviceId));
Customer customer = checkCustomerId(device.getCustomerId());
Device savedDevice = checkNotNull(deviceService.unassignDeviceFromCustomer(deviceId));
logEntityAction(deviceId, device,
device.getCustomerId(),
ActionType.UNASSIGNED_FROM_CUSTOMER, null, strDeviceId, customer.getId().toString(), customer.getName());
return savedDevice;
} catch (Exception e) {
logEntityAction(emptyId(EntityType.DEVICE), null,
null,
ActionType.UNASSIGNED_FROM_CUSTOMER, e, strDeviceId);
throw handleException(e);
}
}
@ -154,8 +187,17 @@ public class DeviceController extends BaseController {
DeviceId deviceId = new DeviceId(toUUID(strDeviceId));
Device device = checkDeviceId(deviceId);
Customer publicCustomer = customerService.findOrCreatePublicCustomer(device.getTenantId());
return checkNotNull(deviceService.assignDeviceToCustomer(deviceId, publicCustomer.getId()));
Device savedDevice = checkNotNull(deviceService.assignDeviceToCustomer(deviceId, publicCustomer.getId()));
logEntityAction(deviceId, savedDevice,
savedDevice.getCustomerId(),
ActionType.ASSIGNED_TO_CUSTOMER, null, strDeviceId, publicCustomer.getId().toString(), publicCustomer.getName());
return savedDevice;
} catch (Exception e) {
logEntityAction(emptyId(EntityType.DEVICE), null,
null,
ActionType.ASSIGNED_TO_CUSTOMER, e, strDeviceId);
throw handleException(e);
}
}
@ -167,9 +209,16 @@ public class DeviceController extends BaseController {
checkParameter(DEVICE_ID, strDeviceId);
try {
DeviceId deviceId = new DeviceId(toUUID(strDeviceId));
checkDeviceId(deviceId);
return checkNotNull(deviceCredentialsService.findDeviceCredentialsByDeviceId(deviceId));
Device device = checkDeviceId(deviceId);
DeviceCredentials deviceCredentials = checkNotNull(deviceCredentialsService.findDeviceCredentialsByDeviceId(deviceId));
logEntityAction(deviceId, device,
device.getCustomerId(),
ActionType.CREDENTIALS_READ, null, strDeviceId);
return deviceCredentials;
} catch (Exception e) {
logEntityAction(emptyId(EntityType.DEVICE), null,
null,
ActionType.CREDENTIALS_READ, e, strDeviceId);
throw handleException(e);
}
}
@ -183,10 +232,15 @@ public class DeviceController extends BaseController {
Device device = checkDeviceId(deviceCredentials.getDeviceId());
DeviceCredentials result = checkNotNull(deviceCredentialsService.updateDeviceCredentials(deviceCredentials));
actorService.onCredentialsUpdate(getCurrentUser().getTenantId(), deviceCredentials.getDeviceId());
logEntitySuccess(device.getId(), device.getName(), device.getCustomerId(), ActionType.CREDENTIALS_UPDATED);
logEntityAction(device.getId(), device,
device.getCustomerId(),
ActionType.CREDENTIALS_UPDATED, null, deviceCredentials);
return result;
} catch (Exception e) {
throw handleException(e, ActionType.CREDENTIALS_UPDATED, "saveDeviceCredentials(" + deviceCredentials + ")");
logEntityAction(emptyId(EntityType.DEVICE), null,
null,
ActionType.CREDENTIALS_UPDATED, e, deviceCredentials);
throw handleException(e);
}
}

View File

@ -18,6 +18,8 @@ package org.thingsboard.server.controller;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.id.PluginId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.TextPageData;
@ -71,8 +73,17 @@ public class PluginController extends BaseController {
PluginMetaData plugin = checkNotNull(pluginService.savePlugin(source));
actorService.onPluginStateChange(plugin.getTenantId(), plugin.getId(),
created ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED);
logEntityAction(plugin.getId(), plugin,
null,
created ? ActionType.ADDED : ActionType.UPDATED, null);
return plugin;
} catch (Exception e) {
logEntityAction(emptyId(EntityType.PLUGIN), source,
null, source.getId() == null ? ActionType.ADDED : ActionType.UPDATED, e);
throw handleException(e);
}
}
@ -87,7 +98,18 @@ public class PluginController extends BaseController {
PluginMetaData plugin = checkPlugin(pluginService.findPluginById(pluginId));
pluginService.activatePluginById(pluginId);
actorService.onPluginStateChange(plugin.getTenantId(), plugin.getId(), ComponentLifecycleEvent.ACTIVATED);
logEntityAction(plugin.getId(), plugin,
null,
ActionType.ACTIVATED, null, strPluginId);
} catch (Exception e) {
logEntityAction(emptyId(EntityType.PLUGIN),
null,
null,
ActionType.ACTIVATED, e, strPluginId);
throw handleException(e);
}
}
@ -102,7 +124,18 @@ public class PluginController extends BaseController {
PluginMetaData plugin = checkPlugin(pluginService.findPluginById(pluginId));
pluginService.suspendPluginById(pluginId);
actorService.onPluginStateChange(plugin.getTenantId(), plugin.getId(), ComponentLifecycleEvent.SUSPENDED);
logEntityAction(plugin.getId(), plugin,
null,
ActionType.SUSPENDED, null, strPluginId);
} catch (Exception e) {
logEntityAction(emptyId(EntityType.PLUGIN),
null,
null,
ActionType.SUSPENDED, e, strPluginId);
throw handleException(e);
}
}
@ -189,7 +222,16 @@ public class PluginController extends BaseController {
PluginMetaData plugin = checkPlugin(pluginService.findPluginById(pluginId));
pluginService.deletePluginById(pluginId);
actorService.onPluginStateChange(plugin.getTenantId(), plugin.getId(), ComponentLifecycleEvent.DELETED);
logEntityAction(pluginId, plugin,
null,
ActionType.DELETED, null, strPluginId);
} catch (Exception e) {
logEntityAction(emptyId(EntityType.PLUGIN),
null,
null,
ActionType.DELETED, e, strPluginId);
throw handleException(e);
}
}

View File

@ -18,6 +18,8 @@ package org.thingsboard.server.controller;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.id.RuleId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.TextPageData;
@ -73,8 +75,17 @@ public class RuleController extends BaseController {
RuleMetaData rule = checkNotNull(ruleService.saveRule(source));
actorService.onRuleStateChange(rule.getTenantId(), rule.getId(),
created ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED);
logEntityAction(rule.getId(), rule,
null,
created ? ActionType.ADDED : ActionType.UPDATED, null);
return rule;
} catch (Exception e) {
logEntityAction(emptyId(EntityType.RULE), source,
null, source.getId() == null ? ActionType.ADDED : ActionType.UPDATED, e);
throw handleException(e);
}
}
@ -89,7 +100,18 @@ public class RuleController extends BaseController {
RuleMetaData rule = checkRule(ruleService.findRuleById(ruleId));
ruleService.activateRuleById(ruleId);
actorService.onRuleStateChange(rule.getTenantId(), rule.getId(), ComponentLifecycleEvent.ACTIVATED);
logEntityAction(rule.getId(), rule,
null,
ActionType.ACTIVATED, null, strRuleId);
} catch (Exception e) {
logEntityAction(emptyId(EntityType.RULE),
null,
null,
ActionType.ACTIVATED, e, strRuleId);
throw handleException(e);
}
}
@ -104,7 +126,18 @@ public class RuleController extends BaseController {
RuleMetaData rule = checkRule(ruleService.findRuleById(ruleId));
ruleService.suspendRuleById(ruleId);
actorService.onRuleStateChange(rule.getTenantId(), rule.getId(), ComponentLifecycleEvent.SUSPENDED);
logEntityAction(rule.getId(), rule,
null,
ActionType.SUSPENDED, null, strRuleId);
} catch (Exception e) {
logEntityAction(emptyId(EntityType.RULE),
null,
null,
ActionType.SUSPENDED, e, strRuleId);
throw handleException(e);
}
}
@ -187,7 +220,18 @@ public class RuleController extends BaseController {
RuleMetaData rule = checkRule(ruleService.findRuleById(ruleId));
ruleService.deleteRuleById(ruleId);
actorService.onRuleStateChange(rule.getTenantId(), rule.getId(), ComponentLifecycleEvent.DELETED);
logEntityAction(ruleId, rule,
null,
ActionType.DELETED, null, strRuleId);
} catch (Exception e) {
logEntityAction(emptyId(EntityType.RULE),
null,
null,
ActionType.DELETED, e, strRuleId);
throw handleException(e);
}
}

View File

@ -19,7 +19,9 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
@ -92,8 +94,17 @@ public class UserController extends BaseController {
throw e;
}
}
logEntityAction(savedUser.getId(), savedUser,
savedUser.getCustomerId(),
user.getId() == null ? ActionType.ADDED : ActionType.UPDATED, null);
return savedUser;
} catch (Exception e) {
logEntityAction(emptyId(EntityType.USER), user,
null, user.getId() == null ? ActionType.ADDED : ActionType.UPDATED, e);
throw handleException(e);
}
}
@ -156,9 +167,18 @@ public class UserController extends BaseController {
checkParameter(USER_ID, strUserId);
try {
UserId userId = new UserId(toUUID(strUserId));
checkUserId(userId);
User user = checkUserId(userId);
userService.deleteUser(userId);
logEntityAction(userId, user,
user.getCustomerId(),
ActionType.DELETED, null, strUserId);
} catch (Exception e) {
logEntityAction(emptyId(EntityType.USER),
null,
null,
ActionType.DELETED, e, strUserId);
throw handleException(e);
}
}

View File

@ -30,6 +30,7 @@ import org.springframework.web.context.request.async.DeferredResult;
import org.thingsboard.server.actors.service.ActorService;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.plugin.PluginMetaData;
import org.thingsboard.server.controller.BaseController;
import org.thingsboard.server.dao.model.ModelConstants;
@ -68,7 +69,10 @@ public class PluginApiController extends BaseController {
if(tenantId != null && ModelConstants.NULL_UUID.equals(tenantId.getId())){
tenantId = null;
}
PluginApiCallSecurityContext securityCtx = new PluginApiCallSecurityContext(pluginMd.getTenantId(), pluginMd.getId(), tenantId, customerId);
UserId userId = getCurrentUser().getId();
String userName = getCurrentUser().getName();
PluginApiCallSecurityContext securityCtx = new PluginApiCallSecurityContext(pluginMd.getTenantId(), pluginMd.getId(),
tenantId, customerId, userId, userName);
actorService.process(new BasicPluginRestMsg(securityCtx, new RestRequest(requestEntity, request), result));
} else {
result.setResult(new ResponseEntity<>(HttpStatus.FORBIDDEN));

View File

@ -28,6 +28,7 @@ import org.springframework.context.annotation.Lazy;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.server.actors.service.ActorService;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.config.WebSocketConfiguration;
import org.thingsboard.server.extensions.api.plugins.PluginConstants;
import org.thingsboard.server.service.security.model.SecurityUser;
@ -151,8 +152,10 @@ public class PluginWebSocketHandler extends TextWebSocketHandler implements Plug
TenantId tenantId = currentUser.getTenantId();
CustomerId customerId = currentUser.getCustomerId();
if (PluginApiController.validatePluginAccess(pluginMd, tenantId, customerId)) {
UserId userId = currentUser.getId();
String userName = currentUser.getName();
PluginApiCallSecurityContext securityCtx = new PluginApiCallSecurityContext(pluginMd.getTenantId(), pluginMd.getId(), tenantId,
currentUser.getCustomerId());
currentUser.getCustomerId(), userId, userName);
return new BasicPluginWebsocketSessionRef(UUID.randomUUID().toString(), securityCtx, session.getUri(), session.getAttributes(),
session.getLocalAddress(), session.getRemoteAddress());
} else {

View File

@ -306,6 +306,14 @@ audit_log:
by_tenant_partitioning: "${AUDIT_LOG_BY_TENANT_PARTITIONING:MONTHS}"
# Number of days as history period if startTime and endTime are not specified
default_query_period: "${AUDIT_LOG_DEFAULT_QUERY_PERIOD:30}"
exceptions:
# Enable/disable audit log functionality for exceptions.
enabled: "${AUDIT_LOG_EXCEPTIONS_ENABLED:true}"
# Logging levels per each entity type.
# Allowed values: OFF (disable), W (log write operations), RW (log read and write operations)
logging_level:
mask:
"device": "W"
"asset": "W"
"dashboard": "W"
"customer": "W"
"user": "W"
"rule": "W"
"plugin": "W"

View File

@ -15,6 +15,27 @@
*/
package org.thingsboard.server.common.data.audit;
import lombok.Getter;
@Getter
public enum ActionType {
ADDED, DELETED, UPDATED, ATTRIBUTE_UPDATED, ATTRIBUTE_DELETED, ATTRIBUTE_ADDED, RPC_CALL, CREDENTIALS_UPDATED
}
ADDED(false), // log entity
DELETED(false), // log string id
UPDATED(false), // log entity
ATTRIBUTES_UPDATED(false), // log attributes/values
ATTRIBUTES_DELETED(false), // log attributes
RPC_CALL(false), // log method and params
CREDENTIALS_UPDATED(false), // log new credentials
ASSIGNED_TO_CUSTOMER(false), // log customer name
UNASSIGNED_FROM_CUSTOMER(false), // log customer name
ACTIVATED(false), // log string id
SUSPENDED(false), // log string id
CREDENTIALS_READ(true), // log device id
ATTRIBUTES_READ(true); // log attributes
private final boolean isRead;
ActionType(boolean isRead) {
this.isRead = isRead;
}
}

View File

@ -0,0 +1,46 @@
/**
* 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 org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.audit.ActionType;
import java.util.HashMap;
import java.util.Map;
public class AuditLogLevelFilter {
private Map<EntityType, AuditLogLevelMask> entityTypeMask = new HashMap<>();
public AuditLogLevelFilter(Map<String, String> mask) {
entityTypeMask.clear();
mask.forEach((entityTypeStr, logLevelMaskStr) -> {
EntityType entityType = EntityType.valueOf(entityTypeStr.toUpperCase());
AuditLogLevelMask logLevelMask = AuditLogLevelMask.valueOf(logLevelMaskStr.toUpperCase());
entityTypeMask.put(entityType, logLevelMask);
});
}
public boolean logEnabled(EntityType entityType, ActionType actionType) {
AuditLogLevelMask logLevelMask = entityTypeMask.get(entityType);
if (logLevelMask != null) {
return actionType.isRead() ? logLevelMask.isRead() : logLevelMask.isWrite();
} else {
return false;
}
}
}

View File

@ -0,0 +1,34 @@
/**
* 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;
@Getter
public enum AuditLogLevelMask {
OFF(false, false),
W(true, false),
RW(true, true);
private final boolean write;
private final boolean read;
AuditLogLevelMask(boolean write, boolean read) {
this.write = write;
this.read = read;
}
}

View File

@ -17,14 +17,13 @@ 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.BaseData;
import org.thingsboard.server.common.data.HasName;
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;
@ -40,13 +39,15 @@ public interface AuditLogService {
TimePageData<AuditLog> findAuditLogsByTenantId(TenantId tenantId, TimePageLink pageLink);
ListenableFuture<List<Void>> logEntityAction(User user,
EntityId entityId,
String entityName,
CustomerId customerId,
ActionType actionType,
JsonNode actionData,
ActionStatus actionStatus,
String actionFailureDetails);
<E extends BaseData<I> & HasName,
I extends UUIDBased & EntityId> ListenableFuture<List<Void>> logEntityAction(
TenantId tenantId,
CustomerId customerId,
UserId userId,
String userName,
I entityId,
E entity,
ActionType actionType,
Exception e, Object... additionalInfo);
}

View File

@ -17,6 +17,9 @@ 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;
@ -24,16 +27,24 @@ 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.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.*;
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.security.DeviceCredentials;
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;
@ -44,12 +55,20 @@ import static org.thingsboard.server.dao.service.Validator.validateId;
@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;
@Override
public TimePageData<AuditLog> findAuditLogsByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, TimePageLink pageLink) {
log.trace("Executing findAuditLogsByTenantIdAndCustomerId [{}], [{}], [{}]", tenantId, customerId, pageLink);
@ -86,25 +105,149 @@ public class AuditLogServiceImpl implements AuditLogService {
}
@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);
public <E extends BaseData<I> & HasName, I extends UUIDBased & 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(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 BaseData<I> & HasName, I extends UUIDBased & EntityId> JsonNode constructActionData(I entityId,
E entity,
ActionType actionType,
Object... additionalInfo) {
ObjectNode actionData = objectMapper.createObjectNode();
switch(actionType) {
case ADDED:
case UPDATED:
ObjectNode entityNode = objectMapper.valueToTree(entity);
if (entityId.getEntityType() == EntityType.DASHBOARD) {
entityNode.put("configuration", "");
}
actionData.set("entity", entityNode);
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;
}
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,

View File

@ -19,14 +19,13 @@ import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.thingsboard.server.common.data.BaseData;
import org.thingsboard.server.common.data.HasName;
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;
@ -57,7 +56,8 @@ public class DummyAuditLogServiceImpl implements AuditLogService {
}
@Override
public ListenableFuture<List<Void>> logEntityAction(User user, EntityId entityId, String entityName, CustomerId customerId, ActionType actionType, JsonNode actionData, ActionStatus actionStatus, String actionFailureDetails) {
return Futures.immediateFuture(Collections.emptyList());
public <E extends BaseData<I> & HasName, I extends UUIDBased & EntityId> ListenableFuture<List<Void>> logEntityAction(TenantId tenantId, CustomerId customerId, UserId userId, String userName, I entityId, E entity, ActionType actionType, Exception e, Object... additionalInfo) {
return null;
}
}

View File

@ -15,46 +15,10 @@
*/
package org.thingsboard.server.dao.sql.audit;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.query.Param;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.dao.model.sql.AuditLogEntity;
import java.util.List;
public interface AuditLogRepository extends CrudRepository<AuditLogEntity, String>, JpaSpecificationExecutor<AuditLogEntity> {
public interface AuditLogRepository extends CrudRepository<AuditLogEntity, String> {
@Query("SELECT al FROM AuditLogEntity al WHERE al.tenantId = :tenantId " +
"AND al.id > :idOffset ORDER BY al.id")
List<AuditLogEntity> findByTenantId(@Param("tenantId") String tenantId,
@Param("idOffset") String idOffset,
Pageable pageable);
@Query("SELECT al FROM AuditLogEntity al WHERE al.tenantId = :tenantId " +
"AND al.entityType = :entityType " +
"AND al.entityId = :entityId " +
"AND al.id > :idOffset ORDER BY al.id")
List<AuditLogEntity> findByTenantIdAndEntityId(@Param("tenantId") String tenantId,
@Param("entityId") String entityId,
@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

@ -20,8 +20,12 @@ import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.UUIDConverter;
import org.thingsboard.server.common.data.audit.AuditLog;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityId;
@ -31,15 +35,18 @@ import org.thingsboard.server.dao.DaoUtil;
import org.thingsboard.server.dao.audit.AuditLogDao;
import org.thingsboard.server.dao.model.sql.AuditLogEntity;
import org.thingsboard.server.dao.sql.JpaAbstractDao;
import org.thingsboard.server.dao.sql.JpaAbstractSearchTimeDao;
import org.thingsboard.server.dao.util.SqlDao;
import javax.annotation.PreDestroy;
import javax.persistence.criteria.Predicate;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.Executors;
import static org.thingsboard.server.common.data.UUIDConverter.fromTimeUUID;
import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID_STR;
import static org.springframework.data.jpa.domain.Specifications.where;
import static org.thingsboard.server.dao.model.ModelConstants.ID_PROPERTY;
@Component
@SqlDao
@ -95,41 +102,54 @@ public class JpaAuditLogDao extends JpaAbstractDao<AuditLogEntity, AuditLog> imp
@Override
public List<AuditLog> findAuditLogsByTenantIdAndEntityId(UUID tenantId, EntityId entityId, TimePageLink pageLink) {
return DaoUtil.convertDataList(
auditLogRepository.findByTenantIdAndEntityId(
fromTimeUUID(tenantId),
fromTimeUUID(entityId.getId()),
entityId.getEntityType(),
pageLink.getIdOffset() == null ? NULL_UUID_STR : fromTimeUUID(pageLink.getIdOffset()),
new PageRequest(0, pageLink.getLimit())));
return findAuditLogs(tenantId, entityId, null, null, pageLink);
}
@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())));
return findAuditLogs(tenantId, null, customerId, null, pageLink);
}
@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())));
return findAuditLogs(tenantId, null, null, userId, pageLink);
}
@Override
public List<AuditLog> findAuditLogsByTenantId(UUID tenantId, TimePageLink pageLink) {
return DaoUtil.convertDataList(
auditLogRepository.findByTenantId(
fromTimeUUID(tenantId),
pageLink.getIdOffset() == null ? NULL_UUID_STR : fromTimeUUID(pageLink.getIdOffset()),
new PageRequest(0, pageLink.getLimit())));
return findAuditLogs(tenantId, null, null, null, pageLink);
}
private List<AuditLog> findAuditLogs(UUID tenantId, EntityId entityId, CustomerId customerId, UserId userId, TimePageLink pageLink) {
Specification<AuditLogEntity> timeSearchSpec = JpaAbstractSearchTimeDao.getTimeSearchPageSpec(pageLink, "id");
Specification<AuditLogEntity> fieldsSpec = getEntityFieldsSpec(tenantId, entityId, customerId, userId);
Sort.Direction sortDirection = pageLink.isAscOrder() ? Sort.Direction.ASC : Sort.Direction.DESC;
Pageable pageable = new PageRequest(0, pageLink.getLimit(), sortDirection, ID_PROPERTY);
return DaoUtil.convertDataList(auditLogRepository.findAll(where(timeSearchSpec).and(fieldsSpec), pageable).getContent());
}
private Specification<AuditLogEntity> getEntityFieldsSpec(UUID tenantId, EntityId entityId, CustomerId customerId, UserId userId) {
return (root, criteriaQuery, criteriaBuilder) -> {
List<Predicate> predicates = new ArrayList<>();
if (tenantId != null) {
Predicate tenantIdPredicate = criteriaBuilder.equal(root.get("tenantId"), UUIDConverter.fromTimeUUID(tenantId));
predicates.add(tenantIdPredicate);
}
if (entityId != null) {
Predicate entityTypePredicate = criteriaBuilder.equal(root.get("entityType"), entityId.getEntityType());
predicates.add(entityTypePredicate);
Predicate entityIdPredicate = criteriaBuilder.equal(root.get("entityId"), UUIDConverter.fromTimeUUID(entityId.getId()));
predicates.add(entityIdPredicate);
}
if (customerId != null) {
Predicate tenantIdPredicate = criteriaBuilder.equal(root.get("customerId"), UUIDConverter.fromTimeUUID(customerId.getId()));
predicates.add(tenantIdPredicate);
}
if (userId != null) {
Predicate tenantIdPredicate = criteriaBuilder.equal(root.get("userId"), UUIDConverter.fromTimeUUID(userId.getId()));
predicates.add(tenantIdPredicate);
}
return criteriaBuilder.and(predicates.toArray(new Predicate[]{}));
};
}
}

View File

@ -57,9 +57,9 @@ CREATE TABLE IF NOT EXISTS audit_log (
user_id varchar(31),
user_name varchar(255),
action_type varchar(255),
action_data varchar(255),
action_data varchar(1000000),
action_status varchar(255),
action_failure_details varchar
action_failure_details varchar(1000000)
);
CREATE TABLE IF NOT EXISTS attribute_kv (

View File

@ -22,6 +22,7 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.TextNode;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.annotation.DirtiesContext;
@ -29,6 +30,7 @@ import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.context.support.AnnotationConfigContextLoader;
import org.thingsboard.server.common.data.BaseData;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.Event;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
@ -40,6 +42,8 @@ import org.thingsboard.server.common.data.plugin.PluginMetaData;
import org.thingsboard.server.common.data.rule.RuleMetaData;
import org.thingsboard.server.dao.alarm.AlarmService;
import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.audit.AuditLogLevelFilter;
import org.thingsboard.server.dao.audit.AuditLogLevelMask;
import org.thingsboard.server.dao.component.ComponentDescriptorService;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.dashboard.DashboardService;
@ -58,6 +62,8 @@ import org.thingsboard.server.dao.widget.WidgetsBundleService;
import java.io.IOException;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ThreadLocalRandom;
@ -227,4 +233,14 @@ public abstract class AbstractServiceTest {
oNode.set("configuration", readFromResource(configuration));
return oNode;
}
@Bean
public AuditLogLevelFilter auditLogLevelFilter() {
Map<String,String> mask = new HashMap<>();
for (EntityType entityType : EntityType.values()) {
mask.put(entityType.name().toLowerCase(), AuditLogLevelMask.RW.name());
}
return new AuditLogLevelFilter(mask);
}
}

View File

@ -5,7 +5,6 @@ zk.zk_dir=/thingsboard
updates.enabled=false
audit_log.enabled=true
audit_log.exceptions.enabled=false
audit_log.by_tenant_partitioning=MONTHS
audit_log.default_query_period=30

View File

@ -15,10 +15,7 @@
*/
package org.thingsboard.server.extensions.api.plugins;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.PluginId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.*;
import java.io.Serializable;
@ -30,13 +27,18 @@ public final class PluginApiCallSecurityContext implements Serializable {
private final PluginId pluginId;
private final TenantId tenantId;
private final CustomerId customerId;
private final UserId userId;
private final String userName;
public PluginApiCallSecurityContext(TenantId pluginTenantId, PluginId pluginId, TenantId tenantId, CustomerId customerId) {
public PluginApiCallSecurityContext(TenantId pluginTenantId, PluginId pluginId, TenantId tenantId, CustomerId customerId,
UserId userId, String userName) {
super();
this.pluginTenantId = pluginTenantId;
this.pluginId = pluginId;
this.tenantId = tenantId;
this.customerId = customerId;
this.userId = userId;
this.userName = userName;
}
public TenantId getPluginTenantId(){
@ -67,4 +69,12 @@ public final class PluginApiCallSecurityContext implements Serializable {
return customerId;
}
public UserId getUserId() {
return userId;
}
public String getUserName() {
return userName;
}
}

View File

@ -24,9 +24,7 @@ import org.thingsboard.server.common.data.kv.TsKvQuery;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import org.thingsboard.server.common.msg.cluster.ServerAddress;
import org.thingsboard.server.extensions.api.plugins.msg.PluginToRuleMsg;
import org.thingsboard.server.extensions.api.plugins.msg.TimeoutMsg;
import org.thingsboard.server.extensions.api.plugins.msg.ToDeviceRpcRequest;
import org.thingsboard.server.extensions.api.plugins.msg.*;
import org.thingsboard.server.extensions.api.plugins.rpc.RpcMsg;
import org.thingsboard.server.extensions.api.plugins.ws.PluginWebsocketSessionRef;
import org.thingsboard.server.extensions.api.plugins.ws.msg.PluginWebsocketMsg;
@ -60,6 +58,7 @@ public interface PluginContext {
void scheduleTimeoutMsg(TimeoutMsg<?> timeoutMsg);
void logRpcRequest(PluginApiCallSecurityContext ctx, DeviceId deviceId, ToDeviceRpcRequestBody body, boolean oneWay, Optional<RpcError> rpcError, Exception e);
/*
Websocket API
@ -96,6 +95,12 @@ public interface PluginContext {
Attributes API
*/
void logAttributesUpdated(PluginApiCallSecurityContext ctx, EntityId entityId, String attributeType, List<AttributeKvEntry> attributes, Exception e);
void logAttributesDeleted(PluginApiCallSecurityContext ctx, EntityId entityId, String attributeType, List<String> keys, Exception e);
void logAttributesRead(PluginApiCallSecurityContext ctx, EntityId entityId, String attributeType, List<String> keys, Exception e);
void saveAttributes(TenantId tenantId, EntityId entityId, String attributeType, List<AttributeKvEntry> attributes, PluginCallback<Void> callback);
void removeAttributes(TenantId tenantId, EntityId entityId, String scope, List<String> attributeKeys, PluginCallback<Void> callback);

View File

@ -18,6 +18,7 @@ package org.thingsboard.server.extensions.api.plugins.msg;
import lombok.Data;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.extensions.api.plugins.PluginApiCallSecurityContext;
import java.io.Serializable;
import java.util.UUID;
@ -28,6 +29,7 @@ import java.util.UUID;
@Data
public class ToDeviceRpcRequest implements Serializable {
private final UUID id;
private final PluginApiCallSecurityContext securityCtx;
private final TenantId tenantId;
private final DeviceId deviceId;
private final boolean oneway;

View File

@ -152,7 +152,7 @@ public class DeviceMessagingRuleMsgHandler implements RuleMsgHandler {
pendingMsgs.put(uid, requestMd);
log.trace("[{}] Forwarding {} to [{}]", uid, params, targetDeviceId);
ToDeviceRpcRequestBody requestBody = new ToDeviceRpcRequestBody(ON_MSG_METHOD_NAME, GSON.toJson(params.get("body")));
ctx.sendRpcRequest(new ToDeviceRpcRequest(uid, targetDevice.getTenantId(), targetDeviceId, oneWay, System.currentTimeMillis() + timeout, requestBody));
ctx.sendRpcRequest(new ToDeviceRpcRequest(uid, null, targetDevice.getTenantId(), targetDeviceId, oneWay, System.currentTimeMillis() + timeout, requestBody));
} else {
replyWithError(ctx, requestMd, RpcError.FORBIDDEN);
}

View File

@ -49,7 +49,7 @@ public class RpcManager {
LocalRequestMetaData md = localRpcRequests.remove(requestId);
if (md != null) {
log.trace("[{}] Processing local rpc response from device [{}]", requestId, md.getRequest().getDeviceId());
restHandler.reply(ctx, md.getResponseWriter(), response);
restHandler.reply(ctx, md.getRequest(), md.getResponseWriter(), response);
} else {
log.trace("[{}] Unknown or stale rpc response received [{}]", requestId, response);
}
@ -62,7 +62,7 @@ public class RpcManager {
LocalRequestMetaData md = localRpcRequests.remove(requestId);
if (md != null) {
log.trace("[{}] Processing rpc timeout for local device [{}]", requestId, md.getRequest().getDeviceId());
restHandler.reply(ctx, md.getResponseWriter(), timeoutReponse);
restHandler.reply(ctx, md.getRequest(), md.getResponseWriter(), timeoutReponse);
}
}
}

View File

@ -94,11 +94,12 @@ public class RpcRestMsgHandler extends DefaultRestMsgHandler {
private boolean handleDeviceRPCRequest(PluginContext ctx, final PluginRestMsg msg, TenantId tenantId, DeviceId deviceId, RpcRequest cmd, boolean oneWay) throws JsonProcessingException {
long timeout = System.currentTimeMillis() + (cmd.getTimeout() != null ? cmd.getTimeout() : defaultTimeout);
ToDeviceRpcRequestBody body = new ToDeviceRpcRequestBody(cmd.getMethodName(), cmd.getRequestData());
ctx.checkAccess(deviceId, new PluginCallback<Void>() {
@Override
public void onSuccess(PluginContext ctx, Void value) {
ToDeviceRpcRequestBody body = new ToDeviceRpcRequestBody(cmd.getMethodName(), cmd.getRequestData());
ToDeviceRpcRequest rpcRequest = new ToDeviceRpcRequest(UUID.randomUUID(),
msg.getSecurityCtx(),
tenantId,
deviceId,
oneWay,
@ -116,15 +117,17 @@ public class RpcRestMsgHandler extends DefaultRestMsgHandler {
} else {
response = new ResponseEntity(HttpStatus.UNAUTHORIZED);
}
ctx.logRpcRequest(msg.getSecurityCtx(), deviceId, body, oneWay, Optional.empty(), e);
msg.getResponseHolder().setResult(response);
}
});
return true;
}
public void reply(PluginContext ctx, DeferredResult<ResponseEntity> responseWriter, FromDeviceRpcResponse response) {
public void reply(PluginContext ctx, ToDeviceRpcRequest rpcRequest, DeferredResult<ResponseEntity> responseWriter, FromDeviceRpcResponse response) {
Optional<RpcError> rpcError = response.getError();
if (rpcError.isPresent()) {
ctx.logRpcRequest(rpcRequest.getSecurityCtx(), rpcRequest.getDeviceId(), rpcRequest.getBody(), rpcRequest.isOneway(), rpcError, null);
RpcError error = rpcError.get();
switch (error) {
case TIMEOUT:
@ -142,12 +145,15 @@ public class RpcRestMsgHandler extends DefaultRestMsgHandler {
if (responseData.isPresent() && !StringUtils.isEmpty(responseData.get())) {
String data = responseData.get();
try {
ctx.logRpcRequest(rpcRequest.getSecurityCtx(), rpcRequest.getDeviceId(), rpcRequest.getBody(), rpcRequest.isOneway(), rpcError, null);
responseWriter.setResult(new ResponseEntity<>(jsonMapper.readTree(data), HttpStatus.OK));
} catch (IOException e) {
log.debug("Failed to decode device response: {}", data, e);
ctx.logRpcRequest(rpcRequest.getSecurityCtx(), rpcRequest.getDeviceId(), rpcRequest.getBody(), rpcRequest.isOneway(), rpcError, e);
responseWriter.setResult(new ResponseEntity<>(HttpStatus.NOT_ACCEPTABLE));
}
} else {
ctx.logRpcRequest(rpcRequest.getSecurityCtx(), rpcRequest.getDeviceId(), rpcRequest.getBody(), rpcRequest.isOneway(), rpcError, null);
responseWriter.setResult(new ResponseEntity<>(HttpStatus.OK));
}
}

View File

@ -77,7 +77,7 @@ public class RpcRuleMsgHandler implements RuleMsgHandler {
@Override
public void onSuccess(PluginContext ctx, Void value) {
ctx.sendRpcRequest(new ToDeviceRpcRequest(UUID.randomUUID(),
tenantId, tmpId, true, expirationTime, body)
null, tenantId, tmpId, true, expirationTime, body)
);
log.trace("[{}] Sent RPC Call Action msg", tmpId);
}

View File

@ -28,6 +28,7 @@ import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityIdFactory;
import org.thingsboard.server.common.data.id.UUIDBased;
import org.thingsboard.server.common.data.kv.*;
import org.thingsboard.server.common.msg.core.TelemetryUploadRequest;
import org.thingsboard.server.common.transport.adaptor.JsonConverter;
@ -150,18 +151,19 @@ public class TelemetryRestMsgHandler extends DefaultRestMsgHandler {
private void handleHttpGetAttributesValues(PluginContext ctx, PluginRestMsg msg,
RestRequest request, String scope, EntityId entityId) throws ServletException {
String keys = request.getParameter("keys", "");
PluginCallback<List<AttributeKvEntry>> callback = getAttributeValuesPluginCallback(msg);
List<String> keyList = null;
if (!StringUtils.isEmpty(keys)) {
keyList = Arrays.asList(keys.split(","));
}
PluginCallback<List<AttributeKvEntry>> callback = getAttributeValuesPluginCallback(msg, scope, entityId, keyList);
if (!StringUtils.isEmpty(scope)) {
if (!StringUtils.isEmpty(keys)) {
List<String> keyList = Arrays.asList(keys.split(","));
if (keyList != null && !keyList.isEmpty()) {
ctx.loadAttributes(entityId, scope, keyList, callback);
} else {
ctx.loadAttributes(entityId, scope, callback);
}
} else {
if (!StringUtils.isEmpty(keys)) {
List<String> keyList = Arrays.asList(keys.split(","));
if (keyList != null && !keyList.isEmpty()) {
ctx.loadAttributes(entityId, Arrays.asList(DataConstants.allScopes()), keyList, callback);
} else {
ctx.loadAttributes(entityId, Arrays.asList(DataConstants.allScopes()), callback);
@ -230,9 +232,11 @@ public class TelemetryRestMsgHandler extends DefaultRestMsgHandler {
if (attributes.isEmpty()) {
throw new IllegalArgumentException("No attributes data found in request body!");
}
ctx.saveAttributes(ctx.getSecurityCtx().orElseThrow(IllegalArgumentException::new).getTenantId(), entityId, scope, attributes, new PluginCallback<Void>() {
@Override
public void onSuccess(PluginContext ctx, Void value) {
ctx.logAttributesUpdated(msg.getSecurityCtx(), entityId, scope, attributes, null);
msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.OK));
subscriptionManager.onAttributesUpdateFromServer(ctx, entityId, scope, attributes);
}
@ -240,6 +244,7 @@ public class TelemetryRestMsgHandler extends DefaultRestMsgHandler {
@Override
public void onFailure(PluginContext ctx, Exception e) {
log.error("Failed to save attributes", e);
ctx.logAttributesUpdated(msg.getSecurityCtx(), entityId, scope, attributes, e);
handleError(e, msg, HttpStatus.BAD_REQUEST);
}
});
@ -334,15 +339,18 @@ public class TelemetryRestMsgHandler extends DefaultRestMsgHandler {
String keysParam = request.getParameter("keys");
if (!StringUtils.isEmpty(keysParam)) {
String[] keys = keysParam.split(",");
ctx.removeAttributes(ctx.getSecurityCtx().orElseThrow(IllegalArgumentException::new).getTenantId(), entityId, scope, Arrays.asList(keys), new PluginCallback<Void>() {
List<String> keyList = Arrays.asList(keys);
ctx.removeAttributes(ctx.getSecurityCtx().orElseThrow(IllegalArgumentException::new).getTenantId(), entityId, scope, keyList, new PluginCallback<Void>() {
@Override
public void onSuccess(PluginContext ctx, Void value) {
ctx.logAttributesDeleted(msg.getSecurityCtx(), entityId, scope, keyList, null);
msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.OK));
}
@Override
public void onFailure(PluginContext ctx, Exception e) {
log.error("Failed to remove attributes", e);
ctx.logAttributesDeleted(msg.getSecurityCtx(), entityId, scope, keyList, e);
handleError(e, msg, HttpStatus.INTERNAL_SERVER_ERROR);
}
});
@ -373,18 +381,21 @@ public class TelemetryRestMsgHandler extends DefaultRestMsgHandler {
};
}
private PluginCallback<List<AttributeKvEntry>> getAttributeValuesPluginCallback(final PluginRestMsg msg) {
private PluginCallback<List<AttributeKvEntry>> getAttributeValuesPluginCallback(final PluginRestMsg msg, final String scope,
final EntityId entityId, final List<String> keyList) {
return new PluginCallback<List<AttributeKvEntry>>() {
@Override
public void onSuccess(PluginContext ctx, List<AttributeKvEntry> attributes) {
List<AttributeData> values = attributes.stream().map(attribute -> new AttributeData(attribute.getLastUpdateTs(),
attribute.getKey(), attribute.getValue())).collect(Collectors.toList());
ctx.logAttributesRead(msg.getSecurityCtx(), entityId, scope, keyList, null);
msg.getResponseHolder().setResult(new ResponseEntity<>(values, HttpStatus.OK));
}
@Override
public void onFailure(PluginContext ctx, Exception e) {
log.error("Failed to fetch attributes", e);
ctx.logAttributesRead(msg.getSecurityCtx(), entityId, scope, keyList, e);
handleError(e, msg, HttpStatus.INTERNAL_SERVER_ERROR);
}
};

View File

@ -29,7 +29,7 @@
</section>
<div flex layout="column" class="tb-alarm-container md-whiteframe-z1">
<md-list flex layout="column" class="tb-alarm-table">
<md-list class="tb-row tb-header" layout="row" tb-alarm-header>
<md-list class="tb-row tb-header" layout="row" layout-align="start center" tb-alarm-header>
</md-list>
<md-progress-linear style="max-height: 0px;" md-mode="indeterminate" ng-disabled="!$root.loading"
ng-show="$root.loading"></md-progress-linear>
@ -39,7 +39,7 @@
class="tb-prompt" ng-show="noData()">alarm.no-alarms-prompt</span>
<md-virtual-repeat-container ng-show="hasData()" flex md-top-index="topIndex" tb-scope-element="repeatContainer">
<md-list-item md-virtual-repeat="alarm in theAlarms" md-on-demand flex ng-style="hasScroll() ? {'margin-right':'-15px'} : {}">
<md-list class="tb-row" flex layout="row" tb-alarm-row alarm="{{alarm}}">
<md-list class="tb-row" flex layout="row" layout-align="start center" tb-alarm-row alarm="{{alarm}}">
</md-list>
<md-divider flex></md-divider>
</md-list-item>

View File

@ -0,0 +1,116 @@
/*
* 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.
*/
export default angular.module('thingsboard.api.auditLog', [])
.factory('auditLogService', AuditLogService)
.name;
/*@ngInject*/
function AuditLogService($http, $q) {
var service = {
getAuditLogsByEntityId: getAuditLogsByEntityId,
getAuditLogsByUserId: getAuditLogsByUserId,
getAuditLogsByCustomerId: getAuditLogsByCustomerId,
getAuditLogs: getAuditLogs
}
return service;
function getAuditLogsByEntityId (entityType, entityId, pageLink) {
var deferred = $q.defer();
var url = `/api/audit/logs/entity/${entityType}/${entityId}?limit=${pageLink.limit}`;
if (angular.isDefined(pageLink.startTime) && pageLink.startTime != null) {
url += '&startTime=' + pageLink.startTime;
}
if (angular.isDefined(pageLink.endTime) && pageLink.endTime != null) {
url += '&endTime=' + pageLink.endTime;
}
if (angular.isDefined(pageLink.idOffset) && pageLink.idOffset != null) {
url += '&offset=' + pageLink.idOffset;
}
$http.get(url, null).then(function success(response) {
deferred.resolve(response.data);
}, function fail() {
deferred.reject();
});
return deferred.promise;
}
function getAuditLogsByUserId (userId, pageLink) {
var deferred = $q.defer();
var url = `/api/audit/logs/user/${userId}?limit=${pageLink.limit}`;
if (angular.isDefined(pageLink.startTime) && pageLink.startTime != null) {
url += '&startTime=' + pageLink.startTime;
}
if (angular.isDefined(pageLink.endTime) && pageLink.endTime != null) {
url += '&endTime=' + pageLink.endTime;
}
if (angular.isDefined(pageLink.idOffset) && pageLink.idOffset != null) {
url += '&offset=' + pageLink.idOffset;
}
$http.get(url, null).then(function success(response) {
deferred.resolve(response.data);
}, function fail() {
deferred.reject();
});
return deferred.promise;
}
function getAuditLogsByCustomerId (customerId, pageLink) {
var deferred = $q.defer();
var url = `/api/audit/logs/customer/${customerId}?limit=${pageLink.limit}`;
if (angular.isDefined(pageLink.startTime) && pageLink.startTime != null) {
url += '&startTime=' + pageLink.startTime;
}
if (angular.isDefined(pageLink.endTime) && pageLink.endTime != null) {
url += '&endTime=' + pageLink.endTime;
}
if (angular.isDefined(pageLink.idOffset) && pageLink.idOffset != null) {
url += '&offset=' + pageLink.idOffset;
}
$http.get(url, null).then(function success(response) {
deferred.resolve(response.data);
}, function fail() {
deferred.reject();
});
return deferred.promise;
}
function getAuditLogs (pageLink) {
var deferred = $q.defer();
var url = `/api/audit/logs?limit=${pageLink.limit}`;
if (angular.isDefined(pageLink.startTime) && pageLink.startTime != null) {
url += '&startTime=' + pageLink.startTime;
}
if (angular.isDefined(pageLink.endTime) && pageLink.endTime != null) {
url += '&endTime=' + pageLink.endTime;
}
if (angular.isDefined(pageLink.idOffset) && pageLink.idOffset != null) {
url += '&offset=' + pageLink.idOffset;
}
$http.get(url, null).then(function success(response) {
deferred.resolve(response.data);
}, function fail() {
deferred.reject();
});
return deferred.promise;
}
}

View File

@ -20,7 +20,7 @@ export default angular.module('thingsboard.api.device', [thingsboardTypes])
.name;
/*@ngInject*/
function DeviceService($http, $q, attributeService, customerService, types) {
function DeviceService($http, $q, $window, userService, attributeService, customerService, types) {
var service = {
assignDeviceToCustomer: assignDeviceToCustomer,
@ -181,14 +181,27 @@ function DeviceService($http, $q, attributeService, customerService, types) {
return deferred.promise;
}
function getDeviceCredentials(deviceId) {
function getDeviceCredentials(deviceId, sync) {
var deferred = $q.defer();
var url = '/api/device/' + deviceId + '/credentials';
$http.get(url, null).then(function success(response) {
deferred.resolve(response.data);
}, function fail() {
deferred.reject();
});
if (sync) {
var request = new $window.XMLHttpRequest();
request.open('GET', url, false);
request.setRequestHeader("Accept", "application/json, text/plain, */*");
userService.setAuthorizationRequestHeader(request);
request.send(null);
if (request.status === 200) {
deferred.resolve(angular.fromJson(request.responseText));
} else {
deferred.reject();
}
} else {
$http.get(url, null).then(function success(response) {
deferred.resolve(response.data);
}, function fail() {
deferred.reject();
});
}
return deferred.promise;
}

View File

@ -54,6 +54,7 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi
refreshJwtToken: refreshJwtToken,
refreshTokenPending: refreshTokenPending,
updateAuthorizationHeader: updateAuthorizationHeader,
setAuthorizationRequestHeader: setAuthorizationRequestHeader,
gotoDefaultPlace: gotoDefaultPlace,
forceDefaultPlace: forceDefaultPlace,
updateLastPublicDashboardId: updateLastPublicDashboardId,
@ -367,6 +368,14 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi
return jwtToken;
}
function setAuthorizationRequestHeader(request) {
var jwtToken = store.get('jwt_token');
if (jwtToken) {
request.setRequestHeader('X-Authorization', 'Bearer ' + jwtToken);
}
return jwtToken;
}
function getTenantAdmins(tenantId, pageLink) {
var deferred = $q.defer();
var url = '/api/tenant/' + tenantId + '/users?limit=' + pageLink.limit;

View File

@ -63,6 +63,7 @@ import thingsboardApiTime from './api/time.service';
import thingsboardKeyboardShortcut from './components/keyboard-shortcut.filter';
import thingsboardHelp from './help/help.directive';
import thingsboardToast from './services/toast';
import thingsboardClipboard from './services/clipboard.service';
import thingsboardHome from './layout';
import thingsboardApiLogin from './api/login.service';
import thingsboardApiDevice from './api/device.service';
@ -72,6 +73,7 @@ import thingsboardApiAsset from './api/asset.service';
import thingsboardApiAttribute from './api/attribute.service';
import thingsboardApiEntity from './api/entity.service';
import thingsboardApiAlarm from './api/alarm.service';
import thingsboardApiAuditLog from './api/audit-log.service';
import 'typeface-roboto';
import 'font-awesome/css/font-awesome.min.css';
@ -123,6 +125,7 @@ angular.module('thingsboard', [
thingsboardKeyboardShortcut,
thingsboardHelp,
thingsboardToast,
thingsboardClipboard,
thingsboardHome,
thingsboardApiLogin,
thingsboardApiDevice,
@ -132,6 +135,7 @@ angular.module('thingsboard', [
thingsboardApiAttribute,
thingsboardApiEntity,
thingsboardApiAlarm,
thingsboardApiAuditLog,
uiRouter])
.config(AppConfig)
.factory('globalInterceptor', GlobalInterceptor)

View File

@ -66,4 +66,10 @@
entity-type="{{vm.types.entityType.asset}}">
</tb-relation-table>
</md-tab>
<md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.grid.isTenantAdmin()" md-on-select="vm.grid.triggerResize()" label="{{ 'audit-log.audit-logs' | translate }}">
<tb-audit-log-table flex entity-type="vm.types.entityType.asset"
entity-id="vm.grid.operatingItem().id.id"
audit-log-mode="{{vm.types.auditLogMode.entity}}">
</tb-audit-log-table>
</md-tab>
</tb-grid>

View File

@ -0,0 +1,103 @@
/*
* 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.
*/
import $ from 'jquery';
import 'brace/ext/language_tools';
import 'brace/mode/java';
import 'brace/theme/github';
/* eslint-disable angular/angularelement */
import './audit-log-details-dialog.scss';
/*@ngInject*/
export default function AuditLogDetailsDialogController($mdDialog, types, auditLog, showingCallback) {
var vm = this;
showingCallback.onShowing = function(scope, element) {
updateEditorSize(element, vm.actionData, 'tb-audit-log-action-data');
vm.actionDataEditor.resize();
if (vm.displayFailureDetails) {
updateEditorSize(element, vm.actionFailureDetails, 'tb-audit-log-failure-details');
vm.failureDetailsEditor.resize();
}
};
vm.types = types;
vm.auditLog = auditLog;
vm.displayFailureDetails = auditLog.actionStatus == types.auditLogActionStatus.FAILURE.value;
vm.actionData = auditLog.actionDataText;
vm.actionFailureDetails = auditLog.actionFailureDetails;
vm.actionDataContentOptions = {
useWrapMode: false,
mode: 'java',
showGutter: false,
showPrintMargin: false,
theme: 'github',
advanced: {
enableSnippets: false,
enableBasicAutocompletion: false,
enableLiveAutocompletion: false
},
onLoad: function (_ace) {
vm.actionDataEditor = _ace;
}
};
vm.failureDetailsContentOptions = {
useWrapMode: false,
mode: 'java',
showGutter: false,
showPrintMargin: false,
theme: 'github',
advanced: {
enableSnippets: false,
enableBasicAutocompletion: false,
enableLiveAutocompletion: false
},
onLoad: function (_ace) {
vm.failureDetailsEditor = _ace;
}
};
function updateEditorSize(element, content, editorId) {
var newHeight = 200;
var newWidth = 600;
if (content && content.length > 0) {
var lines = content.split('\n');
newHeight = 16 * lines.length + 16;
var maxLineLength = 0;
for (var i in lines) {
var line = lines[i].replace(/\t/g, ' ').replace(/\n/g, '');
var lineLength = line.length;
maxLineLength = Math.max(maxLineLength, lineLength);
}
newWidth = 8 * maxLineLength + 16;
}
$('#'+editorId, element).height(newHeight.toString() + "px").css('min-height', newHeight.toString() + "px")
.width(newWidth.toString() + "px");
}
vm.close = close;
function close () {
$mdDialog.hide();
}
}
/* eslint-enable angular/angularelement */

View File

@ -0,0 +1,23 @@
/**
* 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.
*/
#tb-audit-log-action-data, #tb-audit-log-failure-details {
min-width: 400px;
min-height: 50px;
width: 100%;
height: 100%;
border: 1px solid #C0C0C0;
}

View File

@ -0,0 +1,49 @@
<!--
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.
-->
<md-dialog aria-label="{{ 'audit-log.audit-log-details' | translate }}">
<md-toolbar>
<div class="md-toolbar-tools">
<h2 translate>audit-log.audit-log-details</h2>
<span flex></span>
<md-button class="md-icon-button" ng-click="vm.close()">
<ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
</md-button>
</div>
</md-toolbar>
<md-dialog-content>
<div class="md-dialog-content" layout="column">
<label translate class="tb-title no-padding">audit-log.action-data</label>
<div flex id="tb-audit-log-action-data" readonly
ui-ace="vm.actionDataContentOptions"
ng-model="vm.actionData">
</div>
<span style="height: 30px;"></span>
<label ng-show="vm.displayFailureDetails" translate class="tb-title no-padding">audit-log.failure-details</label>
<div ng-show="vm.displayFailureDetails" flex id="tb-audit-log-failure-details" readonly
ui-ace="vm.failureDetailsContentOptions"
ng-model="vm.actionFailureDetails">
</div>
</div>
</md-dialog-content>
<md-dialog-actions layout="row">
<span flex></span>
<md-button ng-disabled="$root.loading" ng-click="vm.close()" style="margin-right:20px;">{{ 'action.close' |
translate }}
</md-button>
</md-dialog-actions>
</md-dialog>

View File

@ -0,0 +1,41 @@
/*
* 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.
*/
/* eslint-disable import/no-unresolved, import/default */
import auditLogHeaderTemplate from './audit-log-header.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
/*@ngInject*/
export default function AuditLogHeaderDirective($compile, $templateCache, types) {
var linker = function (scope, element, attrs) {
var template = $templateCache.get(auditLogHeaderTemplate);
element.html(template);
scope.auditLogMode = attrs.auditLogMode;
scope.types = types;
$compile(element.contents())(scope);
};
return {
restrict: "A",
replace: false,
link: linker,
scope: false
};
}

View File

@ -0,0 +1,24 @@
<!--
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.
-->
<div translate class="tb-cell" flex="30">audit-log.timestamp</div>
<div ng-if="auditLogMode != types.auditLogMode.entity" translate class="tb-cell" flex="10">audit-log.entity-type</div>
<div ng-if="auditLogMode != types.auditLogMode.entity" translate class="tb-cell" flex="30">audit-log.entity-name</div>
<div ng-if="auditLogMode != types.auditLogMode.user" translate class="tb-cell" flex="30">audit-log.user</div>
<div translate class="tb-cell" flex="15">audit-log.type</div>
<div translate class="tb-cell" flex="15">audit-log.status</div>
<div translate class="tb-cell" flex="10">audit-log.details</div>

View File

@ -0,0 +1,67 @@
/*
* 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.
*/
/* eslint-disable import/no-unresolved, import/default */
import auditLogDetailsDialogTemplate from './audit-log-details-dialog.tpl.html';
import auditLogRowTemplate from './audit-log-row.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
/*@ngInject*/
export default function AuditLogRowDirective($compile, $templateCache, types, $mdDialog, $document) {
var linker = function (scope, element, attrs) {
var template = $templateCache.get(auditLogRowTemplate);
element.html(template);
scope.auditLog = attrs.auditLog;
scope.auditLogMode = attrs.auditLogMode;
scope.types = types;
scope.showAuditLogDetails = function($event) {
var onShowingCallback = {
onShowing: function(){}
}
$mdDialog.show({
controller: 'AuditLogDetailsDialogController',
controllerAs: 'vm',
templateUrl: auditLogDetailsDialogTemplate,
locals: {
auditLog: scope.auditLog,
showingCallback: onShowingCallback
},
parent: angular.element($document[0].body),
targetEvent: $event,
fullscreen: true,
skipHide: true,
onShowing: function(scope, element) {
onShowingCallback.onShowing(scope, element);
}
});
}
$compile(element.contents())(scope);
}
return {
restrict: "A",
replace: false,
link: linker,
scope: false
};
}

View File

@ -0,0 +1,36 @@
<!--
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.
-->
<div class="tb-cell" flex="30">{{ auditLog.createdTime | date : 'yyyy-MM-dd HH:mm:ss' }}</div>
<div ng-if="auditLogMode != types.auditLogMode.entity" class="tb-cell" flex="10">{{ auditLog.entityTypeText }}</div>
<div ng-if="auditLogMode != types.auditLogMode.entity" class="tb-cell" flex="30">{{ auditLog.entityName }}</div>
<div ng-if="auditLogMode != types.auditLogMode.user" class="tb-cell" flex="30">{{ auditLog.userName }}</div>
<div class="tb-cell" flex="15">{{ auditLog.actionTypeText }}</div>
<div class="tb-cell" flex="15">{{ auditLog.actionStatusText }}</div>
<div class="tb-cell" flex="10">
<md-button class="md-icon-button md-primary"
ng-click="showAuditLogDetails($event)"
aria-label="{{ 'action.view' | translate }}">
<md-tooltip md-direction="top">
{{ 'audit-log.details' | translate }}
</md-tooltip>
<md-icon aria-label="{{ 'action.view' | translate }}"
class="material-icons">
more_horiz
</md-icon>
</md-button>
</div>

View File

@ -0,0 +1,262 @@
/*
* 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.
*/
import './audit-log.scss';
/* eslint-disable import/no-unresolved, import/default */
import auditLogTableTemplate from './audit-log-table.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
/*@ngInject*/
export default function AuditLogTableDirective($compile, $templateCache, $rootScope, $filter, $translate, types, auditLogService) {
var linker = function (scope, element) {
var template = $templateCache.get(auditLogTableTemplate);
element.html(template);
scope.types = types;
var pageSize = 20;
var startTime = 0;
var endTime = 0;
scope.timewindow = {
history: {
timewindowMs: 24 * 60 * 60 * 1000 // 1 day
}
}
scope.topIndex = 0;
scope.searchText = '';
scope.theAuditLogs = {
getItemAtIndex: function (index) {
if (index > scope.auditLogs.filtered.length) {
scope.theAuditLogs.fetchMoreItems_(index);
return null;
}
return scope.auditLogs.filtered[index];
},
getLength: function () {
if (scope.auditLogs.hasNext) {
return scope.auditLogs.filtered.length + scope.auditLogs.nextPageLink.limit;
} else {
return scope.auditLogs.filtered.length;
}
},
fetchMoreItems_: function () {
if (scope.auditLogs.hasNext && !scope.auditLogs.pending) {
var promise = getAuditLogsPromise(scope.auditLogs.nextPageLink);
if (promise) {
scope.auditLogs.pending = true;
promise.then(
function success(auditLogs) {
scope.auditLogs.data = scope.auditLogs.data.concat(prepareAuditLogsData(auditLogs.data));
scope.auditLogs.filtered = $filter('filter')(scope.auditLogs.data, {$: scope.searchText});
scope.auditLogs.nextPageLink = auditLogs.nextPageLink;
scope.auditLogs.hasNext = auditLogs.hasNext;
if (scope.auditLogs.hasNext) {
scope.auditLogs.nextPageLink.limit = pageSize;
}
scope.auditLogs.pending = false;
},
function fail() {
scope.auditLogs.hasNext = false;
scope.auditLogs.pending = false;
});
} else {
scope.auditLogs.hasNext = false;
}
}
}
};
function prepareAuditLogsData(data) {
data.forEach(
auditLog => {
auditLog.entityTypeText = $translate.instant(types.entityTypeTranslations[auditLog.entityId.entityType].type);
auditLog.actionTypeText = $translate.instant(types.auditLogActionType[auditLog.actionType].name);
auditLog.actionStatusText = $translate.instant(types.auditLogActionStatus[auditLog.actionStatus].name);
auditLog.actionDataText = auditLog.actionData ? angular.toJson(auditLog.actionData, true) : '';
}
);
return data;
}
scope.$watch("entityId", function(newVal, prevVal) {
if (newVal && !angular.equals(newVal, prevVal)) {
resetFilter();
scope.reload();
}
});
scope.$watch("userId", function(newVal, prevVal) {
if (newVal && !angular.equals(newVal, prevVal)) {
resetFilter();
scope.reload();
}
});
scope.$watch("customerId", function(newVal, prevVal) {
if (newVal && !angular.equals(newVal, prevVal)) {
resetFilter();
scope.reload();
}
});
function getAuditLogsPromise(pageLink) {
switch(scope.auditLogMode) {
case types.auditLogMode.tenant:
return auditLogService.getAuditLogs(pageLink);
case types.auditLogMode.entity:
if (scope.entityType && scope.entityId) {
return auditLogService.getAuditLogsByEntityId(scope.entityType, scope.entityId,
pageLink);
} else {
return null;
}
case types.auditLogMode.user:
if (scope.userId) {
return auditLogService.getAuditLogsByUserId(scope.userId, pageLink);
} else {
return null;
}
case types.auditLogMode.customer:
if (scope.customerId) {
return auditLogService.getAuditLogsByCustomerId(scope.customerId, pageLink);
} else {
return null;
}
}
}
function destroyWatchers() {
if (scope.timewindowWatchHandle) {
scope.timewindowWatchHandle();
scope.timewindowWatchHandle = null;
}
if (scope.searchTextWatchHandle) {
scope.searchTextWatchHandle();
scope.searchTextWatchHandle = null;
}
}
function initWatchers() {
scope.timewindowWatchHandle = scope.$watch("timewindow", function(newVal, prevVal) {
if (newVal && !angular.equals(newVal, prevVal)) {
scope.reload();
}
}, true);
scope.searchTextWatchHandle = scope.$watch("searchText", function(newVal, prevVal) {
if (!angular.equals(newVal, prevVal)) {
scope.searchTextUpdated();
}
}, true);
}
function resetFilter() {
destroyWatchers();
scope.timewindow = {
history: {
timewindowMs: 24 * 60 * 60 * 1000 // 1 day
}
};
scope.searchText = '';
initWatchers();
}
function updateTimeWindowRange () {
if (scope.timewindow.history.timewindowMs) {
var currentTime = (new Date).getTime();
startTime = currentTime - scope.timewindow.history.timewindowMs;
endTime = currentTime;
} else {
startTime = scope.timewindow.history.fixedTimewindow.startTimeMs;
endTime = scope.timewindow.history.fixedTimewindow.endTimeMs;
}
}
scope.reload = function() {
scope.topIndex = 0;
updateTimeWindowRange();
scope.auditLogs = {
data: [],
filtered: [],
nextPageLink: {
limit: pageSize,
startTime: startTime,
endTime: endTime
},
hasNext: true,
pending: false
};
scope.theAuditLogs.getItemAtIndex(pageSize);
}
scope.searchTextUpdated = function() {
scope.auditLogs.filtered = $filter('filter')(scope.auditLogs.data, {$: scope.searchText});
scope.theAuditLogs.getItemAtIndex(pageSize);
}
scope.noData = function() {
return scope.auditLogs.data.length == 0 && !scope.auditLogs.hasNext;
}
scope.hasData = function() {
return scope.auditLogs.data.length > 0;
}
scope.loading = function() {
return $rootScope.loading;
}
scope.hasScroll = function() {
var repeatContainer = scope.repeatContainer[0];
if (repeatContainer) {
var scrollElement = repeatContainer.children[0];
if (scrollElement) {
return scrollElement.scrollHeight > scrollElement.clientHeight;
}
}
return false;
}
scope.reload();
initWatchers();
$compile(element.contents())(scope);
}
return {
restrict: "E",
link: linker,
scope: {
entityType: '=?',
entityId: '=?',
userId: '=?',
customerId: '=?',
auditLogMode: '@',
pageMode: '@?'
}
};
}

View File

@ -0,0 +1,68 @@
<!--
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.
-->
<md-content flex class="md-padding tb-absolute-fill" layout="column">
<div flex layout="column" class="tb-audit-logs" ng-class="{'md-whiteframe-z1': pageMode}">
<div layout="column" layout-gt-sm="row" layout-align-gt-sm="start center" class="tb-audit-log-toolbar" ng-class="{'md-padding': pageMode, 'tb-audit-log-margin-18px': !pageMode}">
<tb-timewindow ng-model="timewindow" history-only as-button="true"></tb-timewindow>
<div flex layout="row" layout-align="start center">
<md-button class="md-icon-button" aria-label="{{ 'action.search' | translate }}">
<md-icon aria-label="{{ 'action.search' | translate }}" class="material-icons">search</md-icon>
<md-tooltip md-direction="top">
{{'audit-log.search' | translate}}
</md-tooltip>
</md-button>
<md-input-container flex class="tb-audit-log-search-input">
<label>&nbsp;</label>
<input ng-model="searchText" placeholder="{{'audit-log.search' | translate}}"/>
</md-input-container>
<md-button ng-disabled="$root.loading" class="md-icon-button" aria-label="Close" ng-click="searchText = ''">
<md-icon aria-label="Close" class="material-icons">close</md-icon>
<md-tooltip md-direction="top">
{{ 'audit-log.clear-search' | translate }}
</md-tooltip>
</md-button>
<md-button ng-disabled="$root.loading"
class="md-icon-button" ng-click="reload()">
<md-icon>refresh</md-icon>
<md-tooltip md-direction="top">
{{ 'action.refresh' | translate }}
</md-tooltip>
</md-button>
</div>
</div>
<div flex layout="column" class="tb-audit-log-container" ng-class="{'md-whiteframe-z1': !pageMode}">
<md-list flex layout="column" class="tb-audit-log-table" ng-class="{'tb-audit-log-table-full': pageMode}">
<md-list class="tb-row tb-header" layout="row" layout-align="start center" tb-audit-log-header audit-log-mode="{{auditLogMode}}">
</md-list>
<md-progress-linear style="max-height: 0px;" md-mode="indeterminate" ng-disabled="!$root.loading"
ng-show="$root.loading"></md-progress-linear>
<md-divider></md-divider>
<span translate layout-align="center center"
style="margin-top: 25px;"
class="tb-prompt" ng-show="noData()">audit-log.no-audit-logs-prompt</span>
<md-virtual-repeat-container ng-show="hasData()" flex md-top-index="topIndex" tb-scope-element="repeatContainer">
<md-list-item md-virtual-repeat="auditLog in theAuditLogs" md-on-demand flex ng-style="hasScroll() ? {'margin-right':'-15px'} : {}">
<md-list class="tb-row" flex layout="row" layout-align="start center" tb-audit-log-row audit-log-mode="{{auditLogMode}}" audit-log="{{auditLog}}">
</md-list>
<md-divider flex></md-divider>
</md-list-item>
</md-virtual-repeat-container>
</md-list>
</div>
</div>
</md-content>

View File

@ -0,0 +1,44 @@
/*
* 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.
*/
/* eslint-disable import/no-unresolved, import/default */
import auditLogsTemplate from './audit-logs.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
/*@ngInject*/
export default function AuditLogRoutes($stateProvider) {
$stateProvider
.state('home.auditLogs', {
url: '/auditLogs',
module: 'private',
auth: ['TENANT_ADMIN'],
views: {
"content@home": {
templateUrl: auditLogsTemplate,
controller: 'AuditLogsController',
controllerAs: 'vm'
}
},
data: {
searchEnabled: false,
pageTitle: 'audit-log.audit-logs'
},
ncyBreadcrumb: {
label: '{"icon": "track_changes", "label": "audit-log.audit-logs"}'
}
});
}

View File

@ -0,0 +1,91 @@
/**
* 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.
*/
.tb-audit-logs {
background-color: #fff;
.tb-audit-log-margin-18px {
margin-bottom: 18px;
}
.tb-audit-log-toolbar {
font-size: 20px;
}
md-input-container.tb-audit-log-search-input {
.md-errors-spacer {
min-height: 0px;
}
}
}
.tb-audit-log-container {
overflow-x: auto;
}
md-list.tb-audit-log-table {
padding: 0px;
min-width: 700px;
&.tb-audit-log-table-full {
min-width: 900px;
}
md-list-item {
padding: 0px;
}
.tb-row {
height: 48px;
padding: 0px;
overflow: hidden;
}
.tb-row:hover {
background-color: #EEEEEE;
}
.tb-header:hover {
background: none;
}
.tb-header {
.tb-cell {
color: rgba(0,0,0,.54);
font-size: 12px;
font-weight: 700;
white-space: nowrap;
background: none;
}
}
.tb-cell {
padding: 0 24px;
margin: auto 0;
color: rgba(0,0,0,.87);
font-size: 13px;
vertical-align: middle;
text-align: left;
overflow: hidden;
.md-button {
padding: 0;
margin: 0;
}
}
.tb-cell.tb-number {
text-align: right;
}
}

View File

@ -0,0 +1,24 @@
/*
* 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.
*/
/*@ngInject*/
export default function AuditLogsController(types) {
var vm = this;
vm.types = types;
}

View File

@ -0,0 +1,23 @@
<!--
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.
-->
<tb-audit-log-table class="md-whiteframe-z1"
flex
audit-log-mode="{{vm.types.auditLogMode.tenant}}"
page-mode="true">
</tb-audit-log-table>

31
ui/src/app/audit/index.js Normal file
View File

@ -0,0 +1,31 @@
/*
* 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.
*/
import AuditLogRoutes from './audit-log.routes';
import AuditLogsController from './audit-logs.controller';
import AuditLogDetailsDialogController from './audit-log-details-dialog.controller';
import AuditLogHeaderDirective from './audit-log-header.directive';
import AuditLogRowDirective from './audit-log-row.directive';
import AuditLogTableDirective from './audit-log-table.directive';
export default angular.module('thingsboard.auditLog', [])
.config(AuditLogRoutes)
.controller('AuditLogsController', AuditLogsController)
.controller('AuditLogDetailsDialogController', AuditLogDetailsDialogController)
.directive('tbAuditLogHeader', AuditLogHeaderDirective)
.directive('tbAuditLogRow', AuditLogRowDirective)
.directive('tbAuditLogTable', AuditLogTableDirective)
.name;

View File

@ -156,6 +156,63 @@ export default angular.module('thingsboard.types', [])
color: "green"
}
},
auditLogActionType: {
"ADDED": {
name: "audit-log.type-added"
},
"DELETED": {
name: "audit-log.type-deleted"
},
"UPDATED": {
name: "audit-log.type-updated"
},
"ATTRIBUTES_UPDATED": {
name: "audit-log.type-attributes-updated"
},
"ATTRIBUTES_DELETED": {
name: "audit-log.type-attributes-deleted"
},
"RPC_CALL": {
name: "audit-log.type-rpc-call"
},
"CREDENTIALS_UPDATED": {
name: "audit-log.type-credentials-updated"
},
"ASSIGNED_TO_CUSTOMER": {
name: "audit-log.type-assigned-to-customer"
},
"UNASSIGNED_FROM_CUSTOMER": {
name: "audit-log.type-unassigned-from-customer"
},
"ACTIVATED": {
name: "audit-log.type-activated"
},
"SUSPENDED": {
name: "audit-log.type-suspended"
},
"CREDENTIALS_READ": {
name: "audit-log.type-credentials-read"
},
"ATTRIBUTES_READ": {
name: "audit-log.type-attributes-read"
}
},
auditLogActionStatus: {
"SUCCESS": {
value: "SUCCESS",
name: "audit-log.status-success"
},
"FAILURE": {
value: "FAILURE",
name: "audit-log.status-failure"
}
},
auditLogMode: {
tenant: "tenant",
entity: "entity",
user: "user",
customer: "customer"
},
aliasFilterType: {
singleEntity: {
value: 'singleEntity',

View File

@ -125,7 +125,7 @@ function Grid() {
}
/*@ngInject*/
function GridController($scope, $state, $mdDialog, $document, $q, $mdUtil, $timeout, $translate, $mdMedia, $templateCache, $window) {
function GridController($scope, $state, $mdDialog, $document, $q, $mdUtil, $timeout, $translate, $mdMedia, $templateCache, $window, userService) {
var vm = this;
@ -157,6 +157,7 @@ function GridController($scope, $state, $mdDialog, $document, $q, $mdUtil, $time
vm.saveItem = saveItem;
vm.toggleItemSelection = toggleItemSelection;
vm.triggerResize = triggerResize;
vm.isTenantAdmin = isTenantAdmin;
$scope.$watch(function () {
return $mdMedia('xs') || $mdMedia('sm');
@ -634,6 +635,10 @@ function GridController($scope, $state, $mdDialog, $document, $q, $mdUtil, $time
w.triggerHandler('resize');
}
function isTenantAdmin() {
return userService.getAuthority() == 'TENANT_ADMIN';
}
function moveToTop() {
moveToIndex(0, true);
}

View File

@ -66,5 +66,10 @@
entity-type="{{vm.types.entityType.customer}}">
</tb-relation-table>
</md-tab>
<md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.grid.isTenantAdmin()" md-on-select="vm.grid.triggerResize()" label="{{ 'audit-log.audit-logs' | translate }}">
<tb-audit-log-table flex customer-id="vm.grid.operatingItem().id.id"
audit-log-mode="{{vm.types.auditLogMode.customer}}">
</tb-audit-log-table>
</md-tab>
</md-tabs>
</tb-grid>

View File

@ -19,13 +19,24 @@
<details-buttons tb-help="'dashboards'" help-container-id="help-container">
<div id="help-container"></div>
</details-buttons>
<tb-dashboard-details dashboard="vm.grid.operatingItem()"
is-edit="vm.grid.detailsConfig.isDetailsEditMode"
dashboard-scope="vm.dashboardsScope"
the-form="vm.grid.detailsForm"
on-assign-to-customer="vm.assignToCustomer(event, [ vm.grid.detailsConfig.currentItem.id.id ])"
on-make-public="vm.makePublic(event, vm.grid.detailsConfig.currentItem)"
on-unassign-from-customer="vm.unassignFromCustomer(event, vm.grid.detailsConfig.currentItem, isPublic)"
on-export-dashboard="vm.exportDashboard(event, vm.grid.detailsConfig.currentItem)"
on-delete-dashboard="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"></tb-dashboard-details>
<md-tabs ng-class="{'tb-headless': vm.grid.detailsConfig.isDetailsEditMode}"
id="tabs" md-border-bottom flex class="tb-absolute-fill">
<md-tab label="{{ 'dashboard.details' | translate }}">
<tb-dashboard-details dashboard="vm.grid.operatingItem()"
is-edit="vm.grid.detailsConfig.isDetailsEditMode"
dashboard-scope="vm.dashboardsScope"
the-form="vm.grid.detailsForm"
on-assign-to-customer="vm.assignToCustomer(event, [ vm.grid.detailsConfig.currentItem.id.id ])"
on-make-public="vm.makePublic(event, vm.grid.detailsConfig.currentItem)"
on-unassign-from-customer="vm.unassignFromCustomer(event, vm.grid.detailsConfig.currentItem, isPublic)"
on-export-dashboard="vm.exportDashboard(event, vm.grid.detailsConfig.currentItem)"
on-delete-dashboard="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"></tb-dashboard-details>
</md-tab>
<md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.grid.isTenantAdmin()" md-on-select="vm.grid.triggerResize()" label="{{ 'audit-log.audit-logs' | translate }}">
<tb-audit-log-table flex entity-type="vm.types.entityType.dashboard"
entity-id="vm.grid.operatingItem().id.id"
audit-log-mode="{{vm.types.auditLogMode.entity}}">
</tb-audit-log-table>
</md-tab>
</md-tabs>
</tb-grid>

View File

@ -39,10 +39,8 @@
<md-icon md-svg-icon="mdi:clipboard-arrow-left"></md-icon>
<span translate>device.copyId</span>
</md-button>
<md-button ngclipboard data-clipboard-action="copy"
ngclipboard-success="onAccessTokenCopied(e)"
data-clipboard-text="{{deviceCredentials.credentialsId}}" ng-show="!isEdit"
class="md-raised">
<md-button ng-show="!isEdit"
class="md-raised" ng-click="copyAccessToken($event)">
<md-icon md-svg-icon="mdi:clipboard-arrow-left"></md-icon>
<span translate>device.copyAccessToken</span>
</md-button>

View File

@ -20,7 +20,7 @@ import deviceFieldsetTemplate from './device-fieldset.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
/*@ngInject*/
export default function DeviceDirective($compile, $templateCache, toast, $translate, types, deviceService, customerService) {
export default function DeviceDirective($compile, $templateCache, toast, $translate, types, clipboardService, deviceService, customerService) {
var linker = function (scope, element) {
var template = $templateCache.get(deviceFieldsetTemplate);
element.html(template);
@ -30,17 +30,8 @@ export default function DeviceDirective($compile, $templateCache, toast, $transl
scope.isPublic = false;
scope.assignedCustomer = null;
scope.deviceCredentials = null;
scope.$watch('device', function(newVal) {
if (newVal) {
if (scope.device.id) {
deviceService.getDeviceCredentials(scope.device.id.id).then(
function success(credentials) {
scope.deviceCredentials = credentials;
}
);
}
if (scope.device.customerId && scope.device.customerId.id !== types.id.nullUid) {
scope.isAssignedToCustomer = true;
customerService.getShortCustomerInfo(scope.device.customerId.id).then(
@ -61,8 +52,20 @@ export default function DeviceDirective($compile, $templateCache, toast, $transl
toast.showSuccess($translate.instant('device.idCopiedMessage'), 750, angular.element(element).parent().parent(), 'bottom left');
};
scope.onAccessTokenCopied = function() {
toast.showSuccess($translate.instant('device.accessTokenCopiedMessage'), 750, angular.element(element).parent().parent(), 'bottom left');
scope.copyAccessToken = function(e) {
const trigger = e.delegateTarget || e.currentTarget;
if (scope.device.id) {
deviceService.getDeviceCredentials(scope.device.id.id, true).then(
function success(credentials) {
var credentialsId = credentials.credentialsId;
clipboardService.copyToClipboard(trigger, credentialsId).then(
() => {
toast.showSuccess($translate.instant('device.accessTokenCopiedMessage'), 750, angular.element(element).parent().parent(), 'bottom left');
}
);
}
);
}
};
$compile(element.contents())(scope);

View File

@ -74,4 +74,10 @@
entity-type="{{vm.types.entityType.device}}">
</tb-extension-table>
</md-tab>
<md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.grid.isTenantAdmin()" md-on-select="vm.grid.triggerResize()" label="{{ 'audit-log.audit-logs' | translate }}">
<tb-audit-log-table flex entity-type="vm.types.entityType.device"
entity-id="vm.grid.operatingItem().id.id"
audit-log-mode="{{vm.types.auditLogMode.entity}}">
</tb-audit-log-table>
</md-tab>
</tb-grid>

View File

@ -26,9 +26,16 @@
</md-select>
</md-input-container>
<tb-timewindow flex ng-model="timewindow" history-only as-button="true"></tb-timewindow>
<md-button ng-disabled="$root.loading"
class="md-icon-button" ng-click="reload()">
<md-icon>refresh</md-icon>
<md-tooltip md-direction="top">
{{ 'action.refresh' | translate }}
</md-tooltip>
</md-button>
</section>
<md-list flex layout="column" class="md-whiteframe-z1 tb-event-table">
<md-list class="tb-row tb-header" layout="row" tb-event-header event-type="{{eventType}}">
<md-list class="tb-row tb-header" layout="row" layout-align="start center" tb-event-header event-type="{{eventType}}">
</md-list>
<md-progress-linear style="max-height: 0px;" md-mode="indeterminate" ng-disabled="!$root.loading"
ng-show="$root.loading"></md-progress-linear>
@ -38,7 +45,7 @@
class="tb-prompt" ng-show="noData()">event.no-events-prompt</span>
<md-virtual-repeat-container ng-show="hasData()" flex md-top-index="topIndex" tb-scope-element="repeatContainer">
<md-list-item md-virtual-repeat="event in theEvents" md-on-demand flex ng-style="hasScroll() ? {'margin-right':'-15px'} : {}">
<md-list class="tb-row" flex layout="row" tb-event-row event-type="{{eventType}}" event="{{event}}">
<md-list class="tb-row" flex layout="row" layout-align="start center" tb-event-row event-type="{{eventType}}" event="{{event}}">
</md-list>
<md-divider flex></md-divider>
</md-list-item>

View File

@ -35,6 +35,7 @@ import thingsboardUserMenu from './user-menu.directive';
import thingsboardEntity from '../entity';
import thingsboardEvent from '../event';
import thingsboardAlarm from '../alarm';
import thingsboardAuditLog from '../audit';
import thingsboardExtension from '../extension';
import thingsboardTenant from '../tenant';
import thingsboardCustomer from '../customer';
@ -67,6 +68,7 @@ export default angular.module('thingsboard.home', [
thingsboardEntity,
thingsboardEvent,
thingsboardAlarm,
thingsboardAuditLog,
thingsboardExtension,
thingsboardTenant,
thingsboardCustomer,

View File

@ -286,6 +286,38 @@ export default angular.module('thingsboard.locale', [])
"selected-attributes": "{ count, select, 1 {1 attribute} other {# attributes} } selected",
"selected-telemetry": "{ count, select, 1 {1 telemetry unit} other {# telemetry units} } selected"
},
"audit-log": {
"audit": "Audit",
"audit-logs": "Audit Logs",
"timestamp": "Timestamp",
"entity-type": "Entity Type",
"entity-name": "Entity Name",
"user": "User",
"type": "Type",
"status": "Status",
"details": "Details",
"type-added": "Added",
"type-deleted": "Deleted",
"type-updated": "Updated",
"type-attributes-updated": "Attributes updated",
"type-attributes-deleted": "Attributes deleted",
"type-rpc-call": "RPC call",
"type-credentials-updated": "Credentials updated",
"type-assigned-to-customer": "Assigned to Customer",
"type-unassigned-from-customer": "Unassigned from Customer",
"type-activated": "Activated",
"type-suspended": "Suspended",
"type-credentials-read": "Credentials read",
"type-attributes-read": "Attributes read",
"status-success": "Success",
"status-failure": "Failure",
"audit-log-details": "Audit log details",
"no-audit-logs-prompt": "No logs found",
"action-data": "Action data",
"failure-details": "Failure details",
"search": "Search audit logs",
"clear-search": "Clear search"
},
"confirm-on-exit": {
"message": "You have unsaved changes. Are you sure you want to leave this page?",
"html-message": "You have unsaved changes.<br/>Are you sure you want to leave this page?",
@ -1183,7 +1215,8 @@ export default angular.module('thingsboard.locale', [])
"activation-link": "User activation link",
"activation-link-text": "In order to activate user use the following <a href='{{activationLink}}' target='_blank'>activation link</a> :",
"copy-activation-link": "Copy activation link",
"activation-link-copied-message": "User activation link has been copied to clipboard"
"activation-link-copied-message": "User activation link has been copied to clipboard",
"details": "Details"
},
"value": {
"type": "Value type",

View File

@ -66,5 +66,12 @@
entity-type="{{vm.types.entityType.plugin}}">
</tb-relation-table>
</md-tab>
<md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isPluginEditable(vm.grid.operatingItem()) && vm.grid.isTenantAdmin()"
md-on-select="vm.grid.triggerResize()" label="{{ 'audit-log.audit-logs' | translate }}">
<tb-audit-log-table flex entity-type="vm.types.entityType.plugin"
entity-id="vm.grid.operatingItem().id.id"
audit-log-mode="{{vm.types.auditLogMode.entity}}">
</tb-audit-log-table>
</md-tab>
</md-tabs>
</tb-grid>

View File

@ -66,5 +66,12 @@
entity-type="{{vm.types.entityType.rule}}">
</tb-relation-table>
</md-tab>
<md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isRuleEditable(vm.grid.operatingItem()) && vm.grid.isTenantAdmin()"
md-on-select="vm.grid.triggerResize()" label="{{ 'audit-log.audit-logs' | translate }}">
<tb-audit-log-table flex entity-type="vm.types.entityType.rule"
entity-id="vm.grid.operatingItem().id.id"
audit-log-mode="{{vm.types.auditLogMode.entity}}">
</tb-audit-log-table>
</md-tab>
</md-tabs>
</tb-grid>

View File

@ -0,0 +1,128 @@
/*
* 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.
*/
export default angular.module('thingsboard.clipboard', [])
.factory('clipboardService', ClipboardService)
.name;
/*@ngInject*/
function ClipboardService($q) {
var fakeHandler, fakeHandlerCallback, fakeElem;
var service = {
copyToClipboard: copyToClipboard
};
return service;
/* eslint-disable */
function copyToClipboard(trigger, text) {
var deferred = $q.defer();
const isRTL = document.documentElement.getAttribute('dir') == 'rtl';
removeFake();
fakeHandlerCallback = () => removeFake();
fakeHandler = document.body.addEventListener('click', fakeHandlerCallback) || true;
fakeElem = document.createElement('textarea');
fakeElem.style.fontSize = '12pt';
fakeElem.style.border = '0';
fakeElem.style.padding = '0';
fakeElem.style.margin = '0';
fakeElem.style.position = 'absolute';
fakeElem.style[ isRTL ? 'right' : 'left' ] = '-9999px';
let yPosition = window.pageYOffset || document.documentElement.scrollTop;
fakeElem.style.top = `${yPosition}px`;
fakeElem.setAttribute('readonly', '');
fakeElem.value = text;
document.body.appendChild(fakeElem);
var selectedText = select(fakeElem);
let succeeded;
try {
succeeded = document.execCommand('copy');
}
catch (err) {
succeeded = false;
}
if (trigger) {
trigger.focus();
}
window.getSelection().removeAllRanges();
removeFake();
if (succeeded) {
deferred.resolve(selectedText);
} else {
deferred.reject();
}
return deferred.promise;
}
function removeFake() {
if (fakeHandler) {
document.body.removeEventListener('click', fakeHandlerCallback);
fakeHandler = null;
fakeHandlerCallback = null;
}
if (fakeElem) {
document.body.removeChild(fakeElem);
fakeElem = null;
}
}
function select(element) {
var selectedText;
if (element.nodeName === 'SELECT') {
element.focus();
selectedText = element.value;
}
else if (element.nodeName === 'INPUT' || element.nodeName === 'TEXTAREA') {
var isReadOnly = element.hasAttribute('readonly');
if (!isReadOnly) {
element.setAttribute('readonly', '');
}
element.select();
element.setSelectionRange(0, element.value.length);
if (!isReadOnly) {
element.removeAttribute('readonly');
}
selectedText = element.value;
}
else {
if (element.hasAttribute('contenteditable')) {
element.focus();
}
var selection = window.getSelection();
var range = document.createRange();
range.selectNodeContents(element);
selection.removeAllRanges();
selection.addRange(range);
selectedText = selection.toString();
}
return selectedText;
}
/* eslint-enable */
}

View File

@ -211,6 +211,12 @@ function Menu(userService, $state, $rootScope) {
type: 'link',
state: 'home.dashboards',
icon: 'dashboards'
},
{
name: 'audit-log.audit-logs',
type: 'link',
state: 'home.auditLogs',
icon: 'track_changes'
}];
homeSections =
@ -273,6 +279,16 @@ function Menu(userService, $state, $rootScope) {
state: 'home.dashboards'
}
]
},
{
name: 'audit-log.audit',
places: [
{
name: 'audit-log.audit-logs',
icon: 'track_changes',
state: 'home.auditLogs'
}
]
}];
} else if (authority === 'CUSTOMER_USER') {

View File

@ -42,6 +42,8 @@ export default function UserController(userService, toast, $scope, $mdDialog, $d
var vm = this;
vm.types = types;
vm.userGridConfig = {
deleteItemTitleFunc: deleteUserTitle,
deleteItemContentFunc: deleteUserText,

View File

@ -19,10 +19,20 @@
<details-buttons tb-help="'users'" help-container-id="help-container">
<div id="help-container"></div>
</details-buttons>
<tb-user user="vm.grid.operatingItem()"
is-edit="vm.grid.detailsConfig.isDetailsEditMode"
the-form="vm.grid.detailsForm"
on-display-activation-link="vm.displayActivationLink(event, vm.grid.detailsConfig.currentItem)"
on-resend-activation="vm.resendActivation(vm.grid.detailsConfig.currentItem)"
on-delete-user="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"></tb-user>
<md-tabs ng-class="{'tb-headless': vm.grid.detailsConfig.isDetailsEditMode}"
id="tabs" md-border-bottom flex class="tb-absolute-fill">
<md-tab label="{{ 'user.details' | translate }}">
<tb-user user="vm.grid.operatingItem()"
is-edit="vm.grid.detailsConfig.isDetailsEditMode"
the-form="vm.grid.detailsForm"
on-display-activation-link="vm.displayActivationLink(event, vm.grid.detailsConfig.currentItem)"
on-resend-activation="vm.resendActivation(vm.grid.detailsConfig.currentItem)"
on-delete-user="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"></tb-user>
</md-tab>
<md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.grid.isTenantAdmin()" md-on-select="vm.grid.triggerResize()" label="{{ 'audit-log.audit-logs' | translate }}">
<tb-audit-log-table flex user-id="vm.grid.operatingItem().id.id"
audit-log-mode="{{vm.types.auditLogMode.user}}">
</tb-audit-log-table>
</md-tab>
</md-tabs>
</tb-grid>

View File

@ -203,6 +203,19 @@ md-sidenav {
* THINGSBOARD SPECIFIC
***********************/
label {
&.tb-title {
pointer-events: none;
color: #666;
font-size: 13px;
font-weight: 400;
padding-bottom: 15px;
&.no-padding {
padding-bottom: 0px;
}
}
}
.tb-noselect {
-webkit-touch-callout: none; /* iOS Safari */
-webkit-user-select: none; /* Safari */