From a5f44729e54f11a16d0bd880129759327a816293 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Wed, 21 Feb 2018 20:05:03 +0200 Subject: [PATCH] Implement Audit Logs --- .../main/data/upgrade/1.4.0/schema_update.sql | 4 +- .../server/actors/ActorSystemContext.java | 4 + .../plugin/PluginProcessingContext.java | 74 ++++- .../plugin/SharedPluginProcessingContext.java | 3 + .../actors/rpc/BasicRpcSessionListener.java | 2 +- .../config/AuditLogLevelProperties.java | 43 +++ .../ThingsboardSecurityConfiguration.java | 6 + .../server/controller/AssetController.java | 67 ++++- .../server/controller/BaseController.java | 69 +---- .../server/controller/CustomerController.java | 27 +- .../controller/DashboardController.java | 71 ++++- .../server/controller/DeviceController.java | 78 +++++- .../server/controller/PluginController.java | 42 +++ .../server/controller/RuleController.java | 44 +++ .../server/controller/UserController.java | 22 +- .../plugin/PluginApiController.java | 6 +- .../plugin/PluginWebSocketHandler.java | 5 +- .../src/main/resources/thingsboard.yml | 14 +- .../server/common/data/audit/ActionType.java | 25 +- .../server/dao/audit/AuditLogLevelFilter.java | 46 +++ .../server/dao/audit/AuditLogLevelMask.java | 34 +++ .../server/dao/audit/AuditLogService.java | 25 +- .../server/dao/audit/AuditLogServiceImpl.java | 183 ++++++++++-- .../dao/audit/DummyAuditLogServiceImpl.java | 12 +- .../dao/sql/audit/AuditLogRepository.java | 40 +-- .../server/dao/sql/audit/JpaAuditLogDao.java | 72 +++-- dao/src/main/resources/sql/schema.sql | 4 +- .../dao/service/AbstractServiceTest.java | 16 ++ .../resources/application-test.properties | 1 - .../plugins/PluginApiCallSecurityContext.java | 20 +- .../extensions/api/plugins/PluginContext.java | 11 +- .../api/plugins/msg/ToDeviceRpcRequest.java | 2 + .../DeviceMessagingRuleMsgHandler.java | 2 +- .../core/plugin/rpc/RpcManager.java | 4 +- .../rpc/handlers/RpcRestMsgHandler.java | 10 +- .../rpc/handlers/RpcRuleMsgHandler.java | 2 +- .../handlers/TelemetryRestMsgHandler.java | 27 +- ui/src/app/alarm/alarm-table.tpl.html | 4 +- ui/src/app/api/audit-log.service.js | 116 ++++++++ ui/src/app/api/device.service.js | 27 +- ui/src/app/api/user.service.js | 9 + ui/src/app/app.js | 4 + ui/src/app/asset/assets.tpl.html | 6 + .../audit-log-details-dialog.controller.js | 103 +++++++ .../app/audit/audit-log-details-dialog.scss | 23 ++ .../audit/audit-log-details-dialog.tpl.html | 49 ++++ .../app/audit/audit-log-header.directive.js | 41 +++ ui/src/app/audit/audit-log-header.tpl.html | 24 ++ ui/src/app/audit/audit-log-row.directive.js | 67 +++++ ui/src/app/audit/audit-log-row.tpl.html | 36 +++ ui/src/app/audit/audit-log-table.directive.js | 262 ++++++++++++++++++ ui/src/app/audit/audit-log-table.tpl.html | 68 +++++ ui/src/app/audit/audit-log.routes.js | 44 +++ ui/src/app/audit/audit-log.scss | 91 ++++++ ui/src/app/audit/audit-logs.controller.js | 24 ++ ui/src/app/audit/audit-logs.tpl.html | 23 ++ ui/src/app/audit/index.js | 31 +++ ui/src/app/common/types.constant.js | 57 ++++ ui/src/app/components/grid.directive.js | 7 +- ui/src/app/customer/customers.tpl.html | 5 + ui/src/app/dashboard/dashboards.tpl.html | 29 +- ui/src/app/device/device-fieldset.tpl.html | 6 +- ui/src/app/device/device.directive.js | 27 +- ui/src/app/device/devices.tpl.html | 6 + ui/src/app/event/event-table.tpl.html | 11 +- ui/src/app/layout/index.js | 2 + ui/src/app/locale/locale.constant.js | 35 ++- ui/src/app/plugin/plugins.tpl.html | 7 + ui/src/app/rule/rules.tpl.html | 7 + ui/src/app/services/clipboard.service.js | 128 +++++++++ ui/src/app/services/menu.service.js | 16 ++ ui/src/app/user/user.controller.js | 2 + ui/src/app/user/users.tpl.html | 22 +- ui/src/scss/main.scss | 13 + 74 files changed, 2280 insertions(+), 269 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/config/AuditLogLevelProperties.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogLevelFilter.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogLevelMask.java create mode 100644 ui/src/app/api/audit-log.service.js create mode 100644 ui/src/app/audit/audit-log-details-dialog.controller.js create mode 100644 ui/src/app/audit/audit-log-details-dialog.scss create mode 100644 ui/src/app/audit/audit-log-details-dialog.tpl.html create mode 100644 ui/src/app/audit/audit-log-header.directive.js create mode 100644 ui/src/app/audit/audit-log-header.tpl.html create mode 100644 ui/src/app/audit/audit-log-row.directive.js create mode 100644 ui/src/app/audit/audit-log-row.tpl.html create mode 100644 ui/src/app/audit/audit-log-table.directive.js create mode 100644 ui/src/app/audit/audit-log-table.tpl.html create mode 100644 ui/src/app/audit/audit-log.routes.js create mode 100644 ui/src/app/audit/audit-log.scss create mode 100644 ui/src/app/audit/audit-logs.controller.js create mode 100644 ui/src/app/audit/audit-logs.tpl.html create mode 100644 ui/src/app/audit/index.js create mode 100644 ui/src/app/services/clipboard.service.js diff --git a/application/src/main/data/upgrade/1.4.0/schema_update.sql b/application/src/main/data/upgrade/1.4.0/schema_update.sql index 0d031bf0a4..b2e3a9720f 100644 --- a/application/src/main/data/upgrade/1.4.0/schema_update.sql +++ b/application/src/main/data/upgrade/1.4.0/schema_update.sql @@ -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) ); diff --git a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java index f45028a70e..a42bcab3b1 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java @@ -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; diff --git a/application/src/main/java/org/thingsboard/server/actors/plugin/PluginProcessingContext.java b/application/src/main/java/org/thingsboard/server/actors/plugin/PluginProcessingContext.java index 3f0d39f99e..762bb64471 100644 --- a/application/src/main/java/org/thingsboard/server/actors/plugin/PluginProcessingContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/plugin/PluginProcessingContext.java @@ -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 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 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 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 keys, final PluginCallback> 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, 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); diff --git a/application/src/main/java/org/thingsboard/server/actors/plugin/SharedPluginProcessingContext.java b/application/src/main/java/org/thingsboard/server/actors/plugin/SharedPluginProcessingContext.java index de859a205e..a1cc83542d 100644 --- a/application/src/main/java/org/thingsboard/server/actors/plugin/SharedPluginProcessingContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/plugin/SharedPluginProcessingContext.java @@ -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() { diff --git a/application/src/main/java/org/thingsboard/server/actors/rpc/BasicRpcSessionListener.java b/application/src/main/java/org/thingsboard/server/actors/rpc/BasicRpcSessionListener.java index 6250897b0c..72887cf8a0 100644 --- a/application/src/main/java/org/thingsboard/server/actors/rpc/BasicRpcSessionListener.java +++ b/application/src/main/java/org/thingsboard/server/actors/rpc/BasicRpcSessionListener.java @@ -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); } diff --git a/application/src/main/java/org/thingsboard/server/config/AuditLogLevelProperties.java b/application/src/main/java/org/thingsboard/server/config/AuditLogLevelProperties.java new file mode 100644 index 0000000000..ce622aa2d8 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/config/AuditLogLevelProperties.java @@ -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 mask = new HashMap<>(); + + public AuditLogLevelProperties() { + super(); + } + + public void setMask(Map mask) { + this.mask = mask; + } + + public Map getMask() { + return this.mask; + } +} diff --git a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java index 6755408b0c..833c337578 100644 --- a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java +++ b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java @@ -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()); + } } diff --git a/application/src/main/java/org/thingsboard/server/controller/AssetController.java b/application/src/main/java/org/thingsboard/server/controller/AssetController.java index 194b7d89d4..4790b75169 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AssetController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AssetController.java @@ -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); } } diff --git a/application/src/main/java/org/thingsboard/server/controller/BaseController.java b/application/src/main/java/org/thingsboard/server/controller/BaseController.java index 96343cbbd0..c99a65a83d 100644 --- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java +++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java @@ -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 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 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 & 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); - } + } diff --git a/application/src/main/java/org/thingsboard/server/controller/CustomerController.java b/application/src/main/java/org/thingsboard/server/controller/CustomerController.java index d87ea5cfe0..ee3fbfc7f0 100644 --- a/application/src/main/java/org/thingsboard/server/controller/CustomerController.java +++ b/application/src/main/java/org/thingsboard/server/controller/CustomerController.java @@ -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); } } diff --git a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java index 7bc05d71d6..34320b12df 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java @@ -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); } } diff --git a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java index 65fcaeaa88..18ddfe51e5 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java @@ -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); } } diff --git a/application/src/main/java/org/thingsboard/server/controller/PluginController.java b/application/src/main/java/org/thingsboard/server/controller/PluginController.java index 191fc6b861..6217fefa46 100644 --- a/application/src/main/java/org/thingsboard/server/controller/PluginController.java +++ b/application/src/main/java/org/thingsboard/server/controller/PluginController.java @@ -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); } } diff --git a/application/src/main/java/org/thingsboard/server/controller/RuleController.java b/application/src/main/java/org/thingsboard/server/controller/RuleController.java index 84d1c8f92e..83e3b4bd3f 100644 --- a/application/src/main/java/org/thingsboard/server/controller/RuleController.java +++ b/application/src/main/java/org/thingsboard/server/controller/RuleController.java @@ -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); } } diff --git a/application/src/main/java/org/thingsboard/server/controller/UserController.java b/application/src/main/java/org/thingsboard/server/controller/UserController.java index 05fca0b1d0..95f5fdd514 100644 --- a/application/src/main/java/org/thingsboard/server/controller/UserController.java +++ b/application/src/main/java/org/thingsboard/server/controller/UserController.java @@ -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); } } diff --git a/application/src/main/java/org/thingsboard/server/controller/plugin/PluginApiController.java b/application/src/main/java/org/thingsboard/server/controller/plugin/PluginApiController.java index 1789da6f28..145b0468d5 100644 --- a/application/src/main/java/org/thingsboard/server/controller/plugin/PluginApiController.java +++ b/application/src/main/java/org/thingsboard/server/controller/plugin/PluginApiController.java @@ -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)); diff --git a/application/src/main/java/org/thingsboard/server/controller/plugin/PluginWebSocketHandler.java b/application/src/main/java/org/thingsboard/server/controller/plugin/PluginWebSocketHandler.java index 727fd30f87..1312373e9e 100644 --- a/application/src/main/java/org/thingsboard/server/controller/plugin/PluginWebSocketHandler.java +++ b/application/src/main/java/org/thingsboard/server/controller/plugin/PluginWebSocketHandler.java @@ -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 { diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index ca7fbdf7ed..7ea68c73d3 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -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}" \ No newline at end of file + # 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" diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java b/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java index 495f80dc6f..7e1a976fdd 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java @@ -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 -} \ No newline at end of file + 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; + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogLevelFilter.java b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogLevelFilter.java new file mode 100644 index 0000000000..0ae22e2cf4 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogLevelFilter.java @@ -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 entityTypeMask = new HashMap<>(); + + public AuditLogLevelFilter(Map 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; + } + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogLevelMask.java b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogLevelMask.java new file mode 100644 index 0000000000..ba5efb497f --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogLevelMask.java @@ -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; + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogService.java b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogService.java index d6e61abcc2..86cbbba0df 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogService.java @@ -17,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 findAuditLogsByTenantId(TenantId tenantId, TimePageLink pageLink); - ListenableFuture> logEntityAction(User user, - EntityId entityId, - String entityName, - CustomerId customerId, - ActionType actionType, - JsonNode actionData, - ActionStatus actionStatus, - String actionFailureDetails); + & HasName, + I extends UUIDBased & EntityId> ListenableFuture> logEntityAction( + TenantId tenantId, + CustomerId customerId, + UserId userId, + String userName, + I entityId, + E entity, + ActionType actionType, + Exception e, Object... additionalInfo); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java index 1ee26e5dd8..ab1c313fe2 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java @@ -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 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> 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 & HasName, I extends UUIDBased & EntityId> ListenableFuture> + 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 & 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 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 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 extractParameter(Class clazz, Object... additionalInfo) { + return extractParameter(clazz, 0, additionalInfo); + } + + private T extractParameter(Class 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, diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/DummyAuditLogServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/audit/DummyAuditLogServiceImpl.java index 585dadff2e..885cd2f30b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/audit/DummyAuditLogServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/audit/DummyAuditLogServiceImpl.java @@ -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> logEntityAction(User user, EntityId entityId, String entityName, CustomerId customerId, ActionType actionType, JsonNode actionData, ActionStatus actionStatus, String actionFailureDetails) { - return Futures.immediateFuture(Collections.emptyList()); + public & HasName, I extends UUIDBased & EntityId> ListenableFuture> logEntityAction(TenantId tenantId, CustomerId customerId, UserId userId, String userName, I entityId, E entity, ActionType actionType, Exception e, Object... additionalInfo) { + return null; } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/audit/AuditLogRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/audit/AuditLogRepository.java index bac44c5121..52dbd670c6 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/audit/AuditLogRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/audit/AuditLogRepository.java @@ -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, JpaSpecificationExecutor { -public interface AuditLogRepository extends CrudRepository { - - @Query("SELECT al FROM AuditLogEntity al WHERE al.tenantId = :tenantId " + - "AND al.id > :idOffset ORDER BY al.id") - List 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 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 findByTenantIdAndCustomerId(@Param("tenantId") String tenantId, - @Param("customerId") String customerId, - @Param("idOffset") String idOffset, - Pageable pageable); - - @Query("SELECT al FROM AuditLogEntity al WHERE al.tenantId = :tenantId " + - "AND al.userId = :userId " + - "AND al.id > :idOffset ORDER BY al.id") - List findByTenantIdAndUserId(@Param("tenantId") String tenantId, - @Param("userId") String userId, - @Param("idOffset") String idOffset, - Pageable pageable); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/audit/JpaAuditLogDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/audit/JpaAuditLogDao.java index b4abf07416..14b5271c7c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/audit/JpaAuditLogDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/audit/JpaAuditLogDao.java @@ -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 imp @Override public List 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 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 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 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 findAuditLogs(UUID tenantId, EntityId entityId, CustomerId customerId, UserId userId, TimePageLink pageLink) { + Specification timeSearchSpec = JpaAbstractSearchTimeDao.getTimeSearchPageSpec(pageLink, "id"); + Specification 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 getEntityFieldsSpec(UUID tenantId, EntityId entityId, CustomerId customerId, UserId userId) { + return (root, criteriaQuery, criteriaBuilder) -> { + List 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[]{})); + }; } } diff --git a/dao/src/main/resources/sql/schema.sql b/dao/src/main/resources/sql/schema.sql index 6d6e82542e..1f739f8ea3 100644 --- a/dao/src/main/resources/sql/schema.sql +++ b/dao/src/main/resources/sql/schema.sql @@ -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 ( diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java index e0b5f24e0c..7fc9c67522 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java @@ -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 mask = new HashMap<>(); + for (EntityType entityType : EntityType.values()) { + mask.put(entityType.name().toLowerCase(), AuditLogLevelMask.RW.name()); + } + return new AuditLogLevelFilter(mask); + } + } diff --git a/dao/src/test/resources/application-test.properties b/dao/src/test/resources/application-test.properties index 793f9b6038..21f1794232 100644 --- a/dao/src/test/resources/application-test.properties +++ b/dao/src/test/resources/application-test.properties @@ -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 diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginApiCallSecurityContext.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginApiCallSecurityContext.java index 32600d36c6..3ca662d747 100644 --- a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginApiCallSecurityContext.java +++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginApiCallSecurityContext.java @@ -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; + } + } diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginContext.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginContext.java index c825594ec5..d1a2927f92 100644 --- a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginContext.java +++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginContext.java @@ -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, Exception e); /* Websocket API @@ -96,6 +95,12 @@ public interface PluginContext { Attributes API */ + void logAttributesUpdated(PluginApiCallSecurityContext ctx, EntityId entityId, String attributeType, List attributes, Exception e); + + void logAttributesDeleted(PluginApiCallSecurityContext ctx, EntityId entityId, String attributeType, List keys, Exception e); + + void logAttributesRead(PluginApiCallSecurityContext ctx, EntityId entityId, String attributeType, List keys, Exception e); + void saveAttributes(TenantId tenantId, EntityId entityId, String attributeType, List attributes, PluginCallback callback); void removeAttributes(TenantId tenantId, EntityId entityId, String scope, List attributeKeys, PluginCallback callback); diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/ToDeviceRpcRequest.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/ToDeviceRpcRequest.java index f9269de0d3..b81780b30f 100644 --- a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/ToDeviceRpcRequest.java +++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/ToDeviceRpcRequest.java @@ -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; diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/messaging/DeviceMessagingRuleMsgHandler.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/messaging/DeviceMessagingRuleMsgHandler.java index 4684337a4a..7438a27a4b 100644 --- a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/messaging/DeviceMessagingRuleMsgHandler.java +++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/messaging/DeviceMessagingRuleMsgHandler.java @@ -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); } diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/RpcManager.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/RpcManager.java index 25f7238104..193d52814d 100644 --- a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/RpcManager.java +++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/RpcManager.java @@ -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); } } } diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/handlers/RpcRestMsgHandler.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/handlers/RpcRestMsgHandler.java index f88b796bf4..476154d378 100644 --- a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/handlers/RpcRestMsgHandler.java +++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/handlers/RpcRestMsgHandler.java @@ -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() { @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 responseWriter, FromDeviceRpcResponse response) { + public void reply(PluginContext ctx, ToDeviceRpcRequest rpcRequest, DeferredResult responseWriter, FromDeviceRpcResponse response) { Optional 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)); } } diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/handlers/RpcRuleMsgHandler.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/handlers/RpcRuleMsgHandler.java index f193c015da..7db2acb16f 100644 --- a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/handlers/RpcRuleMsgHandler.java +++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/handlers/RpcRuleMsgHandler.java @@ -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); } diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRestMsgHandler.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRestMsgHandler.java index 7e92fc15f1..1fa034b37e 100644 --- a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRestMsgHandler.java +++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRestMsgHandler.java @@ -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> callback = getAttributeValuesPluginCallback(msg); + List keyList = null; + if (!StringUtils.isEmpty(keys)) { + keyList = Arrays.asList(keys.split(",")); + } + PluginCallback> callback = getAttributeValuesPluginCallback(msg, scope, entityId, keyList); if (!StringUtils.isEmpty(scope)) { - if (!StringUtils.isEmpty(keys)) { - List 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 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() { @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() { + List keyList = Arrays.asList(keys); + ctx.removeAttributes(ctx.getSecurityCtx().orElseThrow(IllegalArgumentException::new).getTenantId(), entityId, scope, keyList, new PluginCallback() { @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> getAttributeValuesPluginCallback(final PluginRestMsg msg) { + private PluginCallback> getAttributeValuesPluginCallback(final PluginRestMsg msg, final String scope, + final EntityId entityId, final List keyList) { return new PluginCallback>() { @Override public void onSuccess(PluginContext ctx, List attributes) { List 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); } }; diff --git a/ui/src/app/alarm/alarm-table.tpl.html b/ui/src/app/alarm/alarm-table.tpl.html index 98d42a2a93..361b9ba23a 100644 --- a/ui/src/app/alarm/alarm-table.tpl.html +++ b/ui/src/app/alarm/alarm-table.tpl.html @@ -29,7 +29,7 @@
- + @@ -39,7 +39,7 @@ class="tb-prompt" ng-show="noData()">alarm.no-alarms-prompt - + diff --git a/ui/src/app/api/audit-log.service.js b/ui/src/app/api/audit-log.service.js new file mode 100644 index 0000000000..b8f73b9cab --- /dev/null +++ b/ui/src/app/api/audit-log.service.js @@ -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; + } + +} diff --git a/ui/src/app/api/device.service.js b/ui/src/app/api/device.service.js index bb4249e190..f2ae8cd30c 100644 --- a/ui/src/app/api/device.service.js +++ b/ui/src/app/api/device.service.js @@ -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; } diff --git a/ui/src/app/api/user.service.js b/ui/src/app/api/user.service.js index 1a146b4e00..ce5e673f18 100644 --- a/ui/src/app/api/user.service.js +++ b/ui/src/app/api/user.service.js @@ -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; diff --git a/ui/src/app/app.js b/ui/src/app/app.js index 5b1908928e..23f69330ba 100644 --- a/ui/src/app/app.js +++ b/ui/src/app/app.js @@ -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) diff --git a/ui/src/app/asset/assets.tpl.html b/ui/src/app/asset/assets.tpl.html index c4c0267993..f16e1cdd73 100644 --- a/ui/src/app/asset/assets.tpl.html +++ b/ui/src/app/asset/assets.tpl.html @@ -66,4 +66,10 @@ entity-type="{{vm.types.entityType.asset}}"> + + + + diff --git a/ui/src/app/audit/audit-log-details-dialog.controller.js b/ui/src/app/audit/audit-log-details-dialog.controller.js new file mode 100644 index 0000000000..88cb10dc78 --- /dev/null +++ b/ui/src/app/audit/audit-log-details-dialog.controller.js @@ -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 */ diff --git a/ui/src/app/audit/audit-log-details-dialog.scss b/ui/src/app/audit/audit-log-details-dialog.scss new file mode 100644 index 0000000000..83aaf40f61 --- /dev/null +++ b/ui/src/app/audit/audit-log-details-dialog.scss @@ -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; +} \ No newline at end of file diff --git a/ui/src/app/audit/audit-log-details-dialog.tpl.html b/ui/src/app/audit/audit-log-details-dialog.tpl.html new file mode 100644 index 0000000000..b1e1e63864 --- /dev/null +++ b/ui/src/app/audit/audit-log-details-dialog.tpl.html @@ -0,0 +1,49 @@ + + + +
+

audit-log.audit-log-details

+ + + + +
+
+ +
+ +
+
+ + +
+
+
+
+ + + {{ 'action.close' | + translate }} + + +
diff --git a/ui/src/app/audit/audit-log-header.directive.js b/ui/src/app/audit/audit-log-header.directive.js new file mode 100644 index 0000000000..504e96c37f --- /dev/null +++ b/ui/src/app/audit/audit-log-header.directive.js @@ -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 + }; +} diff --git a/ui/src/app/audit/audit-log-header.tpl.html b/ui/src/app/audit/audit-log-header.tpl.html new file mode 100644 index 0000000000..6927ad78c9 --- /dev/null +++ b/ui/src/app/audit/audit-log-header.tpl.html @@ -0,0 +1,24 @@ + +
audit-log.timestamp
+
audit-log.entity-type
+
audit-log.entity-name
+
audit-log.user
+
audit-log.type
+
audit-log.status
+
audit-log.details
diff --git a/ui/src/app/audit/audit-log-row.directive.js b/ui/src/app/audit/audit-log-row.directive.js new file mode 100644 index 0000000000..1cb6d5f5d7 --- /dev/null +++ b/ui/src/app/audit/audit-log-row.directive.js @@ -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 + }; +} diff --git a/ui/src/app/audit/audit-log-row.tpl.html b/ui/src/app/audit/audit-log-row.tpl.html new file mode 100644 index 0000000000..fd0af2d15f --- /dev/null +++ b/ui/src/app/audit/audit-log-row.tpl.html @@ -0,0 +1,36 @@ + +
{{ auditLog.createdTime | date : 'yyyy-MM-dd HH:mm:ss' }}
+
{{ auditLog.entityTypeText }}
+
{{ auditLog.entityName }}
+
{{ auditLog.userName }}
+
{{ auditLog.actionTypeText }}
+
{{ auditLog.actionStatusText }}
+
+ + + {{ 'audit-log.details' | translate }} + + + more_horiz + + +
diff --git a/ui/src/app/audit/audit-log-table.directive.js b/ui/src/app/audit/audit-log-table.directive.js new file mode 100644 index 0000000000..ba15249253 --- /dev/null +++ b/ui/src/app/audit/audit-log-table.directive.js @@ -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: '@?' + } + }; +} diff --git a/ui/src/app/audit/audit-log-table.tpl.html b/ui/src/app/audit/audit-log-table.tpl.html new file mode 100644 index 0000000000..88a1d9376e --- /dev/null +++ b/ui/src/app/audit/audit-log-table.tpl.html @@ -0,0 +1,68 @@ + + +
+
+ +
+ + search + + {{'audit-log.search' | translate}} + + + + + + + + close + + {{ 'audit-log.clear-search' | translate }} + + + + refresh + + {{ 'action.refresh' | translate }} + + +
+
+
+ + + + + + audit-log.no-audit-logs-prompt + + + + + + + + +
+
+
diff --git a/ui/src/app/audit/audit-log.routes.js b/ui/src/app/audit/audit-log.routes.js new file mode 100644 index 0000000000..be26b9c457 --- /dev/null +++ b/ui/src/app/audit/audit-log.routes.js @@ -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"}' + } + }); +} diff --git a/ui/src/app/audit/audit-log.scss b/ui/src/app/audit/audit-log.scss new file mode 100644 index 0000000000..1642cbe7e4 --- /dev/null +++ b/ui/src/app/audit/audit-log.scss @@ -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; + } + +} diff --git a/ui/src/app/audit/audit-logs.controller.js b/ui/src/app/audit/audit-logs.controller.js new file mode 100644 index 0000000000..d2cb1e3cd4 --- /dev/null +++ b/ui/src/app/audit/audit-logs.controller.js @@ -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; + +} \ No newline at end of file diff --git a/ui/src/app/audit/audit-logs.tpl.html b/ui/src/app/audit/audit-logs.tpl.html new file mode 100644 index 0000000000..088c339498 --- /dev/null +++ b/ui/src/app/audit/audit-logs.tpl.html @@ -0,0 +1,23 @@ + + + + diff --git a/ui/src/app/audit/index.js b/ui/src/app/audit/index.js new file mode 100644 index 0000000000..aebcd33e73 --- /dev/null +++ b/ui/src/app/audit/index.js @@ -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; diff --git a/ui/src/app/common/types.constant.js b/ui/src/app/common/types.constant.js index 5b3c9e4ac4..d82add14c0 100644 --- a/ui/src/app/common/types.constant.js +++ b/ui/src/app/common/types.constant.js @@ -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', diff --git a/ui/src/app/components/grid.directive.js b/ui/src/app/components/grid.directive.js index 727b999d75..290431ce9b 100644 --- a/ui/src/app/components/grid.directive.js +++ b/ui/src/app/components/grid.directive.js @@ -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); } diff --git a/ui/src/app/customer/customers.tpl.html b/ui/src/app/customer/customers.tpl.html index da0a2e950b..0a0ca84b21 100644 --- a/ui/src/app/customer/customers.tpl.html +++ b/ui/src/app/customer/customers.tpl.html @@ -66,5 +66,10 @@ entity-type="{{vm.types.entityType.customer}}"> + + + + diff --git a/ui/src/app/dashboard/dashboards.tpl.html b/ui/src/app/dashboard/dashboards.tpl.html index dde2f864b7..8149821454 100644 --- a/ui/src/app/dashboard/dashboards.tpl.html +++ b/ui/src/app/dashboard/dashboards.tpl.html @@ -19,13 +19,24 @@
- + + + + + + + + + diff --git a/ui/src/app/device/device-fieldset.tpl.html b/ui/src/app/device/device-fieldset.tpl.html index a4d15f33eb..758c77f867 100644 --- a/ui/src/app/device/device-fieldset.tpl.html +++ b/ui/src/app/device/device-fieldset.tpl.html @@ -39,10 +39,8 @@ device.copyId - + device.copyAccessToken diff --git a/ui/src/app/device/device.directive.js b/ui/src/app/device/device.directive.js index eda4fb2ce9..a40e4da01d 100644 --- a/ui/src/app/device/device.directive.js +++ b/ui/src/app/device/device.directive.js @@ -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); diff --git a/ui/src/app/device/devices.tpl.html b/ui/src/app/device/devices.tpl.html index 1ec0134444..513465da3b 100644 --- a/ui/src/app/device/devices.tpl.html +++ b/ui/src/app/device/devices.tpl.html @@ -74,4 +74,10 @@ entity-type="{{vm.types.entityType.device}}"> + + + + diff --git a/ui/src/app/event/event-table.tpl.html b/ui/src/app/event/event-table.tpl.html index 760b55bfff..e056671720 100644 --- a/ui/src/app/event/event-table.tpl.html +++ b/ui/src/app/event/event-table.tpl.html @@ -26,9 +26,16 @@ + + refresh + + {{ 'action.refresh' | translate }} + + - + @@ -38,7 +45,7 @@ class="tb-prompt" ng-show="noData()">event.no-events-prompt - + diff --git a/ui/src/app/layout/index.js b/ui/src/app/layout/index.js index c23b0086a6..9102928226 100644 --- a/ui/src/app/layout/index.js +++ b/ui/src/app/layout/index.js @@ -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, diff --git a/ui/src/app/locale/locale.constant.js b/ui/src/app/locale/locale.constant.js index b9977f6f9e..96dc1523ec 100644 --- a/ui/src/app/locale/locale.constant.js +++ b/ui/src/app/locale/locale.constant.js @@ -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.
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 activation link :", "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", diff --git a/ui/src/app/plugin/plugins.tpl.html b/ui/src/app/plugin/plugins.tpl.html index b3563e9945..eafb9f7aa7 100644 --- a/ui/src/app/plugin/plugins.tpl.html +++ b/ui/src/app/plugin/plugins.tpl.html @@ -66,5 +66,12 @@ entity-type="{{vm.types.entityType.plugin}}"> + + + + diff --git a/ui/src/app/rule/rules.tpl.html b/ui/src/app/rule/rules.tpl.html index 064728baa9..f46d1248a8 100644 --- a/ui/src/app/rule/rules.tpl.html +++ b/ui/src/app/rule/rules.tpl.html @@ -66,5 +66,12 @@ entity-type="{{vm.types.entityType.rule}}"> + + + + diff --git a/ui/src/app/services/clipboard.service.js b/ui/src/app/services/clipboard.service.js new file mode 100644 index 0000000000..7c6d960959 --- /dev/null +++ b/ui/src/app/services/clipboard.service.js @@ -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 */ + +} \ No newline at end of file diff --git a/ui/src/app/services/menu.service.js b/ui/src/app/services/menu.service.js index 24db768ac5..a492585cf1 100644 --- a/ui/src/app/services/menu.service.js +++ b/ui/src/app/services/menu.service.js @@ -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') { diff --git a/ui/src/app/user/user.controller.js b/ui/src/app/user/user.controller.js index a9f4ce3b84..6a9fdde060 100644 --- a/ui/src/app/user/user.controller.js +++ b/ui/src/app/user/user.controller.js @@ -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, diff --git a/ui/src/app/user/users.tpl.html b/ui/src/app/user/users.tpl.html index 72f67d2853..03a5da24ca 100644 --- a/ui/src/app/user/users.tpl.html +++ b/ui/src/app/user/users.tpl.html @@ -19,10 +19,20 @@
- + + + + + + + + + diff --git a/ui/src/scss/main.scss b/ui/src/scss/main.scss index 93418b27fe..a961ab528a 100644 --- a/ui/src/scss/main.scss +++ b/ui/src/scss/main.scss @@ -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 */