Commit a5f44729e54f11a16d0bd880129759327a816293

Authored by Igor Kulikov
1 parent 2a74985e

Implement Audit Logs

Showing 74 changed files with 2280 additions and 269 deletions
@@ -24,8 +24,8 @@ CREATE TABLE IF NOT EXISTS audit_log ( @@ -24,8 +24,8 @@ CREATE TABLE IF NOT EXISTS audit_log (
24 user_id varchar(31), 24 user_id varchar(31),
25 user_name varchar(255), 25 user_name varchar(255),
26 action_type varchar(255), 26 action_type varchar(255),
27 - action_data varchar(255), 27 + action_data varchar(1000000),
28 action_status varchar(255), 28 action_status varchar(255),
29 - action_failure_details varchar 29 + action_failure_details varchar(1000000)
30 ); 30 );
31 31
@@ -40,6 +40,7 @@ import org.thingsboard.server.controller.plugin.PluginWebSocketMsgEndpoint; @@ -40,6 +40,7 @@ import org.thingsboard.server.controller.plugin.PluginWebSocketMsgEndpoint;
40 import org.thingsboard.server.dao.alarm.AlarmService; 40 import org.thingsboard.server.dao.alarm.AlarmService;
41 import org.thingsboard.server.dao.asset.AssetService; 41 import org.thingsboard.server.dao.asset.AssetService;
42 import org.thingsboard.server.dao.attributes.AttributesService; 42 import org.thingsboard.server.dao.attributes.AttributesService;
  43 +import org.thingsboard.server.dao.audit.AuditLogService;
43 import org.thingsboard.server.dao.customer.CustomerService; 44 import org.thingsboard.server.dao.customer.CustomerService;
44 import org.thingsboard.server.dao.device.DeviceService; 45 import org.thingsboard.server.dao.device.DeviceService;
45 import org.thingsboard.server.dao.event.EventService; 46 import org.thingsboard.server.dao.event.EventService;
@@ -114,6 +115,9 @@ public class ActorSystemContext { @@ -114,6 +115,9 @@ public class ActorSystemContext {
114 @Getter private RelationService relationService; 115 @Getter private RelationService relationService;
115 116
116 @Autowired 117 @Autowired
  118 + @Getter private AuditLogService auditLogService;
  119 +
  120 + @Autowired
117 @Getter @Setter private PluginWebSocketMsgEndpoint wsMsgEndpoint; 121 @Getter @Setter private PluginWebSocketMsgEndpoint wsMsgEndpoint;
118 122
119 @Value("${actors.session.sync.timeout}") 123 @Value("${actors.session.sync.timeout}")
@@ -26,6 +26,7 @@ import org.thingsboard.server.common.data.Device; @@ -26,6 +26,7 @@ import org.thingsboard.server.common.data.Device;
26 import org.thingsboard.server.common.data.EntityType; 26 import org.thingsboard.server.common.data.EntityType;
27 import org.thingsboard.server.common.data.Tenant; 27 import org.thingsboard.server.common.data.Tenant;
28 import org.thingsboard.server.common.data.asset.Asset; 28 import org.thingsboard.server.common.data.asset.Asset;
  29 +import org.thingsboard.server.common.data.audit.ActionType;
29 import org.thingsboard.server.common.data.id.*; 30 import org.thingsboard.server.common.data.id.*;
30 import org.thingsboard.server.common.data.kv.AttributeKey; 31 import org.thingsboard.server.common.data.kv.AttributeKey;
31 import org.thingsboard.server.common.data.kv.AttributeKvEntry; 32 import org.thingsboard.server.common.data.kv.AttributeKvEntry;
@@ -41,9 +42,7 @@ import org.thingsboard.server.extensions.api.device.DeviceAttributesEventNotific @@ -41,9 +42,7 @@ import org.thingsboard.server.extensions.api.device.DeviceAttributesEventNotific
41 import org.thingsboard.server.extensions.api.plugins.PluginApiCallSecurityContext; 42 import org.thingsboard.server.extensions.api.plugins.PluginApiCallSecurityContext;
42 import org.thingsboard.server.extensions.api.plugins.PluginCallback; 43 import org.thingsboard.server.extensions.api.plugins.PluginCallback;
43 import org.thingsboard.server.extensions.api.plugins.PluginContext; 44 import org.thingsboard.server.extensions.api.plugins.PluginContext;
44 -import org.thingsboard.server.extensions.api.plugins.msg.PluginToRuleMsg;  
45 -import org.thingsboard.server.extensions.api.plugins.msg.TimeoutMsg;  
46 -import org.thingsboard.server.extensions.api.plugins.msg.ToDeviceRpcRequest; 45 +import org.thingsboard.server.extensions.api.plugins.msg.*;
47 import org.thingsboard.server.extensions.api.plugins.rpc.PluginRpcMsg; 46 import org.thingsboard.server.extensions.api.plugins.rpc.PluginRpcMsg;
48 import org.thingsboard.server.extensions.api.plugins.rpc.RpcMsg; 47 import org.thingsboard.server.extensions.api.plugins.rpc.RpcMsg;
49 import org.thingsboard.server.extensions.api.plugins.ws.PluginWebsocketSessionRef; 48 import org.thingsboard.server.extensions.api.plugins.ws.PluginWebsocketSessionRef;
@@ -197,6 +196,52 @@ public final class PluginProcessingContext implements PluginContext { @@ -197,6 +196,52 @@ public final class PluginProcessingContext implements PluginContext {
197 } 196 }
198 197
199 @Override 198 @Override
  199 + public void logAttributesUpdated(PluginApiCallSecurityContext ctx, EntityId entityId, String attributeType,
  200 + List<AttributeKvEntry> attributes, Exception e) {
  201 + pluginCtx.auditLogService.logEntityAction(
  202 + ctx.getTenantId(),
  203 + ctx.getCustomerId(),
  204 + ctx.getUserId(),
  205 + ctx.getUserName(),
  206 + (UUIDBased & EntityId)entityId,
  207 + null,
  208 + ActionType.ATTRIBUTES_UPDATED,
  209 + e,
  210 + attributeType,
  211 + attributes);
  212 + }
  213 +
  214 + @Override
  215 + public void logAttributesDeleted(PluginApiCallSecurityContext ctx, EntityId entityId, String attributeType, List<String> keys, Exception e) {
  216 + pluginCtx.auditLogService.logEntityAction(
  217 + ctx.getTenantId(),
  218 + ctx.getCustomerId(),
  219 + ctx.getUserId(),
  220 + ctx.getUserName(),
  221 + (UUIDBased & EntityId)entityId,
  222 + null,
  223 + ActionType.ATTRIBUTES_DELETED,
  224 + e,
  225 + attributeType,
  226 + keys);
  227 + }
  228 +
  229 + @Override
  230 + public void logAttributesRead(PluginApiCallSecurityContext ctx, EntityId entityId, String attributeType, List<String> keys, Exception e) {
  231 + pluginCtx.auditLogService.logEntityAction(
  232 + ctx.getTenantId(),
  233 + ctx.getCustomerId(),
  234 + ctx.getUserId(),
  235 + ctx.getUserName(),
  236 + (UUIDBased & EntityId)entityId,
  237 + null,
  238 + ActionType.ATTRIBUTES_READ,
  239 + e,
  240 + attributeType,
  241 + keys);
  242 + }
  243 +
  244 + @Override
200 public void loadLatestTimeseries(final EntityId entityId, final Collection<String> keys, final PluginCallback<List<TsKvEntry>> callback) { 245 public void loadLatestTimeseries(final EntityId entityId, final Collection<String> keys, final PluginCallback<List<TsKvEntry>> callback) {
201 validate(entityId, new ValidationCallback(callback, ctx -> { 246 validate(entityId, new ValidationCallback(callback, ctx -> {
202 ListenableFuture<List<TsKvEntry>> rsListFuture = pluginCtx.tsService.findLatest(entityId, keys); 247 ListenableFuture<List<TsKvEntry>> rsListFuture = pluginCtx.tsService.findLatest(entityId, keys);
@@ -461,6 +506,29 @@ public final class PluginProcessingContext implements PluginContext { @@ -461,6 +506,29 @@ public final class PluginProcessingContext implements PluginContext {
461 } 506 }
462 507
463 @Override 508 @Override
  509 + public void logRpcRequest(PluginApiCallSecurityContext ctx, DeviceId deviceId, ToDeviceRpcRequestBody body, boolean oneWay, Optional<RpcError> rpcError, Exception e) {
  510 + String rpcErrorStr = "";
  511 + if (rpcError.isPresent()) {
  512 + rpcErrorStr = "RPC Error: " + rpcError.get().name();
  513 + }
  514 + String method = body.getMethod();
  515 + String params = body.getParams();
  516 + pluginCtx.auditLogService.logEntityAction(
  517 + ctx.getTenantId(),
  518 + ctx.getCustomerId(),
  519 + ctx.getUserId(),
  520 + ctx.getUserName(),
  521 + deviceId,
  522 + null,
  523 + ActionType.RPC_CALL,
  524 + e,
  525 + rpcErrorStr,
  526 + new Boolean(oneWay),
  527 + method,
  528 + params);
  529 + }
  530 +
  531 + @Override
464 public void scheduleTimeoutMsg(TimeoutMsg msg) { 532 public void scheduleTimeoutMsg(TimeoutMsg msg) {
465 pluginCtx.scheduleTimeoutMsg(msg); 533 pluginCtx.scheduleTimeoutMsg(msg);
466 } 534 }
@@ -27,6 +27,7 @@ import org.thingsboard.server.controller.plugin.PluginWebSocketMsgEndpoint; @@ -27,6 +27,7 @@ import org.thingsboard.server.controller.plugin.PluginWebSocketMsgEndpoint;
27 import org.thingsboard.server.common.data.id.PluginId; 27 import org.thingsboard.server.common.data.id.PluginId;
28 import org.thingsboard.server.dao.asset.AssetService; 28 import org.thingsboard.server.dao.asset.AssetService;
29 import org.thingsboard.server.dao.attributes.AttributesService; 29 import org.thingsboard.server.dao.attributes.AttributesService;
  30 +import org.thingsboard.server.dao.audit.AuditLogService;
30 import org.thingsboard.server.dao.customer.CustomerService; 31 import org.thingsboard.server.dao.customer.CustomerService;
31 import org.thingsboard.server.dao.device.DeviceService; 32 import org.thingsboard.server.dao.device.DeviceService;
32 import org.thingsboard.server.dao.plugin.PluginService; 33 import org.thingsboard.server.dao.plugin.PluginService;
@@ -63,6 +64,7 @@ public final class SharedPluginProcessingContext { @@ -63,6 +64,7 @@ public final class SharedPluginProcessingContext {
63 final ClusterRpcService rpcService; 64 final ClusterRpcService rpcService;
64 final ClusterRoutingService routingService; 65 final ClusterRoutingService routingService;
65 final RelationService relationService; 66 final RelationService relationService;
  67 + final AuditLogService auditLogService;
66 final PluginId pluginId; 68 final PluginId pluginId;
67 final TenantId tenantId; 69 final TenantId tenantId;
68 70
@@ -86,6 +88,7 @@ public final class SharedPluginProcessingContext { @@ -86,6 +88,7 @@ public final class SharedPluginProcessingContext {
86 this.customerService = sysContext.getCustomerService(); 88 this.customerService = sysContext.getCustomerService();
87 this.tenantService = sysContext.getTenantService(); 89 this.tenantService = sysContext.getTenantService();
88 this.relationService = sysContext.getRelationService(); 90 this.relationService = sysContext.getRelationService();
  91 + this.auditLogService = sysContext.getAuditLogService();
89 } 92 }
90 93
91 public PluginId getPluginId() { 94 public PluginId getPluginId() {
@@ -148,7 +148,7 @@ public class BasicRpcSessionListener implements GrpcSessionListener { @@ -148,7 +148,7 @@ public class BasicRpcSessionListener implements GrpcSessionListener {
148 DeviceId deviceId = new DeviceId(toUUID(msg.getDeviceId())); 148 DeviceId deviceId = new DeviceId(toUUID(msg.getDeviceId()));
149 149
150 ToDeviceRpcRequestBody requestBody = new ToDeviceRpcRequestBody(msg.getMethod(), msg.getParams()); 150 ToDeviceRpcRequestBody requestBody = new ToDeviceRpcRequestBody(msg.getMethod(), msg.getParams());
151 - ToDeviceRpcRequest request = new ToDeviceRpcRequest(toUUID(msg.getMsgId()), deviceTenantId, deviceId, msg.getOneway(), msg.getExpTime(), requestBody); 151 + ToDeviceRpcRequest request = new ToDeviceRpcRequest(toUUID(msg.getMsgId()), null, deviceTenantId, deviceId, msg.getOneway(), msg.getExpTime(), requestBody);
152 152
153 return new ToDeviceRpcRequestPluginMsg(serverAddress, pluginId, pluginTenantId, request); 153 return new ToDeviceRpcRequestPluginMsg(serverAddress, pluginId, pluginTenantId, request);
154 } 154 }
  1 +/**
  2 + * Copyright © 2016-2017 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.config;
  17 +
  18 +import org.springframework.boot.context.properties.ConfigurationProperties;
  19 +import org.springframework.context.annotation.Configuration;
  20 +import org.thingsboard.server.common.data.EntityType;
  21 +import org.thingsboard.server.common.data.audit.ActionType;
  22 +
  23 +import java.util.HashMap;
  24 +import java.util.Map;
  25 +
  26 +@Configuration
  27 +@ConfigurationProperties(prefix = "audit_log.logging_level")
  28 +public class AuditLogLevelProperties {
  29 +
  30 + private Map<String, String> mask = new HashMap<>();
  31 +
  32 + public AuditLogLevelProperties() {
  33 + super();
  34 + }
  35 +
  36 + public void setMask(Map<String, String> mask) {
  37 + this.mask = mask;
  38 + }
  39 +
  40 + public Map<String, String> getMask() {
  41 + return this.mask;
  42 + }
  43 +}
@@ -40,6 +40,7 @@ import org.springframework.security.web.util.matcher.AntPathRequestMatcher; @@ -40,6 +40,7 @@ import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
40 import org.springframework.web.cors.CorsUtils; 40 import org.springframework.web.cors.CorsUtils;
41 import org.springframework.web.cors.UrlBasedCorsConfigurationSource; 41 import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
42 import org.springframework.web.filter.CorsFilter; 42 import org.springframework.web.filter.CorsFilter;
  43 +import org.thingsboard.server.dao.audit.AuditLogLevelFilter;
43 import org.thingsboard.server.exception.ThingsboardErrorResponseHandler; 44 import org.thingsboard.server.exception.ThingsboardErrorResponseHandler;
44 import org.thingsboard.server.service.security.auth.rest.RestAuthenticationProvider; 45 import org.thingsboard.server.service.security.auth.rest.RestAuthenticationProvider;
45 import org.thingsboard.server.service.security.auth.rest.RestLoginProcessingFilter; 46 import org.thingsboard.server.service.security.auth.rest.RestLoginProcessingFilter;
@@ -198,4 +199,9 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt @@ -198,4 +199,9 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt
198 return new CorsFilter(source); 199 return new CorsFilter(source);
199 } 200 }
200 } 201 }
  202 +
  203 + @Bean
  204 + public AuditLogLevelFilter auditLogLevelFilter(@Autowired AuditLogLevelProperties auditLogLevelProperties) {
  205 + return new AuditLogLevelFilter(auditLogLevelProperties.getMask());
  206 + }
201 } 207 }
@@ -21,7 +21,9 @@ import org.springframework.security.access.prepost.PreAuthorize; @@ -21,7 +21,9 @@ import org.springframework.security.access.prepost.PreAuthorize;
21 import org.springframework.web.bind.annotation.*; 21 import org.springframework.web.bind.annotation.*;
22 import org.thingsboard.server.common.data.Customer; 22 import org.thingsboard.server.common.data.Customer;
23 import org.thingsboard.server.common.data.EntitySubtype; 23 import org.thingsboard.server.common.data.EntitySubtype;
  24 +import org.thingsboard.server.common.data.EntityType;
24 import org.thingsboard.server.common.data.asset.Asset; 25 import org.thingsboard.server.common.data.asset.Asset;
  26 +import org.thingsboard.server.common.data.audit.ActionType;
25 import org.thingsboard.server.common.data.id.AssetId; 27 import org.thingsboard.server.common.data.id.AssetId;
26 import org.thingsboard.server.common.data.id.CustomerId; 28 import org.thingsboard.server.common.data.id.CustomerId;
27 import org.thingsboard.server.common.data.id.TenantId; 29 import org.thingsboard.server.common.data.id.TenantId;
@@ -73,8 +75,16 @@ public class AssetController extends BaseController { @@ -73,8 +75,16 @@ public class AssetController extends BaseController {
73 checkCustomerId(asset.getCustomerId()); 75 checkCustomerId(asset.getCustomerId());
74 } 76 }
75 } 77 }
76 - return checkNotNull(assetService.saveAsset(asset)); 78 + Asset savedAsset = checkNotNull(assetService.saveAsset(asset));
  79 +
  80 + logEntityAction(savedAsset.getId(), savedAsset,
  81 + savedAsset.getCustomerId(),
  82 + asset.getId() == null ? ActionType.ADDED : ActionType.UPDATED, null);
  83 +
  84 + return savedAsset;
77 } catch (Exception e) { 85 } catch (Exception e) {
  86 + logEntityAction(emptyId(EntityType.ASSET), asset,
  87 + null, asset.getId() == null ? ActionType.ADDED : ActionType.UPDATED, e);
78 throw handleException(e); 88 throw handleException(e);
79 } 89 }
80 } 90 }
@@ -86,9 +96,18 @@ public class AssetController extends BaseController { @@ -86,9 +96,18 @@ public class AssetController extends BaseController {
86 checkParameter(ASSET_ID, strAssetId); 96 checkParameter(ASSET_ID, strAssetId);
87 try { 97 try {
88 AssetId assetId = new AssetId(toUUID(strAssetId)); 98 AssetId assetId = new AssetId(toUUID(strAssetId));
89 - checkAssetId(assetId); 99 + Asset asset = checkAssetId(assetId);
90 assetService.deleteAsset(assetId); 100 assetService.deleteAsset(assetId);
  101 +
  102 + logEntityAction(assetId, asset,
  103 + asset.getCustomerId(),
  104 + ActionType.DELETED, null, strAssetId);
  105 +
91 } catch (Exception e) { 106 } catch (Exception e) {
  107 + logEntityAction(emptyId(EntityType.ASSET),
  108 + null,
  109 + null,
  110 + ActionType.DELETED, e, strAssetId);
92 throw handleException(e); 111 throw handleException(e);
93 } 112 }
94 } 113 }
@@ -102,13 +121,24 @@ public class AssetController extends BaseController { @@ -102,13 +121,24 @@ public class AssetController extends BaseController {
102 checkParameter(ASSET_ID, strAssetId); 121 checkParameter(ASSET_ID, strAssetId);
103 try { 122 try {
104 CustomerId customerId = new CustomerId(toUUID(strCustomerId)); 123 CustomerId customerId = new CustomerId(toUUID(strCustomerId));
105 - checkCustomerId(customerId); 124 + Customer customer = checkCustomerId(customerId);
106 125
107 AssetId assetId = new AssetId(toUUID(strAssetId)); 126 AssetId assetId = new AssetId(toUUID(strAssetId));
108 checkAssetId(assetId); 127 checkAssetId(assetId);
109 128
110 - return checkNotNull(assetService.assignAssetToCustomer(assetId, customerId)); 129 + Asset savedAsset = checkNotNull(assetService.assignAssetToCustomer(assetId, customerId));
  130 +
  131 + logEntityAction(assetId, savedAsset,
  132 + savedAsset.getCustomerId(),
  133 + ActionType.ASSIGNED_TO_CUSTOMER, null, strAssetId, strCustomerId, customer.getName());
  134 +
  135 + return savedAsset;
111 } catch (Exception e) { 136 } catch (Exception e) {
  137 +
  138 + logEntityAction(emptyId(EntityType.ASSET), null,
  139 + null,
  140 + ActionType.ASSIGNED_TO_CUSTOMER, e, strAssetId, strCustomerId);
  141 +
112 throw handleException(e); 142 throw handleException(e);
113 } 143 }
114 } 144 }
@@ -124,8 +154,22 @@ public class AssetController extends BaseController { @@ -124,8 +154,22 @@ public class AssetController extends BaseController {
124 if (asset.getCustomerId() == null || asset.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) { 154 if (asset.getCustomerId() == null || asset.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) {
125 throw new IncorrectParameterException("Asset isn't assigned to any customer!"); 155 throw new IncorrectParameterException("Asset isn't assigned to any customer!");
126 } 156 }
127 - return checkNotNull(assetService.unassignAssetFromCustomer(assetId)); 157 +
  158 + Customer customer = checkCustomerId(asset.getCustomerId());
  159 +
  160 + Asset savedAsset = checkNotNull(assetService.unassignAssetFromCustomer(assetId));
  161 +
  162 + logEntityAction(assetId, asset,
  163 + asset.getCustomerId(),
  164 + ActionType.UNASSIGNED_FROM_CUSTOMER, null, strAssetId, customer.getId().toString(), customer.getName());
  165 +
  166 + return savedAsset;
128 } catch (Exception e) { 167 } catch (Exception e) {
  168 +
  169 + logEntityAction(emptyId(EntityType.ASSET), null,
  170 + null,
  171 + ActionType.UNASSIGNED_FROM_CUSTOMER, e, strAssetId);
  172 +
129 throw handleException(e); 173 throw handleException(e);
130 } 174 }
131 } 175 }
@@ -139,8 +183,19 @@ public class AssetController extends BaseController { @@ -139,8 +183,19 @@ public class AssetController extends BaseController {
139 AssetId assetId = new AssetId(toUUID(strAssetId)); 183 AssetId assetId = new AssetId(toUUID(strAssetId));
140 Asset asset = checkAssetId(assetId); 184 Asset asset = checkAssetId(assetId);
141 Customer publicCustomer = customerService.findOrCreatePublicCustomer(asset.getTenantId()); 185 Customer publicCustomer = customerService.findOrCreatePublicCustomer(asset.getTenantId());
142 - return checkNotNull(assetService.assignAssetToCustomer(assetId, publicCustomer.getId())); 186 + Asset savedAsset = checkNotNull(assetService.assignAssetToCustomer(assetId, publicCustomer.getId()));
  187 +
  188 + logEntityAction(assetId, savedAsset,
  189 + savedAsset.getCustomerId(),
  190 + ActionType.ASSIGNED_TO_CUSTOMER, null, strAssetId, publicCustomer.getId().toString(), publicCustomer.getName());
  191 +
  192 + return savedAsset;
143 } catch (Exception e) { 193 } catch (Exception e) {
  194 +
  195 + logEntityAction(emptyId(EntityType.ASSET), null,
  196 + null,
  197 + ActionType.ASSIGNED_TO_CUSTOMER, e, strAssetId);
  198 +
144 throw handleException(e); 199 throw handleException(e);
145 } 200 }
146 } 201 }
@@ -79,9 +79,6 @@ public abstract class BaseController { @@ -79,9 +79,6 @@ public abstract class BaseController {
79 public static final String INCORRECT_TENANT_ID = "Incorrect tenantId "; 79 public static final String INCORRECT_TENANT_ID = "Incorrect tenantId ";
80 public static final String YOU_DON_T_HAVE_PERMISSION_TO_PERFORM_THIS_OPERATION = "You don't have permission to perform this operation!"; 80 public static final String YOU_DON_T_HAVE_PERMISSION_TO_PERFORM_THIS_OPERATION = "You don't have permission to perform this operation!";
81 81
82 - @Value("${audit_log.exceptions.enabled}")  
83 - private boolean auditLogExceptionsEnabled;  
84 -  
85 @Autowired 82 @Autowired
86 private ThingsboardErrorResponseHandler errorResponseHandler; 83 private ThingsboardErrorResponseHandler errorResponseHandler;
87 84
@@ -130,11 +127,6 @@ public abstract class BaseController { @@ -130,11 +127,6 @@ public abstract class BaseController {
130 @Autowired 127 @Autowired
131 protected AuditLogService auditLogService; 128 protected AuditLogService auditLogService;
132 129
133 - @ExceptionHandler(Exception.class)  
134 - public void handleException(Exception ex, HttpServletResponse response) {  
135 - errorResponseHandler.handle(ex, response);  
136 - }  
137 -  
138 @ExceptionHandler(ThingsboardException.class) 130 @ExceptionHandler(ThingsboardException.class)
139 public void handleThingsboardException(ThingsboardException ex, HttpServletResponse response) { 131 public void handleThingsboardException(ThingsboardException ex, HttpServletResponse response) {
140 errorResponseHandler.handle(ex, response); 132 errorResponseHandler.handle(ex, response);
@@ -144,11 +136,6 @@ public abstract class BaseController { @@ -144,11 +136,6 @@ public abstract class BaseController {
144 return handleException(exception, true); 136 return handleException(exception, true);
145 } 137 }
146 138
147 - ThingsboardException handleException(Exception exception, ActionType actionType, String actionData) {  
148 - logExceptionToAuditLog(exception, actionType, actionData);  
149 - return handleException(exception, true);  
150 - }  
151 -  
152 private ThingsboardException handleException(Exception exception, boolean logException) { 139 private ThingsboardException handleException(Exception exception, boolean logException) {
153 if (logException) { 140 if (logException) {
154 log.error("Error [{}]", exception.getMessage()); 141 log.error("Error [{}]", exception.getMessage());
@@ -171,36 +158,6 @@ public abstract class BaseController { @@ -171,36 +158,6 @@ public abstract class BaseController {
171 } 158 }
172 } 159 }
173 160
174 - private void logExceptionToAuditLog(Exception exception, ActionType actionType, String actionData) {  
175 - try {  
176 - if (auditLogExceptionsEnabled) {  
177 - SecurityUser currentUser = getCurrentUser();  
178 - EntityId entityId;  
179 - CustomerId customerId;  
180 - if (!currentUser.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) {  
181 - entityId = currentUser.getCustomerId();  
182 - customerId = currentUser.getCustomerId();  
183 - } else {  
184 - entityId = currentUser.getTenantId();  
185 - customerId = new CustomerId(ModelConstants.NULL_UUID);  
186 - }  
187 -  
188 - JsonNode actionDataNode = new ObjectMapper().createObjectNode().put("actionData", actionData);  
189 -  
190 - auditLogService.logEntityAction(currentUser,  
191 - entityId,  
192 - null,  
193 - customerId,  
194 - actionType,  
195 - actionDataNode,  
196 - ActionStatus.FAILURE,  
197 - exception.getMessage());  
198 - }  
199 - } catch (Exception e) {  
200 - log.error("Exception happend during saving to audit log", e);  
201 - }  
202 - }  
203 -  
204 <T> T checkNotNull(T reference) throws ThingsboardException { 161 <T> T checkNotNull(T reference) throws ThingsboardException {
205 if (reference == null) { 162 if (reference == null) {
206 throw new ThingsboardException("Requested item wasn't found!", ThingsboardErrorCode.ITEM_NOT_FOUND); 163 throw new ThingsboardException("Requested item wasn't found!", ThingsboardErrorCode.ITEM_NOT_FOUND);
@@ -594,23 +551,19 @@ public abstract class BaseController { @@ -594,23 +551,19 @@ public abstract class BaseController {
594 return baseUrl; 551 return baseUrl;
595 } 552 }
596 553
597 - protected void logEntityDeleted(EntityId entityId, String entityName, CustomerId customerId) throws ThingsboardException {  
598 - logEntitySuccess(entityId, entityName, customerId, ActionType.DELETED); 554 + protected <I extends UUIDBased & EntityId> I emptyId(EntityType entityType) {
  555 + return (I)EntityIdFactory.getByTypeAndUuid(entityType, ModelConstants.NULL_UUID);
599 } 556 }
600 557
601 - protected void logEntityAddedOrUpdated(EntityId entityId, String entityName, CustomerId customerId, boolean isAddAction) throws ThingsboardException {  
602 - logEntitySuccess(entityId, entityName, customerId, isAddAction ? ActionType.ADDED : ActionType.UPDATED); 558 + protected <E extends BaseData<I> & HasName,
  559 + I extends UUIDBased & EntityId> void logEntityAction(I entityId, E entity, CustomerId customerId,
  560 + ActionType actionType, Exception e, Object... additionalInfo) throws ThingsboardException {
  561 + User user = getCurrentUser();
  562 + if (customerId == null || customerId.isNullUid()) {
  563 + customerId = user.getCustomerId();
  564 + }
  565 + auditLogService.logEntityAction(user.getTenantId(), customerId, user.getId(), user.getName(), entityId, entity, actionType, e, additionalInfo);
603 } 566 }
604 567
605 - protected void logEntitySuccess(EntityId entityId, String entityName, CustomerId customerId, ActionType actionType) throws ThingsboardException {  
606 - auditLogService.logEntityAction(  
607 - getCurrentUser(),  
608 - entityId,  
609 - entityName,  
610 - customerId,  
611 - actionType,  
612 - null,  
613 - ActionStatus.SUCCESS,  
614 - null);  
615 - } 568 +
616 } 569 }
@@ -22,6 +22,8 @@ import org.springframework.http.HttpStatus; @@ -22,6 +22,8 @@ import org.springframework.http.HttpStatus;
22 import org.springframework.security.access.prepost.PreAuthorize; 22 import org.springframework.security.access.prepost.PreAuthorize;
23 import org.springframework.web.bind.annotation.*; 23 import org.springframework.web.bind.annotation.*;
24 import org.thingsboard.server.common.data.Customer; 24 import org.thingsboard.server.common.data.Customer;
  25 +import org.thingsboard.server.common.data.EntityType;
  26 +import org.thingsboard.server.common.data.audit.ActionType;
25 import org.thingsboard.server.common.data.id.CustomerId; 27 import org.thingsboard.server.common.data.id.CustomerId;
26 import org.thingsboard.server.common.data.id.TenantId; 28 import org.thingsboard.server.common.data.id.TenantId;
27 import org.thingsboard.server.common.data.page.TextPageData; 29 import org.thingsboard.server.common.data.page.TextPageData;
@@ -86,8 +88,18 @@ public class CustomerController extends BaseController { @@ -86,8 +88,18 @@ public class CustomerController extends BaseController {
86 public Customer saveCustomer(@RequestBody Customer customer) throws ThingsboardException { 88 public Customer saveCustomer(@RequestBody Customer customer) throws ThingsboardException {
87 try { 89 try {
88 customer.setTenantId(getCurrentUser().getTenantId()); 90 customer.setTenantId(getCurrentUser().getTenantId());
89 - return checkNotNull(customerService.saveCustomer(customer)); 91 + Customer savedCustomer = checkNotNull(customerService.saveCustomer(customer));
  92 +
  93 + logEntityAction(savedCustomer.getId(), savedCustomer,
  94 + savedCustomer.getId(),
  95 + customer.getId() == null ? ActionType.ADDED : ActionType.UPDATED, null);
  96 +
  97 + return savedCustomer;
90 } catch (Exception e) { 98 } catch (Exception e) {
  99 +
  100 + logEntityAction(emptyId(EntityType.CUSTOMER), customer,
  101 + null, customer.getId() == null ? ActionType.ADDED : ActionType.UPDATED, e);
  102 +
91 throw handleException(e); 103 throw handleException(e);
92 } 104 }
93 } 105 }
@@ -99,9 +111,20 @@ public class CustomerController extends BaseController { @@ -99,9 +111,20 @@ public class CustomerController extends BaseController {
99 checkParameter(CUSTOMER_ID, strCustomerId); 111 checkParameter(CUSTOMER_ID, strCustomerId);
100 try { 112 try {
101 CustomerId customerId = new CustomerId(toUUID(strCustomerId)); 113 CustomerId customerId = new CustomerId(toUUID(strCustomerId));
102 - checkCustomerId(customerId); 114 + Customer customer = checkCustomerId(customerId);
103 customerService.deleteCustomer(customerId); 115 customerService.deleteCustomer(customerId);
  116 +
  117 + logEntityAction(customerId, customer,
  118 + customer.getId(),
  119 + ActionType.DELETED, null, strCustomerId);
  120 +
104 } catch (Exception e) { 121 } catch (Exception e) {
  122 +
  123 + logEntityAction(emptyId(EntityType.CUSTOMER),
  124 + null,
  125 + null,
  126 + ActionType.DELETED, e, strCustomerId);
  127 +
105 throw handleException(e); 128 throw handleException(e);
106 } 129 }
107 } 130 }
@@ -21,6 +21,8 @@ import org.springframework.web.bind.annotation.*; @@ -21,6 +21,8 @@ import org.springframework.web.bind.annotation.*;
21 import org.thingsboard.server.common.data.Customer; 21 import org.thingsboard.server.common.data.Customer;
22 import org.thingsboard.server.common.data.Dashboard; 22 import org.thingsboard.server.common.data.Dashboard;
23 import org.thingsboard.server.common.data.DashboardInfo; 23 import org.thingsboard.server.common.data.DashboardInfo;
  24 +import org.thingsboard.server.common.data.EntityType;
  25 +import org.thingsboard.server.common.data.audit.ActionType;
24 import org.thingsboard.server.common.data.id.CustomerId; 26 import org.thingsboard.server.common.data.id.CustomerId;
25 import org.thingsboard.server.common.data.id.DashboardId; 27 import org.thingsboard.server.common.data.id.DashboardId;
26 import org.thingsboard.server.common.data.id.TenantId; 28 import org.thingsboard.server.common.data.id.TenantId;
@@ -75,8 +77,17 @@ public class DashboardController extends BaseController { @@ -75,8 +77,17 @@ public class DashboardController extends BaseController {
75 public Dashboard saveDashboard(@RequestBody Dashboard dashboard) throws ThingsboardException { 77 public Dashboard saveDashboard(@RequestBody Dashboard dashboard) throws ThingsboardException {
76 try { 78 try {
77 dashboard.setTenantId(getCurrentUser().getTenantId()); 79 dashboard.setTenantId(getCurrentUser().getTenantId());
78 - return checkNotNull(dashboardService.saveDashboard(dashboard)); 80 + Dashboard savedDashboard = checkNotNull(dashboardService.saveDashboard(dashboard));
  81 +
  82 + logEntityAction(savedDashboard.getId(), savedDashboard,
  83 + savedDashboard.getCustomerId(),
  84 + dashboard.getId() == null ? ActionType.ADDED : ActionType.UPDATED, null);
  85 +
  86 + return savedDashboard;
79 } catch (Exception e) { 87 } catch (Exception e) {
  88 + logEntityAction(emptyId(EntityType.DASHBOARD), dashboard,
  89 + null, dashboard.getId() == null ? ActionType.ADDED : ActionType.UPDATED, e);
  90 +
80 throw handleException(e); 91 throw handleException(e);
81 } 92 }
82 } 93 }
@@ -88,9 +99,20 @@ public class DashboardController extends BaseController { @@ -88,9 +99,20 @@ public class DashboardController extends BaseController {
88 checkParameter(DASHBOARD_ID, strDashboardId); 99 checkParameter(DASHBOARD_ID, strDashboardId);
89 try { 100 try {
90 DashboardId dashboardId = new DashboardId(toUUID(strDashboardId)); 101 DashboardId dashboardId = new DashboardId(toUUID(strDashboardId));
91 - checkDashboardId(dashboardId); 102 + Dashboard dashboard = checkDashboardId(dashboardId);
92 dashboardService.deleteDashboard(dashboardId); 103 dashboardService.deleteDashboard(dashboardId);
  104 +
  105 + logEntityAction(dashboardId, dashboard,
  106 + dashboard.getCustomerId(),
  107 + ActionType.DELETED, null, strDashboardId);
  108 +
93 } catch (Exception e) { 109 } catch (Exception e) {
  110 +
  111 + logEntityAction(emptyId(EntityType.DASHBOARD),
  112 + null,
  113 + null,
  114 + ActionType.DELETED, e, strDashboardId);
  115 +
94 throw handleException(e); 116 throw handleException(e);
95 } 117 }
96 } 118 }
@@ -104,13 +126,25 @@ public class DashboardController extends BaseController { @@ -104,13 +126,25 @@ public class DashboardController extends BaseController {
104 checkParameter(DASHBOARD_ID, strDashboardId); 126 checkParameter(DASHBOARD_ID, strDashboardId);
105 try { 127 try {
106 CustomerId customerId = new CustomerId(toUUID(strCustomerId)); 128 CustomerId customerId = new CustomerId(toUUID(strCustomerId));
107 - checkCustomerId(customerId); 129 + Customer customer = checkCustomerId(customerId);
108 130
109 DashboardId dashboardId = new DashboardId(toUUID(strDashboardId)); 131 DashboardId dashboardId = new DashboardId(toUUID(strDashboardId));
110 checkDashboardId(dashboardId); 132 checkDashboardId(dashboardId);
111 133
112 - return checkNotNull(dashboardService.assignDashboardToCustomer(dashboardId, customerId)); 134 + Dashboard savedDashboard = checkNotNull(dashboardService.assignDashboardToCustomer(dashboardId, customerId));
  135 +
  136 + logEntityAction(dashboardId, savedDashboard,
  137 + savedDashboard.getCustomerId(),
  138 + ActionType.ASSIGNED_TO_CUSTOMER, null, strDashboardId, strCustomerId, customer.getName());
  139 +
  140 +
  141 + return savedDashboard;
113 } catch (Exception e) { 142 } catch (Exception e) {
  143 +
  144 + logEntityAction(emptyId(EntityType.DASHBOARD), null,
  145 + null,
  146 + ActionType.ASSIGNED_TO_CUSTOMER, e, strDashboardId, strCustomerId);
  147 +
114 throw handleException(e); 148 throw handleException(e);
115 } 149 }
116 } 150 }
@@ -126,8 +160,22 @@ public class DashboardController extends BaseController { @@ -126,8 +160,22 @@ public class DashboardController extends BaseController {
126 if (dashboard.getCustomerId() == null || dashboard.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) { 160 if (dashboard.getCustomerId() == null || dashboard.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) {
127 throw new IncorrectParameterException("Dashboard isn't assigned to any customer!"); 161 throw new IncorrectParameterException("Dashboard isn't assigned to any customer!");
128 } 162 }
129 - return checkNotNull(dashboardService.unassignDashboardFromCustomer(dashboardId)); 163 +
  164 + Customer customer = checkCustomerId(dashboard.getCustomerId());
  165 +
  166 + Dashboard savedDashboard = checkNotNull(dashboardService.unassignDashboardFromCustomer(dashboardId));
  167 +
  168 + logEntityAction(dashboardId, dashboard,
  169 + dashboard.getCustomerId(),
  170 + ActionType.UNASSIGNED_FROM_CUSTOMER, null, strDashboardId, customer.getId().toString(), customer.getName());
  171 +
  172 + return savedDashboard;
130 } catch (Exception e) { 173 } catch (Exception e) {
  174 +
  175 + logEntityAction(emptyId(EntityType.DASHBOARD), null,
  176 + null,
  177 + ActionType.UNASSIGNED_FROM_CUSTOMER, e, strDashboardId);
  178 +
131 throw handleException(e); 179 throw handleException(e);
132 } 180 }
133 } 181 }
@@ -141,8 +189,19 @@ public class DashboardController extends BaseController { @@ -141,8 +189,19 @@ public class DashboardController extends BaseController {
141 DashboardId dashboardId = new DashboardId(toUUID(strDashboardId)); 189 DashboardId dashboardId = new DashboardId(toUUID(strDashboardId));
142 Dashboard dashboard = checkDashboardId(dashboardId); 190 Dashboard dashboard = checkDashboardId(dashboardId);
143 Customer publicCustomer = customerService.findOrCreatePublicCustomer(dashboard.getTenantId()); 191 Customer publicCustomer = customerService.findOrCreatePublicCustomer(dashboard.getTenantId());
144 - return checkNotNull(dashboardService.assignDashboardToCustomer(dashboardId, publicCustomer.getId())); 192 + Dashboard savedDashboard = checkNotNull(dashboardService.assignDashboardToCustomer(dashboardId, publicCustomer.getId()));
  193 +
  194 + logEntityAction(dashboardId, savedDashboard,
  195 + savedDashboard.getCustomerId(),
  196 + ActionType.ASSIGNED_TO_CUSTOMER, null, strDashboardId, publicCustomer.getId().toString(), publicCustomer.getName());
  197 +
  198 + return savedDashboard;
145 } catch (Exception e) { 199 } catch (Exception e) {
  200 +
  201 + logEntityAction(emptyId(EntityType.DASHBOARD), null,
  202 + null,
  203 + ActionType.ASSIGNED_TO_CUSTOMER, e, strDashboardId);
  204 +
146 throw handleException(e); 205 throw handleException(e);
147 } 206 }
148 } 207 }
@@ -22,6 +22,7 @@ import org.springframework.web.bind.annotation.*; @@ -22,6 +22,7 @@ import org.springframework.web.bind.annotation.*;
22 import org.thingsboard.server.common.data.Customer; 22 import org.thingsboard.server.common.data.Customer;
23 import org.thingsboard.server.common.data.Device; 23 import org.thingsboard.server.common.data.Device;
24 import org.thingsboard.server.common.data.EntitySubtype; 24 import org.thingsboard.server.common.data.EntitySubtype;
  25 +import org.thingsboard.server.common.data.EntityType;
25 import org.thingsboard.server.common.data.audit.ActionStatus; 26 import org.thingsboard.server.common.data.audit.ActionStatus;
26 import org.thingsboard.server.common.data.audit.ActionType; 27 import org.thingsboard.server.common.data.audit.ActionType;
27 import org.thingsboard.server.common.data.device.DeviceSearchQuery; 28 import org.thingsboard.server.common.data.device.DeviceSearchQuery;
@@ -85,11 +86,15 @@ public class DeviceController extends BaseController { @@ -85,11 +86,15 @@ public class DeviceController extends BaseController {
85 savedDevice.getName(), 86 savedDevice.getName(),
86 savedDevice.getType()); 87 savedDevice.getType());
87 88
88 - logEntityAddedOrUpdated(savedDevice.getId(), savedDevice.getName(), savedDevice.getCustomerId(), device.getId() == null); 89 + logEntityAction(savedDevice.getId(), savedDevice,
  90 + savedDevice.getCustomerId(),
  91 + device.getId() == null ? ActionType.ADDED : ActionType.UPDATED, null);
89 92
90 return savedDevice; 93 return savedDevice;
91 } catch (Exception e) { 94 } catch (Exception e) {
92 - throw handleException(e, device.getId() == null ? ActionType.ADDED : ActionType.UPDATED, "addDevice(" + device + ")"); 95 + logEntityAction(emptyId(EntityType.DEVICE), device,
  96 + null, device.getId() == null ? ActionType.ADDED : ActionType.UPDATED, e);
  97 + throw handleException(e);
93 } 98 }
94 } 99 }
95 100
@@ -102,9 +107,17 @@ public class DeviceController extends BaseController { @@ -102,9 +107,17 @@ public class DeviceController extends BaseController {
102 DeviceId deviceId = new DeviceId(toUUID(strDeviceId)); 107 DeviceId deviceId = new DeviceId(toUUID(strDeviceId));
103 Device device = checkDeviceId(deviceId); 108 Device device = checkDeviceId(deviceId);
104 deviceService.deleteDevice(deviceId); 109 deviceService.deleteDevice(deviceId);
105 - logEntityDeleted(device.getId(), device.getName(), device.getCustomerId()); 110 +
  111 + logEntityAction(deviceId, device,
  112 + device.getCustomerId(),
  113 + ActionType.DELETED, null, strDeviceId);
  114 +
106 } catch (Exception e) { 115 } catch (Exception e) {
107 - throw handleException(e, ActionType.DELETED, "deleteDevice(" + strDeviceId + ")"); 116 + logEntityAction(emptyId(EntityType.DEVICE),
  117 + null,
  118 + null,
  119 + ActionType.DELETED, e, strDeviceId);
  120 + throw handleException(e);
108 } 121 }
109 } 122 }
110 123
@@ -117,13 +130,22 @@ public class DeviceController extends BaseController { @@ -117,13 +130,22 @@ public class DeviceController extends BaseController {
117 checkParameter(DEVICE_ID, strDeviceId); 130 checkParameter(DEVICE_ID, strDeviceId);
118 try { 131 try {
119 CustomerId customerId = new CustomerId(toUUID(strCustomerId)); 132 CustomerId customerId = new CustomerId(toUUID(strCustomerId));
120 - checkCustomerId(customerId); 133 + Customer customer = checkCustomerId(customerId);
121 134
122 DeviceId deviceId = new DeviceId(toUUID(strDeviceId)); 135 DeviceId deviceId = new DeviceId(toUUID(strDeviceId));
123 checkDeviceId(deviceId); 136 checkDeviceId(deviceId);
124 137
125 - return checkNotNull(deviceService.assignDeviceToCustomer(deviceId, customerId)); 138 + Device savedDevice = checkNotNull(deviceService.assignDeviceToCustomer(deviceId, customerId));
  139 +
  140 + logEntityAction(deviceId, savedDevice,
  141 + savedDevice.getCustomerId(),
  142 + ActionType.ASSIGNED_TO_CUSTOMER, null, strDeviceId, strCustomerId, customer.getName());
  143 +
  144 + return savedDevice;
126 } catch (Exception e) { 145 } catch (Exception e) {
  146 + logEntityAction(emptyId(EntityType.DEVICE), null,
  147 + null,
  148 + ActionType.ASSIGNED_TO_CUSTOMER, e, strDeviceId, strCustomerId);
127 throw handleException(e); 149 throw handleException(e);
128 } 150 }
129 } 151 }
@@ -139,8 +161,19 @@ public class DeviceController extends BaseController { @@ -139,8 +161,19 @@ public class DeviceController extends BaseController {
139 if (device.getCustomerId() == null || device.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) { 161 if (device.getCustomerId() == null || device.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) {
140 throw new IncorrectParameterException("Device isn't assigned to any customer!"); 162 throw new IncorrectParameterException("Device isn't assigned to any customer!");
141 } 163 }
142 - return checkNotNull(deviceService.unassignDeviceFromCustomer(deviceId)); 164 + Customer customer = checkCustomerId(device.getCustomerId());
  165 +
  166 + Device savedDevice = checkNotNull(deviceService.unassignDeviceFromCustomer(deviceId));
  167 +
  168 + logEntityAction(deviceId, device,
  169 + device.getCustomerId(),
  170 + ActionType.UNASSIGNED_FROM_CUSTOMER, null, strDeviceId, customer.getId().toString(), customer.getName());
  171 +
  172 + return savedDevice;
143 } catch (Exception e) { 173 } catch (Exception e) {
  174 + logEntityAction(emptyId(EntityType.DEVICE), null,
  175 + null,
  176 + ActionType.UNASSIGNED_FROM_CUSTOMER, e, strDeviceId);
144 throw handleException(e); 177 throw handleException(e);
145 } 178 }
146 } 179 }
@@ -154,8 +187,17 @@ public class DeviceController extends BaseController { @@ -154,8 +187,17 @@ public class DeviceController extends BaseController {
154 DeviceId deviceId = new DeviceId(toUUID(strDeviceId)); 187 DeviceId deviceId = new DeviceId(toUUID(strDeviceId));
155 Device device = checkDeviceId(deviceId); 188 Device device = checkDeviceId(deviceId);
156 Customer publicCustomer = customerService.findOrCreatePublicCustomer(device.getTenantId()); 189 Customer publicCustomer = customerService.findOrCreatePublicCustomer(device.getTenantId());
157 - return checkNotNull(deviceService.assignDeviceToCustomer(deviceId, publicCustomer.getId())); 190 + Device savedDevice = checkNotNull(deviceService.assignDeviceToCustomer(deviceId, publicCustomer.getId()));
  191 +
  192 + logEntityAction(deviceId, savedDevice,
  193 + savedDevice.getCustomerId(),
  194 + ActionType.ASSIGNED_TO_CUSTOMER, null, strDeviceId, publicCustomer.getId().toString(), publicCustomer.getName());
  195 +
  196 + return savedDevice;
158 } catch (Exception e) { 197 } catch (Exception e) {
  198 + logEntityAction(emptyId(EntityType.DEVICE), null,
  199 + null,
  200 + ActionType.ASSIGNED_TO_CUSTOMER, e, strDeviceId);
159 throw handleException(e); 201 throw handleException(e);
160 } 202 }
161 } 203 }
@@ -167,9 +209,16 @@ public class DeviceController extends BaseController { @@ -167,9 +209,16 @@ public class DeviceController extends BaseController {
167 checkParameter(DEVICE_ID, strDeviceId); 209 checkParameter(DEVICE_ID, strDeviceId);
168 try { 210 try {
169 DeviceId deviceId = new DeviceId(toUUID(strDeviceId)); 211 DeviceId deviceId = new DeviceId(toUUID(strDeviceId));
170 - checkDeviceId(deviceId);  
171 - return checkNotNull(deviceCredentialsService.findDeviceCredentialsByDeviceId(deviceId)); 212 + Device device = checkDeviceId(deviceId);
  213 + DeviceCredentials deviceCredentials = checkNotNull(deviceCredentialsService.findDeviceCredentialsByDeviceId(deviceId));
  214 + logEntityAction(deviceId, device,
  215 + device.getCustomerId(),
  216 + ActionType.CREDENTIALS_READ, null, strDeviceId);
  217 + return deviceCredentials;
172 } catch (Exception e) { 218 } catch (Exception e) {
  219 + logEntityAction(emptyId(EntityType.DEVICE), null,
  220 + null,
  221 + ActionType.CREDENTIALS_READ, e, strDeviceId);
173 throw handleException(e); 222 throw handleException(e);
174 } 223 }
175 } 224 }
@@ -183,10 +232,15 @@ public class DeviceController extends BaseController { @@ -183,10 +232,15 @@ public class DeviceController extends BaseController {
183 Device device = checkDeviceId(deviceCredentials.getDeviceId()); 232 Device device = checkDeviceId(deviceCredentials.getDeviceId());
184 DeviceCredentials result = checkNotNull(deviceCredentialsService.updateDeviceCredentials(deviceCredentials)); 233 DeviceCredentials result = checkNotNull(deviceCredentialsService.updateDeviceCredentials(deviceCredentials));
185 actorService.onCredentialsUpdate(getCurrentUser().getTenantId(), deviceCredentials.getDeviceId()); 234 actorService.onCredentialsUpdate(getCurrentUser().getTenantId(), deviceCredentials.getDeviceId());
186 - logEntitySuccess(device.getId(), device.getName(), device.getCustomerId(), ActionType.CREDENTIALS_UPDATED); 235 + logEntityAction(device.getId(), device,
  236 + device.getCustomerId(),
  237 + ActionType.CREDENTIALS_UPDATED, null, deviceCredentials);
187 return result; 238 return result;
188 } catch (Exception e) { 239 } catch (Exception e) {
189 - throw handleException(e, ActionType.CREDENTIALS_UPDATED, "saveDeviceCredentials(" + deviceCredentials + ")"); 240 + logEntityAction(emptyId(EntityType.DEVICE), null,
  241 + null,
  242 + ActionType.CREDENTIALS_UPDATED, e, deviceCredentials);
  243 + throw handleException(e);
190 } 244 }
191 } 245 }
192 246
@@ -18,6 +18,8 @@ package org.thingsboard.server.controller; @@ -18,6 +18,8 @@ package org.thingsboard.server.controller;
18 import org.springframework.http.HttpStatus; 18 import org.springframework.http.HttpStatus;
19 import org.springframework.security.access.prepost.PreAuthorize; 19 import org.springframework.security.access.prepost.PreAuthorize;
20 import org.springframework.web.bind.annotation.*; 20 import org.springframework.web.bind.annotation.*;
  21 +import org.thingsboard.server.common.data.EntityType;
  22 +import org.thingsboard.server.common.data.audit.ActionType;
21 import org.thingsboard.server.common.data.id.PluginId; 23 import org.thingsboard.server.common.data.id.PluginId;
22 import org.thingsboard.server.common.data.id.TenantId; 24 import org.thingsboard.server.common.data.id.TenantId;
23 import org.thingsboard.server.common.data.page.TextPageData; 25 import org.thingsboard.server.common.data.page.TextPageData;
@@ -71,8 +73,17 @@ public class PluginController extends BaseController { @@ -71,8 +73,17 @@ public class PluginController extends BaseController {
71 PluginMetaData plugin = checkNotNull(pluginService.savePlugin(source)); 73 PluginMetaData plugin = checkNotNull(pluginService.savePlugin(source));
72 actorService.onPluginStateChange(plugin.getTenantId(), plugin.getId(), 74 actorService.onPluginStateChange(plugin.getTenantId(), plugin.getId(),
73 created ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED); 75 created ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED);
  76 +
  77 + logEntityAction(plugin.getId(), plugin,
  78 + null,
  79 + created ? ActionType.ADDED : ActionType.UPDATED, null);
  80 +
74 return plugin; 81 return plugin;
75 } catch (Exception e) { 82 } catch (Exception e) {
  83 +
  84 + logEntityAction(emptyId(EntityType.PLUGIN), source,
  85 + null, source.getId() == null ? ActionType.ADDED : ActionType.UPDATED, e);
  86 +
76 throw handleException(e); 87 throw handleException(e);
77 } 88 }
78 } 89 }
@@ -87,7 +98,18 @@ public class PluginController extends BaseController { @@ -87,7 +98,18 @@ public class PluginController extends BaseController {
87 PluginMetaData plugin = checkPlugin(pluginService.findPluginById(pluginId)); 98 PluginMetaData plugin = checkPlugin(pluginService.findPluginById(pluginId));
88 pluginService.activatePluginById(pluginId); 99 pluginService.activatePluginById(pluginId);
89 actorService.onPluginStateChange(plugin.getTenantId(), plugin.getId(), ComponentLifecycleEvent.ACTIVATED); 100 actorService.onPluginStateChange(plugin.getTenantId(), plugin.getId(), ComponentLifecycleEvent.ACTIVATED);
  101 +
  102 + logEntityAction(plugin.getId(), plugin,
  103 + null,
  104 + ActionType.ACTIVATED, null, strPluginId);
  105 +
90 } catch (Exception e) { 106 } catch (Exception e) {
  107 +
  108 + logEntityAction(emptyId(EntityType.PLUGIN),
  109 + null,
  110 + null,
  111 + ActionType.ACTIVATED, e, strPluginId);
  112 +
91 throw handleException(e); 113 throw handleException(e);
92 } 114 }
93 } 115 }
@@ -102,7 +124,18 @@ public class PluginController extends BaseController { @@ -102,7 +124,18 @@ public class PluginController extends BaseController {
102 PluginMetaData plugin = checkPlugin(pluginService.findPluginById(pluginId)); 124 PluginMetaData plugin = checkPlugin(pluginService.findPluginById(pluginId));
103 pluginService.suspendPluginById(pluginId); 125 pluginService.suspendPluginById(pluginId);
104 actorService.onPluginStateChange(plugin.getTenantId(), plugin.getId(), ComponentLifecycleEvent.SUSPENDED); 126 actorService.onPluginStateChange(plugin.getTenantId(), plugin.getId(), ComponentLifecycleEvent.SUSPENDED);
  127 +
  128 + logEntityAction(plugin.getId(), plugin,
  129 + null,
  130 + ActionType.SUSPENDED, null, strPluginId);
  131 +
105 } catch (Exception e) { 132 } catch (Exception e) {
  133 +
  134 + logEntityAction(emptyId(EntityType.PLUGIN),
  135 + null,
  136 + null,
  137 + ActionType.SUSPENDED, e, strPluginId);
  138 +
106 throw handleException(e); 139 throw handleException(e);
107 } 140 }
108 } 141 }
@@ -189,7 +222,16 @@ public class PluginController extends BaseController { @@ -189,7 +222,16 @@ public class PluginController extends BaseController {
189 PluginMetaData plugin = checkPlugin(pluginService.findPluginById(pluginId)); 222 PluginMetaData plugin = checkPlugin(pluginService.findPluginById(pluginId));
190 pluginService.deletePluginById(pluginId); 223 pluginService.deletePluginById(pluginId);
191 actorService.onPluginStateChange(plugin.getTenantId(), plugin.getId(), ComponentLifecycleEvent.DELETED); 224 actorService.onPluginStateChange(plugin.getTenantId(), plugin.getId(), ComponentLifecycleEvent.DELETED);
  225 +
  226 + logEntityAction(pluginId, plugin,
  227 + null,
  228 + ActionType.DELETED, null, strPluginId);
  229 +
192 } catch (Exception e) { 230 } catch (Exception e) {
  231 + logEntityAction(emptyId(EntityType.PLUGIN),
  232 + null,
  233 + null,
  234 + ActionType.DELETED, e, strPluginId);
193 throw handleException(e); 235 throw handleException(e);
194 } 236 }
195 } 237 }
@@ -18,6 +18,8 @@ package org.thingsboard.server.controller; @@ -18,6 +18,8 @@ package org.thingsboard.server.controller;
18 import org.springframework.http.HttpStatus; 18 import org.springframework.http.HttpStatus;
19 import org.springframework.security.access.prepost.PreAuthorize; 19 import org.springframework.security.access.prepost.PreAuthorize;
20 import org.springframework.web.bind.annotation.*; 20 import org.springframework.web.bind.annotation.*;
  21 +import org.thingsboard.server.common.data.EntityType;
  22 +import org.thingsboard.server.common.data.audit.ActionType;
21 import org.thingsboard.server.common.data.id.RuleId; 23 import org.thingsboard.server.common.data.id.RuleId;
22 import org.thingsboard.server.common.data.id.TenantId; 24 import org.thingsboard.server.common.data.id.TenantId;
23 import org.thingsboard.server.common.data.page.TextPageData; 25 import org.thingsboard.server.common.data.page.TextPageData;
@@ -73,8 +75,17 @@ public class RuleController extends BaseController { @@ -73,8 +75,17 @@ public class RuleController extends BaseController {
73 RuleMetaData rule = checkNotNull(ruleService.saveRule(source)); 75 RuleMetaData rule = checkNotNull(ruleService.saveRule(source));
74 actorService.onRuleStateChange(rule.getTenantId(), rule.getId(), 76 actorService.onRuleStateChange(rule.getTenantId(), rule.getId(),
75 created ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED); 77 created ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED);
  78 +
  79 + logEntityAction(rule.getId(), rule,
  80 + null,
  81 + created ? ActionType.ADDED : ActionType.UPDATED, null);
  82 +
76 return rule; 83 return rule;
77 } catch (Exception e) { 84 } catch (Exception e) {
  85 +
  86 + logEntityAction(emptyId(EntityType.RULE), source,
  87 + null, source.getId() == null ? ActionType.ADDED : ActionType.UPDATED, e);
  88 +
78 throw handleException(e); 89 throw handleException(e);
79 } 90 }
80 } 91 }
@@ -89,7 +100,18 @@ public class RuleController extends BaseController { @@ -89,7 +100,18 @@ public class RuleController extends BaseController {
89 RuleMetaData rule = checkRule(ruleService.findRuleById(ruleId)); 100 RuleMetaData rule = checkRule(ruleService.findRuleById(ruleId));
90 ruleService.activateRuleById(ruleId); 101 ruleService.activateRuleById(ruleId);
91 actorService.onRuleStateChange(rule.getTenantId(), rule.getId(), ComponentLifecycleEvent.ACTIVATED); 102 actorService.onRuleStateChange(rule.getTenantId(), rule.getId(), ComponentLifecycleEvent.ACTIVATED);
  103 +
  104 + logEntityAction(rule.getId(), rule,
  105 + null,
  106 + ActionType.ACTIVATED, null, strRuleId);
  107 +
92 } catch (Exception e) { 108 } catch (Exception e) {
  109 +
  110 + logEntityAction(emptyId(EntityType.RULE),
  111 + null,
  112 + null,
  113 + ActionType.ACTIVATED, e, strRuleId);
  114 +
93 throw handleException(e); 115 throw handleException(e);
94 } 116 }
95 } 117 }
@@ -104,7 +126,18 @@ public class RuleController extends BaseController { @@ -104,7 +126,18 @@ public class RuleController extends BaseController {
104 RuleMetaData rule = checkRule(ruleService.findRuleById(ruleId)); 126 RuleMetaData rule = checkRule(ruleService.findRuleById(ruleId));
105 ruleService.suspendRuleById(ruleId); 127 ruleService.suspendRuleById(ruleId);
106 actorService.onRuleStateChange(rule.getTenantId(), rule.getId(), ComponentLifecycleEvent.SUSPENDED); 128 actorService.onRuleStateChange(rule.getTenantId(), rule.getId(), ComponentLifecycleEvent.SUSPENDED);
  129 +
  130 + logEntityAction(rule.getId(), rule,
  131 + null,
  132 + ActionType.SUSPENDED, null, strRuleId);
  133 +
107 } catch (Exception e) { 134 } catch (Exception e) {
  135 +
  136 + logEntityAction(emptyId(EntityType.RULE),
  137 + null,
  138 + null,
  139 + ActionType.SUSPENDED, e, strRuleId);
  140 +
108 throw handleException(e); 141 throw handleException(e);
109 } 142 }
110 } 143 }
@@ -187,7 +220,18 @@ public class RuleController extends BaseController { @@ -187,7 +220,18 @@ public class RuleController extends BaseController {
187 RuleMetaData rule = checkRule(ruleService.findRuleById(ruleId)); 220 RuleMetaData rule = checkRule(ruleService.findRuleById(ruleId));
188 ruleService.deleteRuleById(ruleId); 221 ruleService.deleteRuleById(ruleId);
189 actorService.onRuleStateChange(rule.getTenantId(), rule.getId(), ComponentLifecycleEvent.DELETED); 222 actorService.onRuleStateChange(rule.getTenantId(), rule.getId(), ComponentLifecycleEvent.DELETED);
  223 +
  224 + logEntityAction(ruleId, rule,
  225 + null,
  226 + ActionType.DELETED, null, strRuleId);
  227 +
190 } catch (Exception e) { 228 } catch (Exception e) {
  229 +
  230 + logEntityAction(emptyId(EntityType.RULE),
  231 + null,
  232 + null,
  233 + ActionType.DELETED, e, strRuleId);
  234 +
191 throw handleException(e); 235 throw handleException(e);
192 } 236 }
193 } 237 }
@@ -19,7 +19,9 @@ import org.springframework.beans.factory.annotation.Autowired; @@ -19,7 +19,9 @@ import org.springframework.beans.factory.annotation.Autowired;
19 import org.springframework.http.HttpStatus; 19 import org.springframework.http.HttpStatus;
20 import org.springframework.security.access.prepost.PreAuthorize; 20 import org.springframework.security.access.prepost.PreAuthorize;
21 import org.springframework.web.bind.annotation.*; 21 import org.springframework.web.bind.annotation.*;
  22 +import org.thingsboard.server.common.data.EntityType;
22 import org.thingsboard.server.common.data.User; 23 import org.thingsboard.server.common.data.User;
  24 +import org.thingsboard.server.common.data.audit.ActionType;
23 import org.thingsboard.server.common.data.id.CustomerId; 25 import org.thingsboard.server.common.data.id.CustomerId;
24 import org.thingsboard.server.common.data.id.TenantId; 26 import org.thingsboard.server.common.data.id.TenantId;
25 import org.thingsboard.server.common.data.id.UserId; 27 import org.thingsboard.server.common.data.id.UserId;
@@ -92,8 +94,17 @@ public class UserController extends BaseController { @@ -92,8 +94,17 @@ public class UserController extends BaseController {
92 throw e; 94 throw e;
93 } 95 }
94 } 96 }
  97 +
  98 + logEntityAction(savedUser.getId(), savedUser,
  99 + savedUser.getCustomerId(),
  100 + user.getId() == null ? ActionType.ADDED : ActionType.UPDATED, null);
  101 +
95 return savedUser; 102 return savedUser;
96 } catch (Exception e) { 103 } catch (Exception e) {
  104 +
  105 + logEntityAction(emptyId(EntityType.USER), user,
  106 + null, user.getId() == null ? ActionType.ADDED : ActionType.UPDATED, e);
  107 +
97 throw handleException(e); 108 throw handleException(e);
98 } 109 }
99 } 110 }
@@ -156,9 +167,18 @@ public class UserController extends BaseController { @@ -156,9 +167,18 @@ public class UserController extends BaseController {
156 checkParameter(USER_ID, strUserId); 167 checkParameter(USER_ID, strUserId);
157 try { 168 try {
158 UserId userId = new UserId(toUUID(strUserId)); 169 UserId userId = new UserId(toUUID(strUserId));
159 - checkUserId(userId); 170 + User user = checkUserId(userId);
160 userService.deleteUser(userId); 171 userService.deleteUser(userId);
  172 +
  173 + logEntityAction(userId, user,
  174 + user.getCustomerId(),
  175 + ActionType.DELETED, null, strUserId);
  176 +
161 } catch (Exception e) { 177 } catch (Exception e) {
  178 + logEntityAction(emptyId(EntityType.USER),
  179 + null,
  180 + null,
  181 + ActionType.DELETED, e, strUserId);
162 throw handleException(e); 182 throw handleException(e);
163 } 183 }
164 } 184 }
@@ -30,6 +30,7 @@ import org.springframework.web.context.request.async.DeferredResult; @@ -30,6 +30,7 @@ import org.springframework.web.context.request.async.DeferredResult;
30 import org.thingsboard.server.actors.service.ActorService; 30 import org.thingsboard.server.actors.service.ActorService;
31 import org.thingsboard.server.common.data.id.CustomerId; 31 import org.thingsboard.server.common.data.id.CustomerId;
32 import org.thingsboard.server.common.data.id.TenantId; 32 import org.thingsboard.server.common.data.id.TenantId;
  33 +import org.thingsboard.server.common.data.id.UserId;
33 import org.thingsboard.server.common.data.plugin.PluginMetaData; 34 import org.thingsboard.server.common.data.plugin.PluginMetaData;
34 import org.thingsboard.server.controller.BaseController; 35 import org.thingsboard.server.controller.BaseController;
35 import org.thingsboard.server.dao.model.ModelConstants; 36 import org.thingsboard.server.dao.model.ModelConstants;
@@ -68,7 +69,10 @@ public class PluginApiController extends BaseController { @@ -68,7 +69,10 @@ public class PluginApiController extends BaseController {
68 if(tenantId != null && ModelConstants.NULL_UUID.equals(tenantId.getId())){ 69 if(tenantId != null && ModelConstants.NULL_UUID.equals(tenantId.getId())){
69 tenantId = null; 70 tenantId = null;
70 } 71 }
71 - PluginApiCallSecurityContext securityCtx = new PluginApiCallSecurityContext(pluginMd.getTenantId(), pluginMd.getId(), tenantId, customerId); 72 + UserId userId = getCurrentUser().getId();
  73 + String userName = getCurrentUser().getName();
  74 + PluginApiCallSecurityContext securityCtx = new PluginApiCallSecurityContext(pluginMd.getTenantId(), pluginMd.getId(),
  75 + tenantId, customerId, userId, userName);
72 actorService.process(new BasicPluginRestMsg(securityCtx, new RestRequest(requestEntity, request), result)); 76 actorService.process(new BasicPluginRestMsg(securityCtx, new RestRequest(requestEntity, request), result));
73 } else { 77 } else {
74 result.setResult(new ResponseEntity<>(HttpStatus.FORBIDDEN)); 78 result.setResult(new ResponseEntity<>(HttpStatus.FORBIDDEN));
@@ -28,6 +28,7 @@ import org.springframework.context.annotation.Lazy; @@ -28,6 +28,7 @@ import org.springframework.context.annotation.Lazy;
28 import org.springframework.web.bind.annotation.RequestMapping; 28 import org.springframework.web.bind.annotation.RequestMapping;
29 import org.springframework.web.bind.annotation.RestController; 29 import org.springframework.web.bind.annotation.RestController;
30 import org.thingsboard.server.actors.service.ActorService; 30 import org.thingsboard.server.actors.service.ActorService;
  31 +import org.thingsboard.server.common.data.id.UserId;
31 import org.thingsboard.server.config.WebSocketConfiguration; 32 import org.thingsboard.server.config.WebSocketConfiguration;
32 import org.thingsboard.server.extensions.api.plugins.PluginConstants; 33 import org.thingsboard.server.extensions.api.plugins.PluginConstants;
33 import org.thingsboard.server.service.security.model.SecurityUser; 34 import org.thingsboard.server.service.security.model.SecurityUser;
@@ -151,8 +152,10 @@ public class PluginWebSocketHandler extends TextWebSocketHandler implements Plug @@ -151,8 +152,10 @@ public class PluginWebSocketHandler extends TextWebSocketHandler implements Plug
151 TenantId tenantId = currentUser.getTenantId(); 152 TenantId tenantId = currentUser.getTenantId();
152 CustomerId customerId = currentUser.getCustomerId(); 153 CustomerId customerId = currentUser.getCustomerId();
153 if (PluginApiController.validatePluginAccess(pluginMd, tenantId, customerId)) { 154 if (PluginApiController.validatePluginAccess(pluginMd, tenantId, customerId)) {
  155 + UserId userId = currentUser.getId();
  156 + String userName = currentUser.getName();
154 PluginApiCallSecurityContext securityCtx = new PluginApiCallSecurityContext(pluginMd.getTenantId(), pluginMd.getId(), tenantId, 157 PluginApiCallSecurityContext securityCtx = new PluginApiCallSecurityContext(pluginMd.getTenantId(), pluginMd.getId(), tenantId,
155 - currentUser.getCustomerId()); 158 + currentUser.getCustomerId(), userId, userName);
156 return new BasicPluginWebsocketSessionRef(UUID.randomUUID().toString(), securityCtx, session.getUri(), session.getAttributes(), 159 return new BasicPluginWebsocketSessionRef(UUID.randomUUID().toString(), securityCtx, session.getUri(), session.getAttributes(),
157 session.getLocalAddress(), session.getRemoteAddress()); 160 session.getLocalAddress(), session.getRemoteAddress());
158 } else { 161 } else {
@@ -306,6 +306,14 @@ audit_log: @@ -306,6 +306,14 @@ audit_log:
306 by_tenant_partitioning: "${AUDIT_LOG_BY_TENANT_PARTITIONING:MONTHS}" 306 by_tenant_partitioning: "${AUDIT_LOG_BY_TENANT_PARTITIONING:MONTHS}"
307 # Number of days as history period if startTime and endTime are not specified 307 # Number of days as history period if startTime and endTime are not specified
308 default_query_period: "${AUDIT_LOG_DEFAULT_QUERY_PERIOD:30}" 308 default_query_period: "${AUDIT_LOG_DEFAULT_QUERY_PERIOD:30}"
309 - exceptions:  
310 - # Enable/disable audit log functionality for exceptions.  
311 - enabled: "${AUDIT_LOG_EXCEPTIONS_ENABLED:true}"  
  309 + # Logging levels per each entity type.
  310 + # Allowed values: OFF (disable), W (log write operations), RW (log read and write operations)
  311 + logging_level:
  312 + mask:
  313 + "device": "W"
  314 + "asset": "W"
  315 + "dashboard": "W"
  316 + "customer": "W"
  317 + "user": "W"
  318 + "rule": "W"
  319 + "plugin": "W"
@@ -15,6 +15,27 @@ @@ -15,6 +15,27 @@
15 */ 15 */
16 package org.thingsboard.server.common.data.audit; 16 package org.thingsboard.server.common.data.audit;
17 17
  18 +import lombok.Getter;
  19 +
  20 +@Getter
18 public enum ActionType { 21 public enum ActionType {
19 - ADDED, DELETED, UPDATED, ATTRIBUTE_UPDATED, ATTRIBUTE_DELETED, ATTRIBUTE_ADDED, RPC_CALL, CREDENTIALS_UPDATED  
20 -}  
  22 + ADDED(false), // log entity
  23 + DELETED(false), // log string id
  24 + UPDATED(false), // log entity
  25 + ATTRIBUTES_UPDATED(false), // log attributes/values
  26 + ATTRIBUTES_DELETED(false), // log attributes
  27 + RPC_CALL(false), // log method and params
  28 + CREDENTIALS_UPDATED(false), // log new credentials
  29 + ASSIGNED_TO_CUSTOMER(false), // log customer name
  30 + UNASSIGNED_FROM_CUSTOMER(false), // log customer name
  31 + ACTIVATED(false), // log string id
  32 + SUSPENDED(false), // log string id
  33 + CREDENTIALS_READ(true), // log device id
  34 + ATTRIBUTES_READ(true); // log attributes
  35 +
  36 + private final boolean isRead;
  37 +
  38 + ActionType(boolean isRead) {
  39 + this.isRead = isRead;
  40 + }
  41 +}
  1 +/**
  2 + * Copyright © 2016-2017 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.dao.audit;
  17 +
  18 +import org.thingsboard.server.common.data.EntityType;
  19 +import org.thingsboard.server.common.data.audit.ActionType;
  20 +
  21 +import java.util.HashMap;
  22 +import java.util.Map;
  23 +
  24 +public class AuditLogLevelFilter {
  25 +
  26 + private Map<EntityType, AuditLogLevelMask> entityTypeMask = new HashMap<>();
  27 +
  28 + public AuditLogLevelFilter(Map<String, String> mask) {
  29 + entityTypeMask.clear();
  30 + mask.forEach((entityTypeStr, logLevelMaskStr) -> {
  31 + EntityType entityType = EntityType.valueOf(entityTypeStr.toUpperCase());
  32 + AuditLogLevelMask logLevelMask = AuditLogLevelMask.valueOf(logLevelMaskStr.toUpperCase());
  33 + entityTypeMask.put(entityType, logLevelMask);
  34 + });
  35 + }
  36 +
  37 + public boolean logEnabled(EntityType entityType, ActionType actionType) {
  38 + AuditLogLevelMask logLevelMask = entityTypeMask.get(entityType);
  39 + if (logLevelMask != null) {
  40 + return actionType.isRead() ? logLevelMask.isRead() : logLevelMask.isWrite();
  41 + } else {
  42 + return false;
  43 + }
  44 + }
  45 +
  46 +}
  1 +/**
  2 + * Copyright © 2016-2017 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.dao.audit;
  17 +
  18 +import lombok.Getter;
  19 +
  20 +@Getter
  21 +public enum AuditLogLevelMask {
  22 +
  23 + OFF(false, false),
  24 + W(true, false),
  25 + RW(true, true);
  26 +
  27 + private final boolean write;
  28 + private final boolean read;
  29 +
  30 + AuditLogLevelMask(boolean write, boolean read) {
  31 + this.write = write;
  32 + this.read = read;
  33 + }
  34 +}
@@ -17,14 +17,13 @@ package org.thingsboard.server.dao.audit; @@ -17,14 +17,13 @@ package org.thingsboard.server.dao.audit;
17 17
18 import com.fasterxml.jackson.databind.JsonNode; 18 import com.fasterxml.jackson.databind.JsonNode;
19 import com.google.common.util.concurrent.ListenableFuture; 19 import com.google.common.util.concurrent.ListenableFuture;
  20 +import org.thingsboard.server.common.data.BaseData;
  21 +import org.thingsboard.server.common.data.HasName;
20 import org.thingsboard.server.common.data.User; 22 import org.thingsboard.server.common.data.User;
21 import org.thingsboard.server.common.data.audit.ActionStatus; 23 import org.thingsboard.server.common.data.audit.ActionStatus;
22 import org.thingsboard.server.common.data.audit.ActionType; 24 import org.thingsboard.server.common.data.audit.ActionType;
23 import org.thingsboard.server.common.data.audit.AuditLog; 25 import org.thingsboard.server.common.data.audit.AuditLog;
24 -import org.thingsboard.server.common.data.id.CustomerId;  
25 -import org.thingsboard.server.common.data.id.EntityId;  
26 -import org.thingsboard.server.common.data.id.TenantId;  
27 -import org.thingsboard.server.common.data.id.UserId; 26 +import org.thingsboard.server.common.data.id.*;
28 import org.thingsboard.server.common.data.page.TimePageData; 27 import org.thingsboard.server.common.data.page.TimePageData;
29 import org.thingsboard.server.common.data.page.TimePageLink; 28 import org.thingsboard.server.common.data.page.TimePageLink;
30 29
@@ -40,13 +39,15 @@ public interface AuditLogService { @@ -40,13 +39,15 @@ public interface AuditLogService {
40 39
41 TimePageData<AuditLog> findAuditLogsByTenantId(TenantId tenantId, TimePageLink pageLink); 40 TimePageData<AuditLog> findAuditLogsByTenantId(TenantId tenantId, TimePageLink pageLink);
42 41
43 - ListenableFuture<List<Void>> logEntityAction(User user,  
44 - EntityId entityId,  
45 - String entityName,  
46 - CustomerId customerId,  
47 - ActionType actionType,  
48 - JsonNode actionData,  
49 - ActionStatus actionStatus,  
50 - String actionFailureDetails); 42 + <E extends BaseData<I> & HasName,
  43 + I extends UUIDBased & EntityId> ListenableFuture<List<Void>> logEntityAction(
  44 + TenantId tenantId,
  45 + CustomerId customerId,
  46 + UserId userId,
  47 + String userName,
  48 + I entityId,
  49 + E entity,
  50 + ActionType actionType,
  51 + Exception e, Object... additionalInfo);
51 52
52 } 53 }
@@ -17,6 +17,9 @@ package org.thingsboard.server.dao.audit; @@ -17,6 +17,9 @@ package org.thingsboard.server.dao.audit;
17 17
18 import com.datastax.driver.core.utils.UUIDs; 18 import com.datastax.driver.core.utils.UUIDs;
19 import com.fasterxml.jackson.databind.JsonNode; 19 import com.fasterxml.jackson.databind.JsonNode;
  20 +import com.fasterxml.jackson.databind.ObjectMapper;
  21 +import com.fasterxml.jackson.databind.node.ArrayNode;
  22 +import com.fasterxml.jackson.databind.node.ObjectNode;
20 import com.google.common.collect.Lists; 23 import com.google.common.collect.Lists;
21 import com.google.common.util.concurrent.Futures; 24 import com.google.common.util.concurrent.Futures;
22 import com.google.common.util.concurrent.ListenableFuture; 25 import com.google.common.util.concurrent.ListenableFuture;
@@ -24,16 +27,24 @@ import lombok.extern.slf4j.Slf4j; @@ -24,16 +27,24 @@ import lombok.extern.slf4j.Slf4j;
24 import org.springframework.beans.factory.annotation.Autowired; 27 import org.springframework.beans.factory.annotation.Autowired;
25 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 28 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
26 import org.springframework.stereotype.Service; 29 import org.springframework.stereotype.Service;
27 -import org.thingsboard.server.common.data.User; 30 +import org.springframework.util.StringUtils;
  31 +import org.thingsboard.server.common.data.BaseData;
  32 +import org.thingsboard.server.common.data.EntityType;
  33 +import org.thingsboard.server.common.data.HasName;
28 import org.thingsboard.server.common.data.audit.ActionStatus; 34 import org.thingsboard.server.common.data.audit.ActionStatus;
29 import org.thingsboard.server.common.data.audit.ActionType; 35 import org.thingsboard.server.common.data.audit.ActionType;
30 import org.thingsboard.server.common.data.audit.AuditLog; 36 import org.thingsboard.server.common.data.audit.AuditLog;
31 import org.thingsboard.server.common.data.id.*; 37 import org.thingsboard.server.common.data.id.*;
  38 +import org.thingsboard.server.common.data.kv.AttributeKvEntry;
32 import org.thingsboard.server.common.data.page.TimePageData; 39 import org.thingsboard.server.common.data.page.TimePageData;
33 import org.thingsboard.server.common.data.page.TimePageLink; 40 import org.thingsboard.server.common.data.page.TimePageLink;
  41 +import org.thingsboard.server.common.data.security.DeviceCredentials;
  42 +import org.thingsboard.server.dao.entity.EntityService;
34 import org.thingsboard.server.dao.exception.DataValidationException; 43 import org.thingsboard.server.dao.exception.DataValidationException;
35 import org.thingsboard.server.dao.service.DataValidator; 44 import org.thingsboard.server.dao.service.DataValidator;
36 45
  46 +import java.io.PrintWriter;
  47 +import java.io.StringWriter;
37 import java.util.List; 48 import java.util.List;
38 49
39 import static org.thingsboard.server.dao.service.Validator.validateEntityId; 50 import static org.thingsboard.server.dao.service.Validator.validateEntityId;
@@ -44,12 +55,20 @@ import static org.thingsboard.server.dao.service.Validator.validateId; @@ -44,12 +55,20 @@ import static org.thingsboard.server.dao.service.Validator.validateId;
44 @ConditionalOnProperty(prefix = "audit_log", value = "enabled", havingValue = "true") 55 @ConditionalOnProperty(prefix = "audit_log", value = "enabled", havingValue = "true")
45 public class AuditLogServiceImpl implements AuditLogService { 56 public class AuditLogServiceImpl implements AuditLogService {
46 57
  58 + private static final ObjectMapper objectMapper = new ObjectMapper();
  59 +
47 private static final String INCORRECT_TENANT_ID = "Incorrect tenantId "; 60 private static final String INCORRECT_TENANT_ID = "Incorrect tenantId ";
48 private static final int INSERTS_PER_ENTRY = 3; 61 private static final int INSERTS_PER_ENTRY = 3;
49 62
50 @Autowired 63 @Autowired
  64 + private AuditLogLevelFilter auditLogLevelFilter;
  65 +
  66 + @Autowired
51 private AuditLogDao auditLogDao; 67 private AuditLogDao auditLogDao;
52 68
  69 + @Autowired
  70 + private EntityService entityService;
  71 +
53 @Override 72 @Override
54 public TimePageData<AuditLog> findAuditLogsByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, TimePageLink pageLink) { 73 public TimePageData<AuditLog> findAuditLogsByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, TimePageLink pageLink) {
55 log.trace("Executing findAuditLogsByTenantIdAndCustomerId [{}], [{}], [{}]", tenantId, customerId, pageLink); 74 log.trace("Executing findAuditLogsByTenantIdAndCustomerId [{}], [{}], [{}]", tenantId, customerId, pageLink);
@@ -86,25 +105,149 @@ public class AuditLogServiceImpl implements AuditLogService { @@ -86,25 +105,149 @@ public class AuditLogServiceImpl implements AuditLogService {
86 } 105 }
87 106
88 @Override 107 @Override
89 - public ListenableFuture<List<Void>> logEntityAction(User user,  
90 - EntityId entityId,  
91 - String entityName,  
92 - CustomerId customerId,  
93 - ActionType actionType,  
94 - JsonNode actionData,  
95 - ActionStatus actionStatus,  
96 - String actionFailureDetails) {  
97 - return logAction(  
98 - user.getTenantId(),  
99 - entityId,  
100 - entityName,  
101 - customerId,  
102 - user.getId(),  
103 - user.getName(),  
104 - actionType,  
105 - actionData,  
106 - actionStatus,  
107 - actionFailureDetails); 108 + public <E extends BaseData<I> & HasName, I extends UUIDBased & EntityId> ListenableFuture<List<Void>>
  109 + logEntityAction(TenantId tenantId, CustomerId customerId, UserId userId, String userName, I entityId, E entity,
  110 + ActionType actionType, Exception e, Object... additionalInfo) {
  111 + if (canLog(entityId.getEntityType(), actionType)) {
  112 + JsonNode actionData = constructActionData(entityId, entity, actionType, additionalInfo);
  113 + ActionStatus actionStatus = ActionStatus.SUCCESS;
  114 + String failureDetails = "";
  115 + String entityName = "";
  116 + if (entity != null) {
  117 + entityName = entity.getName();
  118 + } else {
  119 + try {
  120 + entityName = entityService.fetchEntityNameAsync(entityId).get();
  121 + } catch (Exception ex) {}
  122 + }
  123 + if (e != null) {
  124 + actionStatus = ActionStatus.FAILURE;
  125 + failureDetails = getFailureStack(e);
  126 + }
  127 + if (actionType == ActionType.RPC_CALL) {
  128 + String rpcErrorString = extractParameter(String.class, additionalInfo);
  129 + if (!StringUtils.isEmpty(rpcErrorString)) {
  130 + actionStatus = ActionStatus.FAILURE;
  131 + failureDetails = rpcErrorString;
  132 + }
  133 + }
  134 + return logAction(tenantId,
  135 + entityId,
  136 + entityName,
  137 + customerId,
  138 + userId,
  139 + userName,
  140 + actionType,
  141 + actionData,
  142 + actionStatus,
  143 + failureDetails);
  144 + } else {
  145 + return null;
  146 + }
  147 + }
  148 +
  149 + private <E extends BaseData<I> & HasName, I extends UUIDBased & EntityId> JsonNode constructActionData(I entityId,
  150 + E entity,
  151 + ActionType actionType,
  152 + Object... additionalInfo) {
  153 + ObjectNode actionData = objectMapper.createObjectNode();
  154 + switch(actionType) {
  155 + case ADDED:
  156 + case UPDATED:
  157 + ObjectNode entityNode = objectMapper.valueToTree(entity);
  158 + if (entityId.getEntityType() == EntityType.DASHBOARD) {
  159 + entityNode.put("configuration", "");
  160 + }
  161 + actionData.set("entity", entityNode);
  162 + break;
  163 + case DELETED:
  164 + case ACTIVATED:
  165 + case SUSPENDED:
  166 + case CREDENTIALS_READ:
  167 + String strEntityId = extractParameter(String.class, additionalInfo);
  168 + actionData.put("entityId", strEntityId);
  169 + break;
  170 + case ATTRIBUTES_UPDATED:
  171 + actionData.put("entityId", entityId.toString());
  172 + String scope = extractParameter(String.class, 0, additionalInfo);
  173 + List<AttributeKvEntry> attributes = extractParameter(List.class, 1, additionalInfo);
  174 + actionData.put("scope", scope);
  175 + ObjectNode attrsNode = objectMapper.createObjectNode();
  176 + if (attributes != null) {
  177 + for (AttributeKvEntry attr : attributes) {
  178 + attrsNode.put(attr.getKey(), attr.getValueAsString());
  179 + }
  180 + }
  181 + actionData.set("attributes", attrsNode);
  182 + break;
  183 + case ATTRIBUTES_DELETED:
  184 + case ATTRIBUTES_READ:
  185 + actionData.put("entityId", entityId.toString());
  186 + scope = extractParameter(String.class, 0, additionalInfo);
  187 + actionData.put("scope", scope);
  188 + List<String> keys = extractParameter(List.class, 1, additionalInfo);
  189 + ArrayNode attrsArrayNode = actionData.putArray("attributes");
  190 + if (keys != null) {
  191 + keys.forEach(attrsArrayNode::add);
  192 + }
  193 + break;
  194 + case RPC_CALL:
  195 + actionData.put("entityId", entityId.toString());
  196 + Boolean oneWay = extractParameter(Boolean.class, 1, additionalInfo);
  197 + String method = extractParameter(String.class, 2, additionalInfo);
  198 + String params = extractParameter(String.class, 3, additionalInfo);
  199 + actionData.put("oneWay", oneWay);
  200 + actionData.put("method", method);
  201 + actionData.put("params", params);
  202 + break;
  203 + case CREDENTIALS_UPDATED:
  204 + actionData.put("entityId", entityId.toString());
  205 + DeviceCredentials deviceCredentials = extractParameter(DeviceCredentials.class, additionalInfo);
  206 + actionData.set("credentials", objectMapper.valueToTree(deviceCredentials));
  207 + break;
  208 + case ASSIGNED_TO_CUSTOMER:
  209 + strEntityId = extractParameter(String.class, 0, additionalInfo);
  210 + String strCustomerId = extractParameter(String.class, 1, additionalInfo);
  211 + String strCustomerName = extractParameter(String.class, 2, additionalInfo);
  212 + actionData.put("entityId", strEntityId);
  213 + actionData.put("assignedCustomerId", strCustomerId);
  214 + actionData.put("assignedCustomerName", strCustomerName);
  215 + break;
  216 + case UNASSIGNED_FROM_CUSTOMER:
  217 + strEntityId = extractParameter(String.class, 0, additionalInfo);
  218 + strCustomerId = extractParameter(String.class, 1, additionalInfo);
  219 + strCustomerName = extractParameter(String.class, 2, additionalInfo);
  220 + actionData.put("entityId", strEntityId);
  221 + actionData.put("unassignedCustomerId", strCustomerId);
  222 + actionData.put("unassignedCustomerName", strCustomerName);
  223 + break;
  224 + }
  225 + return actionData;
  226 + }
  227 +
  228 + private <T> T extractParameter(Class<T> clazz, Object... additionalInfo) {
  229 + return extractParameter(clazz, 0, additionalInfo);
  230 + }
  231 +
  232 + private <T> T extractParameter(Class<T> clazz, int index, Object... additionalInfo) {
  233 + T result = null;
  234 + if (additionalInfo != null && additionalInfo.length > index) {
  235 + Object paramObject = additionalInfo[index];
  236 + if (clazz.isInstance(paramObject)) {
  237 + result = clazz.cast(paramObject);
  238 + }
  239 + }
  240 + return result;
  241 + }
  242 +
  243 + private String getFailureStack(Exception e) {
  244 + StringWriter sw = new StringWriter();
  245 + e.printStackTrace(new PrintWriter(sw));
  246 + return sw.toString();
  247 + }
  248 +
  249 + private boolean canLog(EntityType entityType, ActionType actionType) {
  250 + return auditLogLevelFilter.logEnabled(entityType, actionType);
108 } 251 }
109 252
110 private AuditLog createAuditLogEntry(TenantId tenantId, 253 private AuditLog createAuditLogEntry(TenantId tenantId,
@@ -19,14 +19,13 @@ import com.fasterxml.jackson.databind.JsonNode; @@ -19,14 +19,13 @@ import com.fasterxml.jackson.databind.JsonNode;
19 import com.google.common.util.concurrent.Futures; 19 import com.google.common.util.concurrent.Futures;
20 import com.google.common.util.concurrent.ListenableFuture; 20 import com.google.common.util.concurrent.ListenableFuture;
21 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 21 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
  22 +import org.thingsboard.server.common.data.BaseData;
  23 +import org.thingsboard.server.common.data.HasName;
22 import org.thingsboard.server.common.data.User; 24 import org.thingsboard.server.common.data.User;
23 import org.thingsboard.server.common.data.audit.ActionStatus; 25 import org.thingsboard.server.common.data.audit.ActionStatus;
24 import org.thingsboard.server.common.data.audit.ActionType; 26 import org.thingsboard.server.common.data.audit.ActionType;
25 import org.thingsboard.server.common.data.audit.AuditLog; 27 import org.thingsboard.server.common.data.audit.AuditLog;
26 -import org.thingsboard.server.common.data.id.CustomerId;  
27 -import org.thingsboard.server.common.data.id.EntityId;  
28 -import org.thingsboard.server.common.data.id.TenantId;  
29 -import org.thingsboard.server.common.data.id.UserId; 28 +import org.thingsboard.server.common.data.id.*;
30 import org.thingsboard.server.common.data.page.TimePageData; 29 import org.thingsboard.server.common.data.page.TimePageData;
31 import org.thingsboard.server.common.data.page.TimePageLink; 30 import org.thingsboard.server.common.data.page.TimePageLink;
32 31
@@ -57,7 +56,8 @@ public class DummyAuditLogServiceImpl implements AuditLogService { @@ -57,7 +56,8 @@ public class DummyAuditLogServiceImpl implements AuditLogService {
57 } 56 }
58 57
59 @Override 58 @Override
60 - public ListenableFuture<List<Void>> logEntityAction(User user, EntityId entityId, String entityName, CustomerId customerId, ActionType actionType, JsonNode actionData, ActionStatus actionStatus, String actionFailureDetails) {  
61 - return Futures.immediateFuture(Collections.emptyList()); 59 + public <E extends BaseData<I> & HasName, I extends UUIDBased & EntityId> ListenableFuture<List<Void>> logEntityAction(TenantId tenantId, CustomerId customerId, UserId userId, String userName, I entityId, E entity, ActionType actionType, Exception e, Object... additionalInfo) {
  60 + return null;
62 } 61 }
  62 +
63 } 63 }
@@ -15,46 +15,10 @@ @@ -15,46 +15,10 @@
15 */ 15 */
16 package org.thingsboard.server.dao.sql.audit; 16 package org.thingsboard.server.dao.sql.audit;
17 17
18 -import org.springframework.data.domain.Pageable;  
19 -import org.springframework.data.jpa.repository.Query; 18 +import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
20 import org.springframework.data.repository.CrudRepository; 19 import org.springframework.data.repository.CrudRepository;
21 -import org.springframework.data.repository.query.Param;  
22 -import org.thingsboard.server.common.data.EntityType;  
23 import org.thingsboard.server.dao.model.sql.AuditLogEntity; 20 import org.thingsboard.server.dao.model.sql.AuditLogEntity;
24 21
25 -import java.util.List; 22 +public interface AuditLogRepository extends CrudRepository<AuditLogEntity, String>, JpaSpecificationExecutor<AuditLogEntity> {
26 23
27 -public interface AuditLogRepository extends CrudRepository<AuditLogEntity, String> {  
28 -  
29 - @Query("SELECT al FROM AuditLogEntity al WHERE al.tenantId = :tenantId " +  
30 - "AND al.id > :idOffset ORDER BY al.id")  
31 - List<AuditLogEntity> findByTenantId(@Param("tenantId") String tenantId,  
32 - @Param("idOffset") String idOffset,  
33 - Pageable pageable);  
34 -  
35 - @Query("SELECT al FROM AuditLogEntity al WHERE al.tenantId = :tenantId " +  
36 - "AND al.entityType = :entityType " +  
37 - "AND al.entityId = :entityId " +  
38 - "AND al.id > :idOffset ORDER BY al.id")  
39 - List<AuditLogEntity> findByTenantIdAndEntityId(@Param("tenantId") String tenantId,  
40 - @Param("entityId") String entityId,  
41 - @Param("entityType") EntityType entityType,  
42 - @Param("idOffset") String idOffset,  
43 - Pageable pageable);  
44 -  
45 - @Query("SELECT al FROM AuditLogEntity al WHERE al.tenantId = :tenantId " +  
46 - "AND al.customerId = :customerId " +  
47 - "AND al.id > :idOffset ORDER BY al.id")  
48 - List<AuditLogEntity> findByTenantIdAndCustomerId(@Param("tenantId") String tenantId,  
49 - @Param("customerId") String customerId,  
50 - @Param("idOffset") String idOffset,  
51 - Pageable pageable);  
52 -  
53 - @Query("SELECT al FROM AuditLogEntity al WHERE al.tenantId = :tenantId " +  
54 - "AND al.userId = :userId " +  
55 - "AND al.id > :idOffset ORDER BY al.id")  
56 - List<AuditLogEntity> findByTenantIdAndUserId(@Param("tenantId") String tenantId,  
57 - @Param("userId") String userId,  
58 - @Param("idOffset") String idOffset,  
59 - Pageable pageable);  
60 } 24 }
@@ -20,8 +20,12 @@ import com.google.common.util.concurrent.ListeningExecutorService; @@ -20,8 +20,12 @@ import com.google.common.util.concurrent.ListeningExecutorService;
20 import com.google.common.util.concurrent.MoreExecutors; 20 import com.google.common.util.concurrent.MoreExecutors;
21 import org.springframework.beans.factory.annotation.Autowired; 21 import org.springframework.beans.factory.annotation.Autowired;
22 import org.springframework.data.domain.PageRequest; 22 import org.springframework.data.domain.PageRequest;
  23 +import org.springframework.data.domain.Pageable;
  24 +import org.springframework.data.domain.Sort;
  25 +import org.springframework.data.jpa.domain.Specification;
23 import org.springframework.data.repository.CrudRepository; 26 import org.springframework.data.repository.CrudRepository;
24 import org.springframework.stereotype.Component; 27 import org.springframework.stereotype.Component;
  28 +import org.thingsboard.server.common.data.UUIDConverter;
25 import org.thingsboard.server.common.data.audit.AuditLog; 29 import org.thingsboard.server.common.data.audit.AuditLog;
26 import org.thingsboard.server.common.data.id.CustomerId; 30 import org.thingsboard.server.common.data.id.CustomerId;
27 import org.thingsboard.server.common.data.id.EntityId; 31 import org.thingsboard.server.common.data.id.EntityId;
@@ -31,15 +35,18 @@ import org.thingsboard.server.dao.DaoUtil; @@ -31,15 +35,18 @@ import org.thingsboard.server.dao.DaoUtil;
31 import org.thingsboard.server.dao.audit.AuditLogDao; 35 import org.thingsboard.server.dao.audit.AuditLogDao;
32 import org.thingsboard.server.dao.model.sql.AuditLogEntity; 36 import org.thingsboard.server.dao.model.sql.AuditLogEntity;
33 import org.thingsboard.server.dao.sql.JpaAbstractDao; 37 import org.thingsboard.server.dao.sql.JpaAbstractDao;
  38 +import org.thingsboard.server.dao.sql.JpaAbstractSearchTimeDao;
34 import org.thingsboard.server.dao.util.SqlDao; 39 import org.thingsboard.server.dao.util.SqlDao;
35 40
36 import javax.annotation.PreDestroy; 41 import javax.annotation.PreDestroy;
  42 +import javax.persistence.criteria.Predicate;
  43 +import java.util.ArrayList;
37 import java.util.List; 44 import java.util.List;
38 import java.util.UUID; 45 import java.util.UUID;
39 import java.util.concurrent.Executors; 46 import java.util.concurrent.Executors;
40 47
41 -import static org.thingsboard.server.common.data.UUIDConverter.fromTimeUUID;  
42 -import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID_STR; 48 +import static org.springframework.data.jpa.domain.Specifications.where;
  49 +import static org.thingsboard.server.dao.model.ModelConstants.ID_PROPERTY;
43 50
44 @Component 51 @Component
45 @SqlDao 52 @SqlDao
@@ -95,41 +102,54 @@ public class JpaAuditLogDao extends JpaAbstractDao<AuditLogEntity, AuditLog> imp @@ -95,41 +102,54 @@ public class JpaAuditLogDao extends JpaAbstractDao<AuditLogEntity, AuditLog> imp
95 102
96 @Override 103 @Override
97 public List<AuditLog> findAuditLogsByTenantIdAndEntityId(UUID tenantId, EntityId entityId, TimePageLink pageLink) { 104 public List<AuditLog> findAuditLogsByTenantIdAndEntityId(UUID tenantId, EntityId entityId, TimePageLink pageLink) {
98 - return DaoUtil.convertDataList(  
99 - auditLogRepository.findByTenantIdAndEntityId(  
100 - fromTimeUUID(tenantId),  
101 - fromTimeUUID(entityId.getId()),  
102 - entityId.getEntityType(),  
103 - pageLink.getIdOffset() == null ? NULL_UUID_STR : fromTimeUUID(pageLink.getIdOffset()),  
104 - new PageRequest(0, pageLink.getLimit()))); 105 + return findAuditLogs(tenantId, entityId, null, null, pageLink);
105 } 106 }
106 107
107 @Override 108 @Override
108 public List<AuditLog> findAuditLogsByTenantIdAndCustomerId(UUID tenantId, CustomerId customerId, TimePageLink pageLink) { 109 public List<AuditLog> findAuditLogsByTenantIdAndCustomerId(UUID tenantId, CustomerId customerId, TimePageLink pageLink) {
109 - return DaoUtil.convertDataList(  
110 - auditLogRepository.findByTenantIdAndCustomerId(  
111 - fromTimeUUID(tenantId),  
112 - fromTimeUUID(customerId.getId()),  
113 - pageLink.getIdOffset() == null ? NULL_UUID_STR : fromTimeUUID(pageLink.getIdOffset()),  
114 - new PageRequest(0, pageLink.getLimit()))); 110 + return findAuditLogs(tenantId, null, customerId, null, pageLink);
115 } 111 }
116 112
117 @Override 113 @Override
118 public List<AuditLog> findAuditLogsByTenantIdAndUserId(UUID tenantId, UserId userId, TimePageLink pageLink) { 114 public List<AuditLog> findAuditLogsByTenantIdAndUserId(UUID tenantId, UserId userId, TimePageLink pageLink) {
119 - return DaoUtil.convertDataList(  
120 - auditLogRepository.findByTenantIdAndUserId(  
121 - fromTimeUUID(tenantId),  
122 - fromTimeUUID(userId.getId()),  
123 - pageLink.getIdOffset() == null ? NULL_UUID_STR : fromTimeUUID(pageLink.getIdOffset()),  
124 - new PageRequest(0, pageLink.getLimit()))); 115 + return findAuditLogs(tenantId, null, null, userId, pageLink);
125 } 116 }
126 117
127 @Override 118 @Override
128 public List<AuditLog> findAuditLogsByTenantId(UUID tenantId, TimePageLink pageLink) { 119 public List<AuditLog> findAuditLogsByTenantId(UUID tenantId, TimePageLink pageLink) {
129 - return DaoUtil.convertDataList(  
130 - auditLogRepository.findByTenantId(  
131 - fromTimeUUID(tenantId),  
132 - pageLink.getIdOffset() == null ? NULL_UUID_STR : fromTimeUUID(pageLink.getIdOffset()),  
133 - new PageRequest(0, pageLink.getLimit()))); 120 + return findAuditLogs(tenantId, null, null, null, pageLink);
  121 + }
  122 +
  123 + private List<AuditLog> findAuditLogs(UUID tenantId, EntityId entityId, CustomerId customerId, UserId userId, TimePageLink pageLink) {
  124 + Specification<AuditLogEntity> timeSearchSpec = JpaAbstractSearchTimeDao.getTimeSearchPageSpec(pageLink, "id");
  125 + Specification<AuditLogEntity> fieldsSpec = getEntityFieldsSpec(tenantId, entityId, customerId, userId);
  126 + Sort.Direction sortDirection = pageLink.isAscOrder() ? Sort.Direction.ASC : Sort.Direction.DESC;
  127 + Pageable pageable = new PageRequest(0, pageLink.getLimit(), sortDirection, ID_PROPERTY);
  128 + return DaoUtil.convertDataList(auditLogRepository.findAll(where(timeSearchSpec).and(fieldsSpec), pageable).getContent());
  129 + }
  130 +
  131 + private Specification<AuditLogEntity> getEntityFieldsSpec(UUID tenantId, EntityId entityId, CustomerId customerId, UserId userId) {
  132 + return (root, criteriaQuery, criteriaBuilder) -> {
  133 + List<Predicate> predicates = new ArrayList<>();
  134 + if (tenantId != null) {
  135 + Predicate tenantIdPredicate = criteriaBuilder.equal(root.get("tenantId"), UUIDConverter.fromTimeUUID(tenantId));
  136 + predicates.add(tenantIdPredicate);
  137 + }
  138 + if (entityId != null) {
  139 + Predicate entityTypePredicate = criteriaBuilder.equal(root.get("entityType"), entityId.getEntityType());
  140 + predicates.add(entityTypePredicate);
  141 + Predicate entityIdPredicate = criteriaBuilder.equal(root.get("entityId"), UUIDConverter.fromTimeUUID(entityId.getId()));
  142 + predicates.add(entityIdPredicate);
  143 + }
  144 + if (customerId != null) {
  145 + Predicate tenantIdPredicate = criteriaBuilder.equal(root.get("customerId"), UUIDConverter.fromTimeUUID(customerId.getId()));
  146 + predicates.add(tenantIdPredicate);
  147 + }
  148 + if (userId != null) {
  149 + Predicate tenantIdPredicate = criteriaBuilder.equal(root.get("userId"), UUIDConverter.fromTimeUUID(userId.getId()));
  150 + predicates.add(tenantIdPredicate);
  151 + }
  152 + return criteriaBuilder.and(predicates.toArray(new Predicate[]{}));
  153 + };
134 } 154 }
135 } 155 }
@@ -57,9 +57,9 @@ CREATE TABLE IF NOT EXISTS audit_log ( @@ -57,9 +57,9 @@ CREATE TABLE IF NOT EXISTS audit_log (
57 user_id varchar(31), 57 user_id varchar(31),
58 user_name varchar(255), 58 user_name varchar(255),
59 action_type varchar(255), 59 action_type varchar(255),
60 - action_data varchar(255), 60 + action_data varchar(1000000),
61 action_status varchar(255), 61 action_status varchar(255),
62 - action_failure_details varchar 62 + action_failure_details varchar(1000000)
63 ); 63 );
64 64
65 CREATE TABLE IF NOT EXISTS attribute_kv ( 65 CREATE TABLE IF NOT EXISTS attribute_kv (
@@ -22,6 +22,7 @@ import com.fasterxml.jackson.databind.node.ObjectNode; @@ -22,6 +22,7 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
22 import com.fasterxml.jackson.databind.node.TextNode; 22 import com.fasterxml.jackson.databind.node.TextNode;
23 import org.junit.runner.RunWith; 23 import org.junit.runner.RunWith;
24 import org.springframework.beans.factory.annotation.Autowired; 24 import org.springframework.beans.factory.annotation.Autowired;
  25 +import org.springframework.context.annotation.Bean;
25 import org.springframework.context.annotation.ComponentScan; 26 import org.springframework.context.annotation.ComponentScan;
26 import org.springframework.context.annotation.Configuration; 27 import org.springframework.context.annotation.Configuration;
27 import org.springframework.test.annotation.DirtiesContext; 28 import org.springframework.test.annotation.DirtiesContext;
@@ -29,6 +30,7 @@ import org.springframework.test.context.ContextConfiguration; @@ -29,6 +30,7 @@ import org.springframework.test.context.ContextConfiguration;
29 import org.springframework.test.context.junit4.SpringRunner; 30 import org.springframework.test.context.junit4.SpringRunner;
30 import org.springframework.test.context.support.AnnotationConfigContextLoader; 31 import org.springframework.test.context.support.AnnotationConfigContextLoader;
31 import org.thingsboard.server.common.data.BaseData; 32 import org.thingsboard.server.common.data.BaseData;
  33 +import org.thingsboard.server.common.data.EntityType;
32 import org.thingsboard.server.common.data.Event; 34 import org.thingsboard.server.common.data.Event;
33 import org.thingsboard.server.common.data.id.EntityId; 35 import org.thingsboard.server.common.data.id.EntityId;
34 import org.thingsboard.server.common.data.id.TenantId; 36 import org.thingsboard.server.common.data.id.TenantId;
@@ -40,6 +42,8 @@ import org.thingsboard.server.common.data.plugin.PluginMetaData; @@ -40,6 +42,8 @@ import org.thingsboard.server.common.data.plugin.PluginMetaData;
40 import org.thingsboard.server.common.data.rule.RuleMetaData; 42 import org.thingsboard.server.common.data.rule.RuleMetaData;
41 import org.thingsboard.server.dao.alarm.AlarmService; 43 import org.thingsboard.server.dao.alarm.AlarmService;
42 import org.thingsboard.server.dao.asset.AssetService; 44 import org.thingsboard.server.dao.asset.AssetService;
  45 +import org.thingsboard.server.dao.audit.AuditLogLevelFilter;
  46 +import org.thingsboard.server.dao.audit.AuditLogLevelMask;
43 import org.thingsboard.server.dao.component.ComponentDescriptorService; 47 import org.thingsboard.server.dao.component.ComponentDescriptorService;
44 import org.thingsboard.server.dao.customer.CustomerService; 48 import org.thingsboard.server.dao.customer.CustomerService;
45 import org.thingsboard.server.dao.dashboard.DashboardService; 49 import org.thingsboard.server.dao.dashboard.DashboardService;
@@ -58,6 +62,8 @@ import org.thingsboard.server.dao.widget.WidgetsBundleService; @@ -58,6 +62,8 @@ import org.thingsboard.server.dao.widget.WidgetsBundleService;
58 62
59 import java.io.IOException; 63 import java.io.IOException;
60 import java.util.Comparator; 64 import java.util.Comparator;
  65 +import java.util.HashMap;
  66 +import java.util.Map;
61 import java.util.UUID; 67 import java.util.UUID;
62 import java.util.concurrent.ThreadLocalRandom; 68 import java.util.concurrent.ThreadLocalRandom;
63 69
@@ -227,4 +233,14 @@ public abstract class AbstractServiceTest { @@ -227,4 +233,14 @@ public abstract class AbstractServiceTest {
227 oNode.set("configuration", readFromResource(configuration)); 233 oNode.set("configuration", readFromResource(configuration));
228 return oNode; 234 return oNode;
229 } 235 }
  236 +
  237 + @Bean
  238 + public AuditLogLevelFilter auditLogLevelFilter() {
  239 + Map<String,String> mask = new HashMap<>();
  240 + for (EntityType entityType : EntityType.values()) {
  241 + mask.put(entityType.name().toLowerCase(), AuditLogLevelMask.RW.name());
  242 + }
  243 + return new AuditLogLevelFilter(mask);
  244 + }
  245 +
230 } 246 }
@@ -5,7 +5,6 @@ zk.zk_dir=/thingsboard @@ -5,7 +5,6 @@ zk.zk_dir=/thingsboard
5 updates.enabled=false 5 updates.enabled=false
6 6
7 audit_log.enabled=true 7 audit_log.enabled=true
8 -audit_log.exceptions.enabled=false  
9 audit_log.by_tenant_partitioning=MONTHS 8 audit_log.by_tenant_partitioning=MONTHS
10 audit_log.default_query_period=30 9 audit_log.default_query_period=30
11 10
@@ -15,10 +15,7 @@ @@ -15,10 +15,7 @@
15 */ 15 */
16 package org.thingsboard.server.extensions.api.plugins; 16 package org.thingsboard.server.extensions.api.plugins;
17 17
18 -import org.thingsboard.server.common.data.id.CustomerId;  
19 -import org.thingsboard.server.common.data.id.EntityId;  
20 -import org.thingsboard.server.common.data.id.PluginId;  
21 -import org.thingsboard.server.common.data.id.TenantId; 18 +import org.thingsboard.server.common.data.id.*;
22 19
23 import java.io.Serializable; 20 import java.io.Serializable;
24 21
@@ -30,13 +27,18 @@ public final class PluginApiCallSecurityContext implements Serializable { @@ -30,13 +27,18 @@ public final class PluginApiCallSecurityContext implements Serializable {
30 private final PluginId pluginId; 27 private final PluginId pluginId;
31 private final TenantId tenantId; 28 private final TenantId tenantId;
32 private final CustomerId customerId; 29 private final CustomerId customerId;
  30 + private final UserId userId;
  31 + private final String userName;
33 32
34 - public PluginApiCallSecurityContext(TenantId pluginTenantId, PluginId pluginId, TenantId tenantId, CustomerId customerId) { 33 + public PluginApiCallSecurityContext(TenantId pluginTenantId, PluginId pluginId, TenantId tenantId, CustomerId customerId,
  34 + UserId userId, String userName) {
35 super(); 35 super();
36 this.pluginTenantId = pluginTenantId; 36 this.pluginTenantId = pluginTenantId;
37 this.pluginId = pluginId; 37 this.pluginId = pluginId;
38 this.tenantId = tenantId; 38 this.tenantId = tenantId;
39 this.customerId = customerId; 39 this.customerId = customerId;
  40 + this.userId = userId;
  41 + this.userName = userName;
40 } 42 }
41 43
42 public TenantId getPluginTenantId(){ 44 public TenantId getPluginTenantId(){
@@ -67,4 +69,12 @@ public final class PluginApiCallSecurityContext implements Serializable { @@ -67,4 +69,12 @@ public final class PluginApiCallSecurityContext implements Serializable {
67 return customerId; 69 return customerId;
68 } 70 }
69 71
  72 + public UserId getUserId() {
  73 + return userId;
  74 + }
  75 +
  76 + public String getUserName() {
  77 + return userName;
  78 + }
  79 +
70 } 80 }
@@ -24,9 +24,7 @@ import org.thingsboard.server.common.data.kv.TsKvQuery; @@ -24,9 +24,7 @@ import org.thingsboard.server.common.data.kv.TsKvQuery;
24 import org.thingsboard.server.common.data.relation.EntityRelation; 24 import org.thingsboard.server.common.data.relation.EntityRelation;
25 import org.thingsboard.server.common.data.relation.RelationTypeGroup; 25 import org.thingsboard.server.common.data.relation.RelationTypeGroup;
26 import org.thingsboard.server.common.msg.cluster.ServerAddress; 26 import org.thingsboard.server.common.msg.cluster.ServerAddress;
27 -import org.thingsboard.server.extensions.api.plugins.msg.PluginToRuleMsg;  
28 -import org.thingsboard.server.extensions.api.plugins.msg.TimeoutMsg;  
29 -import org.thingsboard.server.extensions.api.plugins.msg.ToDeviceRpcRequest; 27 +import org.thingsboard.server.extensions.api.plugins.msg.*;
30 import org.thingsboard.server.extensions.api.plugins.rpc.RpcMsg; 28 import org.thingsboard.server.extensions.api.plugins.rpc.RpcMsg;
31 import org.thingsboard.server.extensions.api.plugins.ws.PluginWebsocketSessionRef; 29 import org.thingsboard.server.extensions.api.plugins.ws.PluginWebsocketSessionRef;
32 import org.thingsboard.server.extensions.api.plugins.ws.msg.PluginWebsocketMsg; 30 import org.thingsboard.server.extensions.api.plugins.ws.msg.PluginWebsocketMsg;
@@ -60,6 +58,7 @@ public interface PluginContext { @@ -60,6 +58,7 @@ public interface PluginContext {
60 58
61 void scheduleTimeoutMsg(TimeoutMsg<?> timeoutMsg); 59 void scheduleTimeoutMsg(TimeoutMsg<?> timeoutMsg);
62 60
  61 + void logRpcRequest(PluginApiCallSecurityContext ctx, DeviceId deviceId, ToDeviceRpcRequestBody body, boolean oneWay, Optional<RpcError> rpcError, Exception e);
63 62
64 /* 63 /*
65 Websocket API 64 Websocket API
@@ -96,6 +95,12 @@ public interface PluginContext { @@ -96,6 +95,12 @@ public interface PluginContext {
96 Attributes API 95 Attributes API
97 */ 96 */
98 97
  98 + void logAttributesUpdated(PluginApiCallSecurityContext ctx, EntityId entityId, String attributeType, List<AttributeKvEntry> attributes, Exception e);
  99 +
  100 + void logAttributesDeleted(PluginApiCallSecurityContext ctx, EntityId entityId, String attributeType, List<String> keys, Exception e);
  101 +
  102 + void logAttributesRead(PluginApiCallSecurityContext ctx, EntityId entityId, String attributeType, List<String> keys, Exception e);
  103 +
99 void saveAttributes(TenantId tenantId, EntityId entityId, String attributeType, List<AttributeKvEntry> attributes, PluginCallback<Void> callback); 104 void saveAttributes(TenantId tenantId, EntityId entityId, String attributeType, List<AttributeKvEntry> attributes, PluginCallback<Void> callback);
100 105
101 void removeAttributes(TenantId tenantId, EntityId entityId, String scope, List<String> attributeKeys, PluginCallback<Void> callback); 106 void removeAttributes(TenantId tenantId, EntityId entityId, String scope, List<String> attributeKeys, PluginCallback<Void> callback);
@@ -18,6 +18,7 @@ package org.thingsboard.server.extensions.api.plugins.msg; @@ -18,6 +18,7 @@ package org.thingsboard.server.extensions.api.plugins.msg;
18 import lombok.Data; 18 import lombok.Data;
19 import org.thingsboard.server.common.data.id.DeviceId; 19 import org.thingsboard.server.common.data.id.DeviceId;
20 import org.thingsboard.server.common.data.id.TenantId; 20 import org.thingsboard.server.common.data.id.TenantId;
  21 +import org.thingsboard.server.extensions.api.plugins.PluginApiCallSecurityContext;
21 22
22 import java.io.Serializable; 23 import java.io.Serializable;
23 import java.util.UUID; 24 import java.util.UUID;
@@ -28,6 +29,7 @@ import java.util.UUID; @@ -28,6 +29,7 @@ import java.util.UUID;
28 @Data 29 @Data
29 public class ToDeviceRpcRequest implements Serializable { 30 public class ToDeviceRpcRequest implements Serializable {
30 private final UUID id; 31 private final UUID id;
  32 + private final PluginApiCallSecurityContext securityCtx;
31 private final TenantId tenantId; 33 private final TenantId tenantId;
32 private final DeviceId deviceId; 34 private final DeviceId deviceId;
33 private final boolean oneway; 35 private final boolean oneway;
@@ -152,7 +152,7 @@ public class DeviceMessagingRuleMsgHandler implements RuleMsgHandler { @@ -152,7 +152,7 @@ public class DeviceMessagingRuleMsgHandler implements RuleMsgHandler {
152 pendingMsgs.put(uid, requestMd); 152 pendingMsgs.put(uid, requestMd);
153 log.trace("[{}] Forwarding {} to [{}]", uid, params, targetDeviceId); 153 log.trace("[{}] Forwarding {} to [{}]", uid, params, targetDeviceId);
154 ToDeviceRpcRequestBody requestBody = new ToDeviceRpcRequestBody(ON_MSG_METHOD_NAME, GSON.toJson(params.get("body"))); 154 ToDeviceRpcRequestBody requestBody = new ToDeviceRpcRequestBody(ON_MSG_METHOD_NAME, GSON.toJson(params.get("body")));
155 - ctx.sendRpcRequest(new ToDeviceRpcRequest(uid, targetDevice.getTenantId(), targetDeviceId, oneWay, System.currentTimeMillis() + timeout, requestBody)); 155 + ctx.sendRpcRequest(new ToDeviceRpcRequest(uid, null, targetDevice.getTenantId(), targetDeviceId, oneWay, System.currentTimeMillis() + timeout, requestBody));
156 } else { 156 } else {
157 replyWithError(ctx, requestMd, RpcError.FORBIDDEN); 157 replyWithError(ctx, requestMd, RpcError.FORBIDDEN);
158 } 158 }
@@ -49,7 +49,7 @@ public class RpcManager { @@ -49,7 +49,7 @@ public class RpcManager {
49 LocalRequestMetaData md = localRpcRequests.remove(requestId); 49 LocalRequestMetaData md = localRpcRequests.remove(requestId);
50 if (md != null) { 50 if (md != null) {
51 log.trace("[{}] Processing local rpc response from device [{}]", requestId, md.getRequest().getDeviceId()); 51 log.trace("[{}] Processing local rpc response from device [{}]", requestId, md.getRequest().getDeviceId());
52 - restHandler.reply(ctx, md.getResponseWriter(), response); 52 + restHandler.reply(ctx, md.getRequest(), md.getResponseWriter(), response);
53 } else { 53 } else {
54 log.trace("[{}] Unknown or stale rpc response received [{}]", requestId, response); 54 log.trace("[{}] Unknown or stale rpc response received [{}]", requestId, response);
55 } 55 }
@@ -62,7 +62,7 @@ public class RpcManager { @@ -62,7 +62,7 @@ public class RpcManager {
62 LocalRequestMetaData md = localRpcRequests.remove(requestId); 62 LocalRequestMetaData md = localRpcRequests.remove(requestId);
63 if (md != null) { 63 if (md != null) {
64 log.trace("[{}] Processing rpc timeout for local device [{}]", requestId, md.getRequest().getDeviceId()); 64 log.trace("[{}] Processing rpc timeout for local device [{}]", requestId, md.getRequest().getDeviceId());
65 - restHandler.reply(ctx, md.getResponseWriter(), timeoutReponse); 65 + restHandler.reply(ctx, md.getRequest(), md.getResponseWriter(), timeoutReponse);
66 } 66 }
67 } 67 }
68 } 68 }
@@ -94,11 +94,12 @@ public class RpcRestMsgHandler extends DefaultRestMsgHandler { @@ -94,11 +94,12 @@ public class RpcRestMsgHandler extends DefaultRestMsgHandler {
94 94
95 private boolean handleDeviceRPCRequest(PluginContext ctx, final PluginRestMsg msg, TenantId tenantId, DeviceId deviceId, RpcRequest cmd, boolean oneWay) throws JsonProcessingException { 95 private boolean handleDeviceRPCRequest(PluginContext ctx, final PluginRestMsg msg, TenantId tenantId, DeviceId deviceId, RpcRequest cmd, boolean oneWay) throws JsonProcessingException {
96 long timeout = System.currentTimeMillis() + (cmd.getTimeout() != null ? cmd.getTimeout() : defaultTimeout); 96 long timeout = System.currentTimeMillis() + (cmd.getTimeout() != null ? cmd.getTimeout() : defaultTimeout);
  97 + ToDeviceRpcRequestBody body = new ToDeviceRpcRequestBody(cmd.getMethodName(), cmd.getRequestData());
97 ctx.checkAccess(deviceId, new PluginCallback<Void>() { 98 ctx.checkAccess(deviceId, new PluginCallback<Void>() {
98 @Override 99 @Override
99 public void onSuccess(PluginContext ctx, Void value) { 100 public void onSuccess(PluginContext ctx, Void value) {
100 - ToDeviceRpcRequestBody body = new ToDeviceRpcRequestBody(cmd.getMethodName(), cmd.getRequestData());  
101 ToDeviceRpcRequest rpcRequest = new ToDeviceRpcRequest(UUID.randomUUID(), 101 ToDeviceRpcRequest rpcRequest = new ToDeviceRpcRequest(UUID.randomUUID(),
  102 + msg.getSecurityCtx(),
102 tenantId, 103 tenantId,
103 deviceId, 104 deviceId,
104 oneWay, 105 oneWay,
@@ -116,15 +117,17 @@ public class RpcRestMsgHandler extends DefaultRestMsgHandler { @@ -116,15 +117,17 @@ public class RpcRestMsgHandler extends DefaultRestMsgHandler {
116 } else { 117 } else {
117 response = new ResponseEntity(HttpStatus.UNAUTHORIZED); 118 response = new ResponseEntity(HttpStatus.UNAUTHORIZED);
118 } 119 }
  120 + ctx.logRpcRequest(msg.getSecurityCtx(), deviceId, body, oneWay, Optional.empty(), e);
119 msg.getResponseHolder().setResult(response); 121 msg.getResponseHolder().setResult(response);
120 } 122 }
121 }); 123 });
122 return true; 124 return true;
123 } 125 }
124 126
125 - public void reply(PluginContext ctx, DeferredResult<ResponseEntity> responseWriter, FromDeviceRpcResponse response) { 127 + public void reply(PluginContext ctx, ToDeviceRpcRequest rpcRequest, DeferredResult<ResponseEntity> responseWriter, FromDeviceRpcResponse response) {
126 Optional<RpcError> rpcError = response.getError(); 128 Optional<RpcError> rpcError = response.getError();
127 if (rpcError.isPresent()) { 129 if (rpcError.isPresent()) {
  130 + ctx.logRpcRequest(rpcRequest.getSecurityCtx(), rpcRequest.getDeviceId(), rpcRequest.getBody(), rpcRequest.isOneway(), rpcError, null);
128 RpcError error = rpcError.get(); 131 RpcError error = rpcError.get();
129 switch (error) { 132 switch (error) {
130 case TIMEOUT: 133 case TIMEOUT:
@@ -142,12 +145,15 @@ public class RpcRestMsgHandler extends DefaultRestMsgHandler { @@ -142,12 +145,15 @@ public class RpcRestMsgHandler extends DefaultRestMsgHandler {
142 if (responseData.isPresent() && !StringUtils.isEmpty(responseData.get())) { 145 if (responseData.isPresent() && !StringUtils.isEmpty(responseData.get())) {
143 String data = responseData.get(); 146 String data = responseData.get();
144 try { 147 try {
  148 + ctx.logRpcRequest(rpcRequest.getSecurityCtx(), rpcRequest.getDeviceId(), rpcRequest.getBody(), rpcRequest.isOneway(), rpcError, null);
145 responseWriter.setResult(new ResponseEntity<>(jsonMapper.readTree(data), HttpStatus.OK)); 149 responseWriter.setResult(new ResponseEntity<>(jsonMapper.readTree(data), HttpStatus.OK));
146 } catch (IOException e) { 150 } catch (IOException e) {
147 log.debug("Failed to decode device response: {}", data, e); 151 log.debug("Failed to decode device response: {}", data, e);
  152 + ctx.logRpcRequest(rpcRequest.getSecurityCtx(), rpcRequest.getDeviceId(), rpcRequest.getBody(), rpcRequest.isOneway(), rpcError, e);
148 responseWriter.setResult(new ResponseEntity<>(HttpStatus.NOT_ACCEPTABLE)); 153 responseWriter.setResult(new ResponseEntity<>(HttpStatus.NOT_ACCEPTABLE));
149 } 154 }
150 } else { 155 } else {
  156 + ctx.logRpcRequest(rpcRequest.getSecurityCtx(), rpcRequest.getDeviceId(), rpcRequest.getBody(), rpcRequest.isOneway(), rpcError, null);
151 responseWriter.setResult(new ResponseEntity<>(HttpStatus.OK)); 157 responseWriter.setResult(new ResponseEntity<>(HttpStatus.OK));
152 } 158 }
153 } 159 }
@@ -77,7 +77,7 @@ public class RpcRuleMsgHandler implements RuleMsgHandler { @@ -77,7 +77,7 @@ public class RpcRuleMsgHandler implements RuleMsgHandler {
77 @Override 77 @Override
78 public void onSuccess(PluginContext ctx, Void value) { 78 public void onSuccess(PluginContext ctx, Void value) {
79 ctx.sendRpcRequest(new ToDeviceRpcRequest(UUID.randomUUID(), 79 ctx.sendRpcRequest(new ToDeviceRpcRequest(UUID.randomUUID(),
80 - tenantId, tmpId, true, expirationTime, body) 80 + null, tenantId, tmpId, true, expirationTime, body)
81 ); 81 );
82 log.trace("[{}] Sent RPC Call Action msg", tmpId); 82 log.trace("[{}] Sent RPC Call Action msg", tmpId);
83 } 83 }
@@ -28,6 +28,7 @@ import org.thingsboard.server.common.data.EntityType; @@ -28,6 +28,7 @@ import org.thingsboard.server.common.data.EntityType;
28 import org.thingsboard.server.common.data.id.DeviceId; 28 import org.thingsboard.server.common.data.id.DeviceId;
29 import org.thingsboard.server.common.data.id.EntityId; 29 import org.thingsboard.server.common.data.id.EntityId;
30 import org.thingsboard.server.common.data.id.EntityIdFactory; 30 import org.thingsboard.server.common.data.id.EntityIdFactory;
  31 +import org.thingsboard.server.common.data.id.UUIDBased;
31 import org.thingsboard.server.common.data.kv.*; 32 import org.thingsboard.server.common.data.kv.*;
32 import org.thingsboard.server.common.msg.core.TelemetryUploadRequest; 33 import org.thingsboard.server.common.msg.core.TelemetryUploadRequest;
33 import org.thingsboard.server.common.transport.adaptor.JsonConverter; 34 import org.thingsboard.server.common.transport.adaptor.JsonConverter;
@@ -150,18 +151,19 @@ public class TelemetryRestMsgHandler extends DefaultRestMsgHandler { @@ -150,18 +151,19 @@ public class TelemetryRestMsgHandler extends DefaultRestMsgHandler {
150 private void handleHttpGetAttributesValues(PluginContext ctx, PluginRestMsg msg, 151 private void handleHttpGetAttributesValues(PluginContext ctx, PluginRestMsg msg,
151 RestRequest request, String scope, EntityId entityId) throws ServletException { 152 RestRequest request, String scope, EntityId entityId) throws ServletException {
152 String keys = request.getParameter("keys", ""); 153 String keys = request.getParameter("keys", "");
153 -  
154 - PluginCallback<List<AttributeKvEntry>> callback = getAttributeValuesPluginCallback(msg); 154 + List<String> keyList = null;
  155 + if (!StringUtils.isEmpty(keys)) {
  156 + keyList = Arrays.asList(keys.split(","));
  157 + }
  158 + PluginCallback<List<AttributeKvEntry>> callback = getAttributeValuesPluginCallback(msg, scope, entityId, keyList);
155 if (!StringUtils.isEmpty(scope)) { 159 if (!StringUtils.isEmpty(scope)) {
156 - if (!StringUtils.isEmpty(keys)) {  
157 - List<String> keyList = Arrays.asList(keys.split(",")); 160 + if (keyList != null && !keyList.isEmpty()) {
158 ctx.loadAttributes(entityId, scope, keyList, callback); 161 ctx.loadAttributes(entityId, scope, keyList, callback);
159 } else { 162 } else {
160 ctx.loadAttributes(entityId, scope, callback); 163 ctx.loadAttributes(entityId, scope, callback);
161 } 164 }
162 } else { 165 } else {
163 - if (!StringUtils.isEmpty(keys)) {  
164 - List<String> keyList = Arrays.asList(keys.split(",")); 166 + if (keyList != null && !keyList.isEmpty()) {
165 ctx.loadAttributes(entityId, Arrays.asList(DataConstants.allScopes()), keyList, callback); 167 ctx.loadAttributes(entityId, Arrays.asList(DataConstants.allScopes()), keyList, callback);
166 } else { 168 } else {
167 ctx.loadAttributes(entityId, Arrays.asList(DataConstants.allScopes()), callback); 169 ctx.loadAttributes(entityId, Arrays.asList(DataConstants.allScopes()), callback);
@@ -230,9 +232,11 @@ public class TelemetryRestMsgHandler extends DefaultRestMsgHandler { @@ -230,9 +232,11 @@ public class TelemetryRestMsgHandler extends DefaultRestMsgHandler {
230 if (attributes.isEmpty()) { 232 if (attributes.isEmpty()) {
231 throw new IllegalArgumentException("No attributes data found in request body!"); 233 throw new IllegalArgumentException("No attributes data found in request body!");
232 } 234 }
  235 +
233 ctx.saveAttributes(ctx.getSecurityCtx().orElseThrow(IllegalArgumentException::new).getTenantId(), entityId, scope, attributes, new PluginCallback<Void>() { 236 ctx.saveAttributes(ctx.getSecurityCtx().orElseThrow(IllegalArgumentException::new).getTenantId(), entityId, scope, attributes, new PluginCallback<Void>() {
234 @Override 237 @Override
235 public void onSuccess(PluginContext ctx, Void value) { 238 public void onSuccess(PluginContext ctx, Void value) {
  239 + ctx.logAttributesUpdated(msg.getSecurityCtx(), entityId, scope, attributes, null);
236 msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.OK)); 240 msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.OK));
237 subscriptionManager.onAttributesUpdateFromServer(ctx, entityId, scope, attributes); 241 subscriptionManager.onAttributesUpdateFromServer(ctx, entityId, scope, attributes);
238 } 242 }
@@ -240,6 +244,7 @@ public class TelemetryRestMsgHandler extends DefaultRestMsgHandler { @@ -240,6 +244,7 @@ public class TelemetryRestMsgHandler extends DefaultRestMsgHandler {
240 @Override 244 @Override
241 public void onFailure(PluginContext ctx, Exception e) { 245 public void onFailure(PluginContext ctx, Exception e) {
242 log.error("Failed to save attributes", e); 246 log.error("Failed to save attributes", e);
  247 + ctx.logAttributesUpdated(msg.getSecurityCtx(), entityId, scope, attributes, e);
243 handleError(e, msg, HttpStatus.BAD_REQUEST); 248 handleError(e, msg, HttpStatus.BAD_REQUEST);
244 } 249 }
245 }); 250 });
@@ -334,15 +339,18 @@ public class TelemetryRestMsgHandler extends DefaultRestMsgHandler { @@ -334,15 +339,18 @@ public class TelemetryRestMsgHandler extends DefaultRestMsgHandler {
334 String keysParam = request.getParameter("keys"); 339 String keysParam = request.getParameter("keys");
335 if (!StringUtils.isEmpty(keysParam)) { 340 if (!StringUtils.isEmpty(keysParam)) {
336 String[] keys = keysParam.split(","); 341 String[] keys = keysParam.split(",");
337 - ctx.removeAttributes(ctx.getSecurityCtx().orElseThrow(IllegalArgumentException::new).getTenantId(), entityId, scope, Arrays.asList(keys), new PluginCallback<Void>() { 342 + List<String> keyList = Arrays.asList(keys);
  343 + ctx.removeAttributes(ctx.getSecurityCtx().orElseThrow(IllegalArgumentException::new).getTenantId(), entityId, scope, keyList, new PluginCallback<Void>() {
338 @Override 344 @Override
339 public void onSuccess(PluginContext ctx, Void value) { 345 public void onSuccess(PluginContext ctx, Void value) {
  346 + ctx.logAttributesDeleted(msg.getSecurityCtx(), entityId, scope, keyList, null);
340 msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.OK)); 347 msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.OK));
341 } 348 }
342 349
343 @Override 350 @Override
344 public void onFailure(PluginContext ctx, Exception e) { 351 public void onFailure(PluginContext ctx, Exception e) {
345 log.error("Failed to remove attributes", e); 352 log.error("Failed to remove attributes", e);
  353 + ctx.logAttributesDeleted(msg.getSecurityCtx(), entityId, scope, keyList, e);
346 handleError(e, msg, HttpStatus.INTERNAL_SERVER_ERROR); 354 handleError(e, msg, HttpStatus.INTERNAL_SERVER_ERROR);
347 } 355 }
348 }); 356 });
@@ -373,18 +381,21 @@ public class TelemetryRestMsgHandler extends DefaultRestMsgHandler { @@ -373,18 +381,21 @@ public class TelemetryRestMsgHandler extends DefaultRestMsgHandler {
373 }; 381 };
374 } 382 }
375 383
376 - private PluginCallback<List<AttributeKvEntry>> getAttributeValuesPluginCallback(final PluginRestMsg msg) { 384 + private PluginCallback<List<AttributeKvEntry>> getAttributeValuesPluginCallback(final PluginRestMsg msg, final String scope,
  385 + final EntityId entityId, final List<String> keyList) {
377 return new PluginCallback<List<AttributeKvEntry>>() { 386 return new PluginCallback<List<AttributeKvEntry>>() {
378 @Override 387 @Override
379 public void onSuccess(PluginContext ctx, List<AttributeKvEntry> attributes) { 388 public void onSuccess(PluginContext ctx, List<AttributeKvEntry> attributes) {
380 List<AttributeData> values = attributes.stream().map(attribute -> new AttributeData(attribute.getLastUpdateTs(), 389 List<AttributeData> values = attributes.stream().map(attribute -> new AttributeData(attribute.getLastUpdateTs(),
381 attribute.getKey(), attribute.getValue())).collect(Collectors.toList()); 390 attribute.getKey(), attribute.getValue())).collect(Collectors.toList());
  391 + ctx.logAttributesRead(msg.getSecurityCtx(), entityId, scope, keyList, null);
382 msg.getResponseHolder().setResult(new ResponseEntity<>(values, HttpStatus.OK)); 392 msg.getResponseHolder().setResult(new ResponseEntity<>(values, HttpStatus.OK));
383 } 393 }
384 394
385 @Override 395 @Override
386 public void onFailure(PluginContext ctx, Exception e) { 396 public void onFailure(PluginContext ctx, Exception e) {
387 log.error("Failed to fetch attributes", e); 397 log.error("Failed to fetch attributes", e);
  398 + ctx.logAttributesRead(msg.getSecurityCtx(), entityId, scope, keyList, e);
388 handleError(e, msg, HttpStatus.INTERNAL_SERVER_ERROR); 399 handleError(e, msg, HttpStatus.INTERNAL_SERVER_ERROR);
389 } 400 }
390 }; 401 };
@@ -29,7 +29,7 @@ @@ -29,7 +29,7 @@
29 </section> 29 </section>
30 <div flex layout="column" class="tb-alarm-container md-whiteframe-z1"> 30 <div flex layout="column" class="tb-alarm-container md-whiteframe-z1">
31 <md-list flex layout="column" class="tb-alarm-table"> 31 <md-list flex layout="column" class="tb-alarm-table">
32 - <md-list class="tb-row tb-header" layout="row" tb-alarm-header> 32 + <md-list class="tb-row tb-header" layout="row" layout-align="start center" tb-alarm-header>
33 </md-list> 33 </md-list>
34 <md-progress-linear style="max-height: 0px;" md-mode="indeterminate" ng-disabled="!$root.loading" 34 <md-progress-linear style="max-height: 0px;" md-mode="indeterminate" ng-disabled="!$root.loading"
35 ng-show="$root.loading"></md-progress-linear> 35 ng-show="$root.loading"></md-progress-linear>
@@ -39,7 +39,7 @@ @@ -39,7 +39,7 @@
39 class="tb-prompt" ng-show="noData()">alarm.no-alarms-prompt</span> 39 class="tb-prompt" ng-show="noData()">alarm.no-alarms-prompt</span>
40 <md-virtual-repeat-container ng-show="hasData()" flex md-top-index="topIndex" tb-scope-element="repeatContainer"> 40 <md-virtual-repeat-container ng-show="hasData()" flex md-top-index="topIndex" tb-scope-element="repeatContainer">
41 <md-list-item md-virtual-repeat="alarm in theAlarms" md-on-demand flex ng-style="hasScroll() ? {'margin-right':'-15px'} : {}"> 41 <md-list-item md-virtual-repeat="alarm in theAlarms" md-on-demand flex ng-style="hasScroll() ? {'margin-right':'-15px'} : {}">
42 - <md-list class="tb-row" flex layout="row" tb-alarm-row alarm="{{alarm}}"> 42 + <md-list class="tb-row" flex layout="row" layout-align="start center" tb-alarm-row alarm="{{alarm}}">
43 </md-list> 43 </md-list>
44 <md-divider flex></md-divider> 44 <md-divider flex></md-divider>
45 </md-list-item> 45 </md-list-item>
  1 +/*
  2 + * Copyright © 2016-2017 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +export default angular.module('thingsboard.api.auditLog', [])
  17 + .factory('auditLogService', AuditLogService)
  18 + .name;
  19 +
  20 +/*@ngInject*/
  21 +function AuditLogService($http, $q) {
  22 +
  23 + var service = {
  24 + getAuditLogsByEntityId: getAuditLogsByEntityId,
  25 + getAuditLogsByUserId: getAuditLogsByUserId,
  26 + getAuditLogsByCustomerId: getAuditLogsByCustomerId,
  27 + getAuditLogs: getAuditLogs
  28 + }
  29 +
  30 + return service;
  31 +
  32 + function getAuditLogsByEntityId (entityType, entityId, pageLink) {
  33 + var deferred = $q.defer();
  34 + var url = `/api/audit/logs/entity/${entityType}/${entityId}?limit=${pageLink.limit}`;
  35 +
  36 + if (angular.isDefined(pageLink.startTime) && pageLink.startTime != null) {
  37 + url += '&startTime=' + pageLink.startTime;
  38 + }
  39 + if (angular.isDefined(pageLink.endTime) && pageLink.endTime != null) {
  40 + url += '&endTime=' + pageLink.endTime;
  41 + }
  42 + if (angular.isDefined(pageLink.idOffset) && pageLink.idOffset != null) {
  43 + url += '&offset=' + pageLink.idOffset;
  44 + }
  45 + $http.get(url, null).then(function success(response) {
  46 + deferred.resolve(response.data);
  47 + }, function fail() {
  48 + deferred.reject();
  49 + });
  50 + return deferred.promise;
  51 + }
  52 +
  53 + function getAuditLogsByUserId (userId, pageLink) {
  54 + var deferred = $q.defer();
  55 + var url = `/api/audit/logs/user/${userId}?limit=${pageLink.limit}`;
  56 +
  57 + if (angular.isDefined(pageLink.startTime) && pageLink.startTime != null) {
  58 + url += '&startTime=' + pageLink.startTime;
  59 + }
  60 + if (angular.isDefined(pageLink.endTime) && pageLink.endTime != null) {
  61 + url += '&endTime=' + pageLink.endTime;
  62 + }
  63 + if (angular.isDefined(pageLink.idOffset) && pageLink.idOffset != null) {
  64 + url += '&offset=' + pageLink.idOffset;
  65 + }
  66 + $http.get(url, null).then(function success(response) {
  67 + deferred.resolve(response.data);
  68 + }, function fail() {
  69 + deferred.reject();
  70 + });
  71 + return deferred.promise;
  72 + }
  73 +
  74 + function getAuditLogsByCustomerId (customerId, pageLink) {
  75 + var deferred = $q.defer();
  76 + var url = `/api/audit/logs/customer/${customerId}?limit=${pageLink.limit}`;
  77 +
  78 + if (angular.isDefined(pageLink.startTime) && pageLink.startTime != null) {
  79 + url += '&startTime=' + pageLink.startTime;
  80 + }
  81 + if (angular.isDefined(pageLink.endTime) && pageLink.endTime != null) {
  82 + url += '&endTime=' + pageLink.endTime;
  83 + }
  84 + if (angular.isDefined(pageLink.idOffset) && pageLink.idOffset != null) {
  85 + url += '&offset=' + pageLink.idOffset;
  86 + }
  87 + $http.get(url, null).then(function success(response) {
  88 + deferred.resolve(response.data);
  89 + }, function fail() {
  90 + deferred.reject();
  91 + });
  92 + return deferred.promise;
  93 + }
  94 +
  95 + function getAuditLogs (pageLink) {
  96 + var deferred = $q.defer();
  97 + var url = `/api/audit/logs?limit=${pageLink.limit}`;
  98 +
  99 + if (angular.isDefined(pageLink.startTime) && pageLink.startTime != null) {
  100 + url += '&startTime=' + pageLink.startTime;
  101 + }
  102 + if (angular.isDefined(pageLink.endTime) && pageLink.endTime != null) {
  103 + url += '&endTime=' + pageLink.endTime;
  104 + }
  105 + if (angular.isDefined(pageLink.idOffset) && pageLink.idOffset != null) {
  106 + url += '&offset=' + pageLink.idOffset;
  107 + }
  108 + $http.get(url, null).then(function success(response) {
  109 + deferred.resolve(response.data);
  110 + }, function fail() {
  111 + deferred.reject();
  112 + });
  113 + return deferred.promise;
  114 + }
  115 +
  116 +}
@@ -20,7 +20,7 @@ export default angular.module('thingsboard.api.device', [thingsboardTypes]) @@ -20,7 +20,7 @@ export default angular.module('thingsboard.api.device', [thingsboardTypes])
20 .name; 20 .name;
21 21
22 /*@ngInject*/ 22 /*@ngInject*/
23 -function DeviceService($http, $q, attributeService, customerService, types) { 23 +function DeviceService($http, $q, $window, userService, attributeService, customerService, types) {
24 24
25 var service = { 25 var service = {
26 assignDeviceToCustomer: assignDeviceToCustomer, 26 assignDeviceToCustomer: assignDeviceToCustomer,
@@ -181,14 +181,27 @@ function DeviceService($http, $q, attributeService, customerService, types) { @@ -181,14 +181,27 @@ function DeviceService($http, $q, attributeService, customerService, types) {
181 return deferred.promise; 181 return deferred.promise;
182 } 182 }
183 183
184 - function getDeviceCredentials(deviceId) { 184 + function getDeviceCredentials(deviceId, sync) {
185 var deferred = $q.defer(); 185 var deferred = $q.defer();
186 var url = '/api/device/' + deviceId + '/credentials'; 186 var url = '/api/device/' + deviceId + '/credentials';
187 - $http.get(url, null).then(function success(response) {  
188 - deferred.resolve(response.data);  
189 - }, function fail() {  
190 - deferred.reject();  
191 - }); 187 + if (sync) {
  188 + var request = new $window.XMLHttpRequest();
  189 + request.open('GET', url, false);
  190 + request.setRequestHeader("Accept", "application/json, text/plain, */*");
  191 + userService.setAuthorizationRequestHeader(request);
  192 + request.send(null);
  193 + if (request.status === 200) {
  194 + deferred.resolve(angular.fromJson(request.responseText));
  195 + } else {
  196 + deferred.reject();
  197 + }
  198 + } else {
  199 + $http.get(url, null).then(function success(response) {
  200 + deferred.resolve(response.data);
  201 + }, function fail() {
  202 + deferred.reject();
  203 + });
  204 + }
192 return deferred.promise; 205 return deferred.promise;
193 } 206 }
194 207
@@ -54,6 +54,7 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi @@ -54,6 +54,7 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi
54 refreshJwtToken: refreshJwtToken, 54 refreshJwtToken: refreshJwtToken,
55 refreshTokenPending: refreshTokenPending, 55 refreshTokenPending: refreshTokenPending,
56 updateAuthorizationHeader: updateAuthorizationHeader, 56 updateAuthorizationHeader: updateAuthorizationHeader,
  57 + setAuthorizationRequestHeader: setAuthorizationRequestHeader,
57 gotoDefaultPlace: gotoDefaultPlace, 58 gotoDefaultPlace: gotoDefaultPlace,
58 forceDefaultPlace: forceDefaultPlace, 59 forceDefaultPlace: forceDefaultPlace,
59 updateLastPublicDashboardId: updateLastPublicDashboardId, 60 updateLastPublicDashboardId: updateLastPublicDashboardId,
@@ -367,6 +368,14 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi @@ -367,6 +368,14 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi
367 return jwtToken; 368 return jwtToken;
368 } 369 }
369 370
  371 + function setAuthorizationRequestHeader(request) {
  372 + var jwtToken = store.get('jwt_token');
  373 + if (jwtToken) {
  374 + request.setRequestHeader('X-Authorization', 'Bearer ' + jwtToken);
  375 + }
  376 + return jwtToken;
  377 + }
  378 +
370 function getTenantAdmins(tenantId, pageLink) { 379 function getTenantAdmins(tenantId, pageLink) {
371 var deferred = $q.defer(); 380 var deferred = $q.defer();
372 var url = '/api/tenant/' + tenantId + '/users?limit=' + pageLink.limit; 381 var url = '/api/tenant/' + tenantId + '/users?limit=' + pageLink.limit;
@@ -63,6 +63,7 @@ import thingsboardApiTime from './api/time.service'; @@ -63,6 +63,7 @@ import thingsboardApiTime from './api/time.service';
63 import thingsboardKeyboardShortcut from './components/keyboard-shortcut.filter'; 63 import thingsboardKeyboardShortcut from './components/keyboard-shortcut.filter';
64 import thingsboardHelp from './help/help.directive'; 64 import thingsboardHelp from './help/help.directive';
65 import thingsboardToast from './services/toast'; 65 import thingsboardToast from './services/toast';
  66 +import thingsboardClipboard from './services/clipboard.service';
66 import thingsboardHome from './layout'; 67 import thingsboardHome from './layout';
67 import thingsboardApiLogin from './api/login.service'; 68 import thingsboardApiLogin from './api/login.service';
68 import thingsboardApiDevice from './api/device.service'; 69 import thingsboardApiDevice from './api/device.service';
@@ -72,6 +73,7 @@ import thingsboardApiAsset from './api/asset.service'; @@ -72,6 +73,7 @@ import thingsboardApiAsset from './api/asset.service';
72 import thingsboardApiAttribute from './api/attribute.service'; 73 import thingsboardApiAttribute from './api/attribute.service';
73 import thingsboardApiEntity from './api/entity.service'; 74 import thingsboardApiEntity from './api/entity.service';
74 import thingsboardApiAlarm from './api/alarm.service'; 75 import thingsboardApiAlarm from './api/alarm.service';
  76 +import thingsboardApiAuditLog from './api/audit-log.service';
75 77
76 import 'typeface-roboto'; 78 import 'typeface-roboto';
77 import 'font-awesome/css/font-awesome.min.css'; 79 import 'font-awesome/css/font-awesome.min.css';
@@ -123,6 +125,7 @@ angular.module('thingsboard', [ @@ -123,6 +125,7 @@ angular.module('thingsboard', [
123 thingsboardKeyboardShortcut, 125 thingsboardKeyboardShortcut,
124 thingsboardHelp, 126 thingsboardHelp,
125 thingsboardToast, 127 thingsboardToast,
  128 + thingsboardClipboard,
126 thingsboardHome, 129 thingsboardHome,
127 thingsboardApiLogin, 130 thingsboardApiLogin,
128 thingsboardApiDevice, 131 thingsboardApiDevice,
@@ -132,6 +135,7 @@ angular.module('thingsboard', [ @@ -132,6 +135,7 @@ angular.module('thingsboard', [
132 thingsboardApiAttribute, 135 thingsboardApiAttribute,
133 thingsboardApiEntity, 136 thingsboardApiEntity,
134 thingsboardApiAlarm, 137 thingsboardApiAlarm,
  138 + thingsboardApiAuditLog,
135 uiRouter]) 139 uiRouter])
136 .config(AppConfig) 140 .config(AppConfig)
137 .factory('globalInterceptor', GlobalInterceptor) 141 .factory('globalInterceptor', GlobalInterceptor)
@@ -66,4 +66,10 @@ @@ -66,4 +66,10 @@
66 entity-type="{{vm.types.entityType.asset}}"> 66 entity-type="{{vm.types.entityType.asset}}">
67 </tb-relation-table> 67 </tb-relation-table>
68 </md-tab> 68 </md-tab>
  69 + <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.grid.isTenantAdmin()" md-on-select="vm.grid.triggerResize()" label="{{ 'audit-log.audit-logs' | translate }}">
  70 + <tb-audit-log-table flex entity-type="vm.types.entityType.asset"
  71 + entity-id="vm.grid.operatingItem().id.id"
  72 + audit-log-mode="{{vm.types.auditLogMode.entity}}">
  73 + </tb-audit-log-table>
  74 + </md-tab>
69 </tb-grid> 75 </tb-grid>
  1 +/*
  2 + * Copyright © 2016-2017 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +import $ from 'jquery';
  17 +import 'brace/ext/language_tools';
  18 +import 'brace/mode/java';
  19 +import 'brace/theme/github';
  20 +
  21 +/* eslint-disable angular/angularelement */
  22 +
  23 +import './audit-log-details-dialog.scss';
  24 +
  25 +/*@ngInject*/
  26 +export default function AuditLogDetailsDialogController($mdDialog, types, auditLog, showingCallback) {
  27 +
  28 + var vm = this;
  29 +
  30 + showingCallback.onShowing = function(scope, element) {
  31 + updateEditorSize(element, vm.actionData, 'tb-audit-log-action-data');
  32 + vm.actionDataEditor.resize();
  33 + if (vm.displayFailureDetails) {
  34 + updateEditorSize(element, vm.actionFailureDetails, 'tb-audit-log-failure-details');
  35 + vm.failureDetailsEditor.resize();
  36 + }
  37 + };
  38 +
  39 + vm.types = types;
  40 + vm.auditLog = auditLog;
  41 + vm.displayFailureDetails = auditLog.actionStatus == types.auditLogActionStatus.FAILURE.value;
  42 + vm.actionData = auditLog.actionDataText;
  43 + vm.actionFailureDetails = auditLog.actionFailureDetails;
  44 +
  45 + vm.actionDataContentOptions = {
  46 + useWrapMode: false,
  47 + mode: 'java',
  48 + showGutter: false,
  49 + showPrintMargin: false,
  50 + theme: 'github',
  51 + advanced: {
  52 + enableSnippets: false,
  53 + enableBasicAutocompletion: false,
  54 + enableLiveAutocompletion: false
  55 + },
  56 + onLoad: function (_ace) {
  57 + vm.actionDataEditor = _ace;
  58 + }
  59 + };
  60 +
  61 + vm.failureDetailsContentOptions = {
  62 + useWrapMode: false,
  63 + mode: 'java',
  64 + showGutter: false,
  65 + showPrintMargin: false,
  66 + theme: 'github',
  67 + advanced: {
  68 + enableSnippets: false,
  69 + enableBasicAutocompletion: false,
  70 + enableLiveAutocompletion: false
  71 + },
  72 + onLoad: function (_ace) {
  73 + vm.failureDetailsEditor = _ace;
  74 + }
  75 + };
  76 +
  77 + function updateEditorSize(element, content, editorId) {
  78 + var newHeight = 200;
  79 + var newWidth = 600;
  80 + if (content && content.length > 0) {
  81 + var lines = content.split('\n');
  82 + newHeight = 16 * lines.length + 16;
  83 + var maxLineLength = 0;
  84 + for (var i in lines) {
  85 + var line = lines[i].replace(/\t/g, ' ').replace(/\n/g, '');
  86 + var lineLength = line.length;
  87 + maxLineLength = Math.max(maxLineLength, lineLength);
  88 + }
  89 + newWidth = 8 * maxLineLength + 16;
  90 + }
  91 + $('#'+editorId, element).height(newHeight.toString() + "px").css('min-height', newHeight.toString() + "px")
  92 + .width(newWidth.toString() + "px");
  93 + }
  94 +
  95 + vm.close = close;
  96 +
  97 + function close () {
  98 + $mdDialog.hide();
  99 + }
  100 +
  101 +}
  102 +
  103 +/* eslint-enable angular/angularelement */
  1 +/**
  2 + * Copyright © 2016-2017 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +#tb-audit-log-action-data, #tb-audit-log-failure-details {
  18 + min-width: 400px;
  19 + min-height: 50px;
  20 + width: 100%;
  21 + height: 100%;
  22 + border: 1px solid #C0C0C0;
  23 +}
  1 +<!--
  2 +
  3 + Copyright © 2016-2017 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<md-dialog aria-label="{{ 'audit-log.audit-log-details' | translate }}">
  19 + <md-toolbar>
  20 + <div class="md-toolbar-tools">
  21 + <h2 translate>audit-log.audit-log-details</h2>
  22 + <span flex></span>
  23 + <md-button class="md-icon-button" ng-click="vm.close()">
  24 + <ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
  25 + </md-button>
  26 + </div>
  27 + </md-toolbar>
  28 + <md-dialog-content>
  29 + <div class="md-dialog-content" layout="column">
  30 + <label translate class="tb-title no-padding">audit-log.action-data</label>
  31 + <div flex id="tb-audit-log-action-data" readonly
  32 + ui-ace="vm.actionDataContentOptions"
  33 + ng-model="vm.actionData">
  34 + </div>
  35 + <span style="height: 30px;"></span>
  36 + <label ng-show="vm.displayFailureDetails" translate class="tb-title no-padding">audit-log.failure-details</label>
  37 + <div ng-show="vm.displayFailureDetails" flex id="tb-audit-log-failure-details" readonly
  38 + ui-ace="vm.failureDetailsContentOptions"
  39 + ng-model="vm.actionFailureDetails">
  40 + </div>
  41 + </div>
  42 + </md-dialog-content>
  43 + <md-dialog-actions layout="row">
  44 + <span flex></span>
  45 + <md-button ng-disabled="$root.loading" ng-click="vm.close()" style="margin-right:20px;">{{ 'action.close' |
  46 + translate }}
  47 + </md-button>
  48 + </md-dialog-actions>
  49 +</md-dialog>
  1 +/*
  2 + * Copyright © 2016-2017 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +/* eslint-disable import/no-unresolved, import/default */
  17 +
  18 +import auditLogHeaderTemplate from './audit-log-header.tpl.html';
  19 +
  20 +/* eslint-enable import/no-unresolved, import/default */
  21 +
  22 +/*@ngInject*/
  23 +export default function AuditLogHeaderDirective($compile, $templateCache, types) {
  24 +
  25 + var linker = function (scope, element, attrs) {
  26 +
  27 + var template = $templateCache.get(auditLogHeaderTemplate);
  28 + element.html(template);
  29 + scope.auditLogMode = attrs.auditLogMode;
  30 + scope.types = types;
  31 + $compile(element.contents())(scope);
  32 +
  33 + };
  34 +
  35 + return {
  36 + restrict: "A",
  37 + replace: false,
  38 + link: linker,
  39 + scope: false
  40 + };
  41 +}
  1 +<!--
  2 +
  3 + Copyright © 2016-2017 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<div translate class="tb-cell" flex="30">audit-log.timestamp</div>
  19 +<div ng-if="auditLogMode != types.auditLogMode.entity" translate class="tb-cell" flex="10">audit-log.entity-type</div>
  20 +<div ng-if="auditLogMode != types.auditLogMode.entity" translate class="tb-cell" flex="30">audit-log.entity-name</div>
  21 +<div ng-if="auditLogMode != types.auditLogMode.user" translate class="tb-cell" flex="30">audit-log.user</div>
  22 +<div translate class="tb-cell" flex="15">audit-log.type</div>
  23 +<div translate class="tb-cell" flex="15">audit-log.status</div>
  24 +<div translate class="tb-cell" flex="10">audit-log.details</div>
  1 +/*
  2 + * Copyright © 2016-2017 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +/* eslint-disable import/no-unresolved, import/default */
  17 +
  18 +import auditLogDetailsDialogTemplate from './audit-log-details-dialog.tpl.html';
  19 +
  20 +import auditLogRowTemplate from './audit-log-row.tpl.html';
  21 +
  22 +/* eslint-enable import/no-unresolved, import/default */
  23 +
  24 +/*@ngInject*/
  25 +export default function AuditLogRowDirective($compile, $templateCache, types, $mdDialog, $document) {
  26 +
  27 + var linker = function (scope, element, attrs) {
  28 +
  29 + var template = $templateCache.get(auditLogRowTemplate);
  30 + element.html(template);
  31 +
  32 + scope.auditLog = attrs.auditLog;
  33 + scope.auditLogMode = attrs.auditLogMode;
  34 + scope.types = types;
  35 +
  36 + scope.showAuditLogDetails = function($event) {
  37 + var onShowingCallback = {
  38 + onShowing: function(){}
  39 + }
  40 + $mdDialog.show({
  41 + controller: 'AuditLogDetailsDialogController',
  42 + controllerAs: 'vm',
  43 + templateUrl: auditLogDetailsDialogTemplate,
  44 + locals: {
  45 + auditLog: scope.auditLog,
  46 + showingCallback: onShowingCallback
  47 + },
  48 + parent: angular.element($document[0].body),
  49 + targetEvent: $event,
  50 + fullscreen: true,
  51 + skipHide: true,
  52 + onShowing: function(scope, element) {
  53 + onShowingCallback.onShowing(scope, element);
  54 + }
  55 + });
  56 + }
  57 +
  58 + $compile(element.contents())(scope);
  59 + }
  60 +
  61 + return {
  62 + restrict: "A",
  63 + replace: false,
  64 + link: linker,
  65 + scope: false
  66 + };
  67 +}
  1 +<!--
  2 +
  3 + Copyright © 2016-2017 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<div class="tb-cell" flex="30">{{ auditLog.createdTime | date : 'yyyy-MM-dd HH:mm:ss' }}</div>
  19 +<div ng-if="auditLogMode != types.auditLogMode.entity" class="tb-cell" flex="10">{{ auditLog.entityTypeText }}</div>
  20 +<div ng-if="auditLogMode != types.auditLogMode.entity" class="tb-cell" flex="30">{{ auditLog.entityName }}</div>
  21 +<div ng-if="auditLogMode != types.auditLogMode.user" class="tb-cell" flex="30">{{ auditLog.userName }}</div>
  22 +<div class="tb-cell" flex="15">{{ auditLog.actionTypeText }}</div>
  23 +<div class="tb-cell" flex="15">{{ auditLog.actionStatusText }}</div>
  24 +<div class="tb-cell" flex="10">
  25 + <md-button class="md-icon-button md-primary"
  26 + ng-click="showAuditLogDetails($event)"
  27 + aria-label="{{ 'action.view' | translate }}">
  28 + <md-tooltip md-direction="top">
  29 + {{ 'audit-log.details' | translate }}
  30 + </md-tooltip>
  31 + <md-icon aria-label="{{ 'action.view' | translate }}"
  32 + class="material-icons">
  33 + more_horiz
  34 + </md-icon>
  35 + </md-button>
  36 +</div>
  1 +/*
  2 + * Copyright © 2016-2017 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +import './audit-log.scss';
  17 +
  18 +/* eslint-disable import/no-unresolved, import/default */
  19 +
  20 +import auditLogTableTemplate from './audit-log-table.tpl.html';
  21 +
  22 +/* eslint-enable import/no-unresolved, import/default */
  23 +
  24 +/*@ngInject*/
  25 +export default function AuditLogTableDirective($compile, $templateCache, $rootScope, $filter, $translate, types, auditLogService) {
  26 +
  27 + var linker = function (scope, element) {
  28 +
  29 + var template = $templateCache.get(auditLogTableTemplate);
  30 +
  31 + element.html(template);
  32 +
  33 + scope.types = types;
  34 +
  35 + var pageSize = 20;
  36 + var startTime = 0;
  37 + var endTime = 0;
  38 +
  39 + scope.timewindow = {
  40 + history: {
  41 + timewindowMs: 24 * 60 * 60 * 1000 // 1 day
  42 + }
  43 + }
  44 +
  45 + scope.topIndex = 0;
  46 + scope.searchText = '';
  47 +
  48 + scope.theAuditLogs = {
  49 + getItemAtIndex: function (index) {
  50 + if (index > scope.auditLogs.filtered.length) {
  51 + scope.theAuditLogs.fetchMoreItems_(index);
  52 + return null;
  53 + }
  54 + return scope.auditLogs.filtered[index];
  55 + },
  56 +
  57 + getLength: function () {
  58 + if (scope.auditLogs.hasNext) {
  59 + return scope.auditLogs.filtered.length + scope.auditLogs.nextPageLink.limit;
  60 + } else {
  61 + return scope.auditLogs.filtered.length;
  62 + }
  63 + },
  64 +
  65 + fetchMoreItems_: function () {
  66 + if (scope.auditLogs.hasNext && !scope.auditLogs.pending) {
  67 + var promise = getAuditLogsPromise(scope.auditLogs.nextPageLink);
  68 + if (promise) {
  69 + scope.auditLogs.pending = true;
  70 + promise.then(
  71 + function success(auditLogs) {
  72 + scope.auditLogs.data = scope.auditLogs.data.concat(prepareAuditLogsData(auditLogs.data));
  73 + scope.auditLogs.filtered = $filter('filter')(scope.auditLogs.data, {$: scope.searchText});
  74 + scope.auditLogs.nextPageLink = auditLogs.nextPageLink;
  75 + scope.auditLogs.hasNext = auditLogs.hasNext;
  76 + if (scope.auditLogs.hasNext) {
  77 + scope.auditLogs.nextPageLink.limit = pageSize;
  78 + }
  79 + scope.auditLogs.pending = false;
  80 + },
  81 + function fail() {
  82 + scope.auditLogs.hasNext = false;
  83 + scope.auditLogs.pending = false;
  84 + });
  85 + } else {
  86 + scope.auditLogs.hasNext = false;
  87 + }
  88 + }
  89 + }
  90 + };
  91 +
  92 + function prepareAuditLogsData(data) {
  93 + data.forEach(
  94 + auditLog => {
  95 + auditLog.entityTypeText = $translate.instant(types.entityTypeTranslations[auditLog.entityId.entityType].type);
  96 + auditLog.actionTypeText = $translate.instant(types.auditLogActionType[auditLog.actionType].name);
  97 + auditLog.actionStatusText = $translate.instant(types.auditLogActionStatus[auditLog.actionStatus].name);
  98 + auditLog.actionDataText = auditLog.actionData ? angular.toJson(auditLog.actionData, true) : '';
  99 + }
  100 + );
  101 + return data;
  102 + }
  103 +
  104 + scope.$watch("entityId", function(newVal, prevVal) {
  105 + if (newVal && !angular.equals(newVal, prevVal)) {
  106 + resetFilter();
  107 + scope.reload();
  108 + }
  109 + });
  110 +
  111 + scope.$watch("userId", function(newVal, prevVal) {
  112 + if (newVal && !angular.equals(newVal, prevVal)) {
  113 + resetFilter();
  114 + scope.reload();
  115 + }
  116 + });
  117 +
  118 + scope.$watch("customerId", function(newVal, prevVal) {
  119 + if (newVal && !angular.equals(newVal, prevVal)) {
  120 + resetFilter();
  121 + scope.reload();
  122 + }
  123 + });
  124 +
  125 + function getAuditLogsPromise(pageLink) {
  126 + switch(scope.auditLogMode) {
  127 + case types.auditLogMode.tenant:
  128 + return auditLogService.getAuditLogs(pageLink);
  129 + case types.auditLogMode.entity:
  130 + if (scope.entityType && scope.entityId) {
  131 + return auditLogService.getAuditLogsByEntityId(scope.entityType, scope.entityId,
  132 + pageLink);
  133 + } else {
  134 + return null;
  135 + }
  136 + case types.auditLogMode.user:
  137 + if (scope.userId) {
  138 + return auditLogService.getAuditLogsByUserId(scope.userId, pageLink);
  139 + } else {
  140 + return null;
  141 + }
  142 + case types.auditLogMode.customer:
  143 + if (scope.customerId) {
  144 + return auditLogService.getAuditLogsByCustomerId(scope.customerId, pageLink);
  145 + } else {
  146 + return null;
  147 + }
  148 + }
  149 + }
  150 +
  151 + function destroyWatchers() {
  152 + if (scope.timewindowWatchHandle) {
  153 + scope.timewindowWatchHandle();
  154 + scope.timewindowWatchHandle = null;
  155 + }
  156 + if (scope.searchTextWatchHandle) {
  157 + scope.searchTextWatchHandle();
  158 + scope.searchTextWatchHandle = null;
  159 + }
  160 + }
  161 +
  162 + function initWatchers() {
  163 + scope.timewindowWatchHandle = scope.$watch("timewindow", function(newVal, prevVal) {
  164 + if (newVal && !angular.equals(newVal, prevVal)) {
  165 + scope.reload();
  166 + }
  167 + }, true);
  168 +
  169 + scope.searchTextWatchHandle = scope.$watch("searchText", function(newVal, prevVal) {
  170 + if (!angular.equals(newVal, prevVal)) {
  171 + scope.searchTextUpdated();
  172 + }
  173 + }, true);
  174 + }
  175 +
  176 + function resetFilter() {
  177 + destroyWatchers();
  178 + scope.timewindow = {
  179 + history: {
  180 + timewindowMs: 24 * 60 * 60 * 1000 // 1 day
  181 + }
  182 + };
  183 + scope.searchText = '';
  184 + initWatchers();
  185 + }
  186 +
  187 + function updateTimeWindowRange () {
  188 + if (scope.timewindow.history.timewindowMs) {
  189 + var currentTime = (new Date).getTime();
  190 + startTime = currentTime - scope.timewindow.history.timewindowMs;
  191 + endTime = currentTime;
  192 + } else {
  193 + startTime = scope.timewindow.history.fixedTimewindow.startTimeMs;
  194 + endTime = scope.timewindow.history.fixedTimewindow.endTimeMs;
  195 + }
  196 + }
  197 +
  198 + scope.reload = function() {
  199 + scope.topIndex = 0;
  200 + updateTimeWindowRange();
  201 + scope.auditLogs = {
  202 + data: [],
  203 + filtered: [],
  204 + nextPageLink: {
  205 + limit: pageSize,
  206 + startTime: startTime,
  207 + endTime: endTime
  208 + },
  209 + hasNext: true,
  210 + pending: false
  211 + };
  212 + scope.theAuditLogs.getItemAtIndex(pageSize);
  213 + }
  214 +
  215 + scope.searchTextUpdated = function() {
  216 + scope.auditLogs.filtered = $filter('filter')(scope.auditLogs.data, {$: scope.searchText});
  217 + scope.theAuditLogs.getItemAtIndex(pageSize);
  218 + }
  219 +
  220 + scope.noData = function() {
  221 + return scope.auditLogs.data.length == 0 && !scope.auditLogs.hasNext;
  222 + }
  223 +
  224 + scope.hasData = function() {
  225 + return scope.auditLogs.data.length > 0;
  226 + }
  227 +
  228 + scope.loading = function() {
  229 + return $rootScope.loading;
  230 + }
  231 +
  232 + scope.hasScroll = function() {
  233 + var repeatContainer = scope.repeatContainer[0];
  234 + if (repeatContainer) {
  235 + var scrollElement = repeatContainer.children[0];
  236 + if (scrollElement) {
  237 + return scrollElement.scrollHeight > scrollElement.clientHeight;
  238 + }
  239 + }
  240 + return false;
  241 + }
  242 +
  243 + scope.reload();
  244 +
  245 + initWatchers();
  246 +
  247 + $compile(element.contents())(scope);
  248 + }
  249 +
  250 + return {
  251 + restrict: "E",
  252 + link: linker,
  253 + scope: {
  254 + entityType: '=?',
  255 + entityId: '=?',
  256 + userId: '=?',
  257 + customerId: '=?',
  258 + auditLogMode: '@',
  259 + pageMode: '@?'
  260 + }
  261 + };
  262 +}
  1 +<!--
  2 +
  3 + Copyright © 2016-2017 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<md-content flex class="md-padding tb-absolute-fill" layout="column">
  19 + <div flex layout="column" class="tb-audit-logs" ng-class="{'md-whiteframe-z1': pageMode}">
  20 + <div layout="column" layout-gt-sm="row" layout-align-gt-sm="start center" class="tb-audit-log-toolbar" ng-class="{'md-padding': pageMode, 'tb-audit-log-margin-18px': !pageMode}">
  21 + <tb-timewindow ng-model="timewindow" history-only as-button="true"></tb-timewindow>
  22 + <div flex layout="row" layout-align="start center">
  23 + <md-button class="md-icon-button" aria-label="{{ 'action.search' | translate }}">
  24 + <md-icon aria-label="{{ 'action.search' | translate }}" class="material-icons">search</md-icon>
  25 + <md-tooltip md-direction="top">
  26 + {{'audit-log.search' | translate}}
  27 + </md-tooltip>
  28 + </md-button>
  29 + <md-input-container flex class="tb-audit-log-search-input">
  30 + <label>&nbsp;</label>
  31 + <input ng-model="searchText" placeholder="{{'audit-log.search' | translate}}"/>
  32 + </md-input-container>
  33 + <md-button ng-disabled="$root.loading" class="md-icon-button" aria-label="Close" ng-click="searchText = ''">
  34 + <md-icon aria-label="Close" class="material-icons">close</md-icon>
  35 + <md-tooltip md-direction="top">
  36 + {{ 'audit-log.clear-search' | translate }}
  37 + </md-tooltip>
  38 + </md-button>
  39 + <md-button ng-disabled="$root.loading"
  40 + class="md-icon-button" ng-click="reload()">
  41 + <md-icon>refresh</md-icon>
  42 + <md-tooltip md-direction="top">
  43 + {{ 'action.refresh' | translate }}
  44 + </md-tooltip>
  45 + </md-button>
  46 + </div>
  47 + </div>
  48 + <div flex layout="column" class="tb-audit-log-container" ng-class="{'md-whiteframe-z1': !pageMode}">
  49 + <md-list flex layout="column" class="tb-audit-log-table" ng-class="{'tb-audit-log-table-full': pageMode}">
  50 + <md-list class="tb-row tb-header" layout="row" layout-align="start center" tb-audit-log-header audit-log-mode="{{auditLogMode}}">
  51 + </md-list>
  52 + <md-progress-linear style="max-height: 0px;" md-mode="indeterminate" ng-disabled="!$root.loading"
  53 + ng-show="$root.loading"></md-progress-linear>
  54 + <md-divider></md-divider>
  55 + <span translate layout-align="center center"
  56 + style="margin-top: 25px;"
  57 + class="tb-prompt" ng-show="noData()">audit-log.no-audit-logs-prompt</span>
  58 + <md-virtual-repeat-container ng-show="hasData()" flex md-top-index="topIndex" tb-scope-element="repeatContainer">
  59 + <md-list-item md-virtual-repeat="auditLog in theAuditLogs" md-on-demand flex ng-style="hasScroll() ? {'margin-right':'-15px'} : {}">
  60 + <md-list class="tb-row" flex layout="row" layout-align="start center" tb-audit-log-row audit-log-mode="{{auditLogMode}}" audit-log="{{auditLog}}">
  61 + </md-list>
  62 + <md-divider flex></md-divider>
  63 + </md-list-item>
  64 + </md-virtual-repeat-container>
  65 + </md-list>
  66 + </div>
  67 + </div>
  68 +</md-content>
  1 +/*
  2 + * Copyright © 2016-2017 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +/* eslint-disable import/no-unresolved, import/default */
  17 +
  18 +import auditLogsTemplate from './audit-logs.tpl.html';
  19 +
  20 +/* eslint-enable import/no-unresolved, import/default */
  21 +
  22 +/*@ngInject*/
  23 +export default function AuditLogRoutes($stateProvider) {
  24 + $stateProvider
  25 + .state('home.auditLogs', {
  26 + url: '/auditLogs',
  27 + module: 'private',
  28 + auth: ['TENANT_ADMIN'],
  29 + views: {
  30 + "content@home": {
  31 + templateUrl: auditLogsTemplate,
  32 + controller: 'AuditLogsController',
  33 + controllerAs: 'vm'
  34 + }
  35 + },
  36 + data: {
  37 + searchEnabled: false,
  38 + pageTitle: 'audit-log.audit-logs'
  39 + },
  40 + ncyBreadcrumb: {
  41 + label: '{"icon": "track_changes", "label": "audit-log.audit-logs"}'
  42 + }
  43 + });
  44 +}
  1 +/**
  2 + * Copyright © 2016-2017 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +.tb-audit-logs {
  18 + background-color: #fff;
  19 + .tb-audit-log-margin-18px {
  20 + margin-bottom: 18px;
  21 + }
  22 + .tb-audit-log-toolbar {
  23 + font-size: 20px;
  24 + }
  25 + md-input-container.tb-audit-log-search-input {
  26 + .md-errors-spacer {
  27 + min-height: 0px;
  28 + }
  29 + }
  30 +}
  31 +
  32 +.tb-audit-log-container {
  33 + overflow-x: auto;
  34 +}
  35 +
  36 +
  37 +
  38 +md-list.tb-audit-log-table {
  39 + padding: 0px;
  40 + min-width: 700px;
  41 + &.tb-audit-log-table-full {
  42 + min-width: 900px;
  43 + }
  44 +
  45 + md-list-item {
  46 + padding: 0px;
  47 + }
  48 +
  49 + .tb-row {
  50 + height: 48px;
  51 + padding: 0px;
  52 + overflow: hidden;
  53 + }
  54 +
  55 + .tb-row:hover {
  56 + background-color: #EEEEEE;
  57 + }
  58 +
  59 + .tb-header:hover {
  60 + background: none;
  61 + }
  62 +
  63 + .tb-header {
  64 + .tb-cell {
  65 + color: rgba(0,0,0,.54);
  66 + font-size: 12px;
  67 + font-weight: 700;
  68 + white-space: nowrap;
  69 + background: none;
  70 + }
  71 + }
  72 +
  73 + .tb-cell {
  74 + padding: 0 24px;
  75 + margin: auto 0;
  76 + color: rgba(0,0,0,.87);
  77 + font-size: 13px;
  78 + vertical-align: middle;
  79 + text-align: left;
  80 + overflow: hidden;
  81 + .md-button {
  82 + padding: 0;
  83 + margin: 0;
  84 + }
  85 + }
  86 +
  87 + .tb-cell.tb-number {
  88 + text-align: right;
  89 + }
  90 +
  91 +}
  1 +/*
  2 + * Copyright © 2016-2017 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +/*@ngInject*/
  18 +export default function AuditLogsController(types) {
  19 +
  20 + var vm = this;
  21 +
  22 + vm.types = types;
  23 +
  24 +}
  1 +<!--
  2 +
  3 + Copyright © 2016-2017 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +
  19 +<tb-audit-log-table class="md-whiteframe-z1"
  20 + flex
  21 + audit-log-mode="{{vm.types.auditLogMode.tenant}}"
  22 + page-mode="true">
  23 +</tb-audit-log-table>
  1 +/*
  2 + * Copyright © 2016-2017 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +import AuditLogRoutes from './audit-log.routes';
  18 +import AuditLogsController from './audit-logs.controller';
  19 +import AuditLogDetailsDialogController from './audit-log-details-dialog.controller';
  20 +import AuditLogHeaderDirective from './audit-log-header.directive';
  21 +import AuditLogRowDirective from './audit-log-row.directive';
  22 +import AuditLogTableDirective from './audit-log-table.directive';
  23 +
  24 +export default angular.module('thingsboard.auditLog', [])
  25 + .config(AuditLogRoutes)
  26 + .controller('AuditLogsController', AuditLogsController)
  27 + .controller('AuditLogDetailsDialogController', AuditLogDetailsDialogController)
  28 + .directive('tbAuditLogHeader', AuditLogHeaderDirective)
  29 + .directive('tbAuditLogRow', AuditLogRowDirective)
  30 + .directive('tbAuditLogTable', AuditLogTableDirective)
  31 + .name;
@@ -156,6 +156,63 @@ export default angular.module('thingsboard.types', []) @@ -156,6 +156,63 @@ export default angular.module('thingsboard.types', [])
156 color: "green" 156 color: "green"
157 } 157 }
158 }, 158 },
  159 + auditLogActionType: {
  160 + "ADDED": {
  161 + name: "audit-log.type-added"
  162 + },
  163 + "DELETED": {
  164 + name: "audit-log.type-deleted"
  165 + },
  166 + "UPDATED": {
  167 + name: "audit-log.type-updated"
  168 + },
  169 + "ATTRIBUTES_UPDATED": {
  170 + name: "audit-log.type-attributes-updated"
  171 + },
  172 + "ATTRIBUTES_DELETED": {
  173 + name: "audit-log.type-attributes-deleted"
  174 + },
  175 + "RPC_CALL": {
  176 + name: "audit-log.type-rpc-call"
  177 + },
  178 + "CREDENTIALS_UPDATED": {
  179 + name: "audit-log.type-credentials-updated"
  180 + },
  181 + "ASSIGNED_TO_CUSTOMER": {
  182 + name: "audit-log.type-assigned-to-customer"
  183 + },
  184 + "UNASSIGNED_FROM_CUSTOMER": {
  185 + name: "audit-log.type-unassigned-from-customer"
  186 + },
  187 + "ACTIVATED": {
  188 + name: "audit-log.type-activated"
  189 + },
  190 + "SUSPENDED": {
  191 + name: "audit-log.type-suspended"
  192 + },
  193 + "CREDENTIALS_READ": {
  194 + name: "audit-log.type-credentials-read"
  195 + },
  196 + "ATTRIBUTES_READ": {
  197 + name: "audit-log.type-attributes-read"
  198 + }
  199 + },
  200 + auditLogActionStatus: {
  201 + "SUCCESS": {
  202 + value: "SUCCESS",
  203 + name: "audit-log.status-success"
  204 + },
  205 + "FAILURE": {
  206 + value: "FAILURE",
  207 + name: "audit-log.status-failure"
  208 + }
  209 + },
  210 + auditLogMode: {
  211 + tenant: "tenant",
  212 + entity: "entity",
  213 + user: "user",
  214 + customer: "customer"
  215 + },
159 aliasFilterType: { 216 aliasFilterType: {
160 singleEntity: { 217 singleEntity: {
161 value: 'singleEntity', 218 value: 'singleEntity',
@@ -125,7 +125,7 @@ function Grid() { @@ -125,7 +125,7 @@ function Grid() {
125 } 125 }
126 126
127 /*@ngInject*/ 127 /*@ngInject*/
128 -function GridController($scope, $state, $mdDialog, $document, $q, $mdUtil, $timeout, $translate, $mdMedia, $templateCache, $window) { 128 +function GridController($scope, $state, $mdDialog, $document, $q, $mdUtil, $timeout, $translate, $mdMedia, $templateCache, $window, userService) {
129 129
130 var vm = this; 130 var vm = this;
131 131
@@ -157,6 +157,7 @@ function GridController($scope, $state, $mdDialog, $document, $q, $mdUtil, $time @@ -157,6 +157,7 @@ function GridController($scope, $state, $mdDialog, $document, $q, $mdUtil, $time
157 vm.saveItem = saveItem; 157 vm.saveItem = saveItem;
158 vm.toggleItemSelection = toggleItemSelection; 158 vm.toggleItemSelection = toggleItemSelection;
159 vm.triggerResize = triggerResize; 159 vm.triggerResize = triggerResize;
  160 + vm.isTenantAdmin = isTenantAdmin;
160 161
161 $scope.$watch(function () { 162 $scope.$watch(function () {
162 return $mdMedia('xs') || $mdMedia('sm'); 163 return $mdMedia('xs') || $mdMedia('sm');
@@ -634,6 +635,10 @@ function GridController($scope, $state, $mdDialog, $document, $q, $mdUtil, $time @@ -634,6 +635,10 @@ function GridController($scope, $state, $mdDialog, $document, $q, $mdUtil, $time
634 w.triggerHandler('resize'); 635 w.triggerHandler('resize');
635 } 636 }
636 637
  638 + function isTenantAdmin() {
  639 + return userService.getAuthority() == 'TENANT_ADMIN';
  640 + }
  641 +
637 function moveToTop() { 642 function moveToTop() {
638 moveToIndex(0, true); 643 moveToIndex(0, true);
639 } 644 }
@@ -66,5 +66,10 @@ @@ -66,5 +66,10 @@
66 entity-type="{{vm.types.entityType.customer}}"> 66 entity-type="{{vm.types.entityType.customer}}">
67 </tb-relation-table> 67 </tb-relation-table>
68 </md-tab> 68 </md-tab>
  69 + <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.grid.isTenantAdmin()" md-on-select="vm.grid.triggerResize()" label="{{ 'audit-log.audit-logs' | translate }}">
  70 + <tb-audit-log-table flex customer-id="vm.grid.operatingItem().id.id"
  71 + audit-log-mode="{{vm.types.auditLogMode.customer}}">
  72 + </tb-audit-log-table>
  73 + </md-tab>
69 </md-tabs> 74 </md-tabs>
70 </tb-grid> 75 </tb-grid>
@@ -19,13 +19,24 @@ @@ -19,13 +19,24 @@
19 <details-buttons tb-help="'dashboards'" help-container-id="help-container"> 19 <details-buttons tb-help="'dashboards'" help-container-id="help-container">
20 <div id="help-container"></div> 20 <div id="help-container"></div>
21 </details-buttons> 21 </details-buttons>
22 - <tb-dashboard-details dashboard="vm.grid.operatingItem()"  
23 - is-edit="vm.grid.detailsConfig.isDetailsEditMode"  
24 - dashboard-scope="vm.dashboardsScope"  
25 - the-form="vm.grid.detailsForm"  
26 - on-assign-to-customer="vm.assignToCustomer(event, [ vm.grid.detailsConfig.currentItem.id.id ])"  
27 - on-make-public="vm.makePublic(event, vm.grid.detailsConfig.currentItem)"  
28 - on-unassign-from-customer="vm.unassignFromCustomer(event, vm.grid.detailsConfig.currentItem, isPublic)"  
29 - on-export-dashboard="vm.exportDashboard(event, vm.grid.detailsConfig.currentItem)"  
30 - on-delete-dashboard="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"></tb-dashboard-details> 22 + <md-tabs ng-class="{'tb-headless': vm.grid.detailsConfig.isDetailsEditMode}"
  23 + id="tabs" md-border-bottom flex class="tb-absolute-fill">
  24 + <md-tab label="{{ 'dashboard.details' | translate }}">
  25 + <tb-dashboard-details dashboard="vm.grid.operatingItem()"
  26 + is-edit="vm.grid.detailsConfig.isDetailsEditMode"
  27 + dashboard-scope="vm.dashboardsScope"
  28 + the-form="vm.grid.detailsForm"
  29 + on-assign-to-customer="vm.assignToCustomer(event, [ vm.grid.detailsConfig.currentItem.id.id ])"
  30 + on-make-public="vm.makePublic(event, vm.grid.detailsConfig.currentItem)"
  31 + on-unassign-from-customer="vm.unassignFromCustomer(event, vm.grid.detailsConfig.currentItem, isPublic)"
  32 + on-export-dashboard="vm.exportDashboard(event, vm.grid.detailsConfig.currentItem)"
  33 + on-delete-dashboard="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"></tb-dashboard-details>
  34 + </md-tab>
  35 + <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.grid.isTenantAdmin()" md-on-select="vm.grid.triggerResize()" label="{{ 'audit-log.audit-logs' | translate }}">
  36 + <tb-audit-log-table flex entity-type="vm.types.entityType.dashboard"
  37 + entity-id="vm.grid.operatingItem().id.id"
  38 + audit-log-mode="{{vm.types.auditLogMode.entity}}">
  39 + </tb-audit-log-table>
  40 + </md-tab>
  41 + </md-tabs>
31 </tb-grid> 42 </tb-grid>
@@ -39,10 +39,8 @@ @@ -39,10 +39,8 @@
39 <md-icon md-svg-icon="mdi:clipboard-arrow-left"></md-icon> 39 <md-icon md-svg-icon="mdi:clipboard-arrow-left"></md-icon>
40 <span translate>device.copyId</span> 40 <span translate>device.copyId</span>
41 </md-button> 41 </md-button>
42 - <md-button ngclipboard data-clipboard-action="copy"  
43 - ngclipboard-success="onAccessTokenCopied(e)"  
44 - data-clipboard-text="{{deviceCredentials.credentialsId}}" ng-show="!isEdit"  
45 - class="md-raised"> 42 + <md-button ng-show="!isEdit"
  43 + class="md-raised" ng-click="copyAccessToken($event)">
46 <md-icon md-svg-icon="mdi:clipboard-arrow-left"></md-icon> 44 <md-icon md-svg-icon="mdi:clipboard-arrow-left"></md-icon>
47 <span translate>device.copyAccessToken</span> 45 <span translate>device.copyAccessToken</span>
48 </md-button> 46 </md-button>
@@ -20,7 +20,7 @@ import deviceFieldsetTemplate from './device-fieldset.tpl.html'; @@ -20,7 +20,7 @@ import deviceFieldsetTemplate from './device-fieldset.tpl.html';
20 /* eslint-enable import/no-unresolved, import/default */ 20 /* eslint-enable import/no-unresolved, import/default */
21 21
22 /*@ngInject*/ 22 /*@ngInject*/
23 -export default function DeviceDirective($compile, $templateCache, toast, $translate, types, deviceService, customerService) { 23 +export default function DeviceDirective($compile, $templateCache, toast, $translate, types, clipboardService, deviceService, customerService) {
24 var linker = function (scope, element) { 24 var linker = function (scope, element) {
25 var template = $templateCache.get(deviceFieldsetTemplate); 25 var template = $templateCache.get(deviceFieldsetTemplate);
26 element.html(template); 26 element.html(template);
@@ -30,17 +30,8 @@ export default function DeviceDirective($compile, $templateCache, toast, $transl @@ -30,17 +30,8 @@ export default function DeviceDirective($compile, $templateCache, toast, $transl
30 scope.isPublic = false; 30 scope.isPublic = false;
31 scope.assignedCustomer = null; 31 scope.assignedCustomer = null;
32 32
33 - scope.deviceCredentials = null;  
34 -  
35 scope.$watch('device', function(newVal) { 33 scope.$watch('device', function(newVal) {
36 if (newVal) { 34 if (newVal) {
37 - if (scope.device.id) {  
38 - deviceService.getDeviceCredentials(scope.device.id.id).then(  
39 - function success(credentials) {  
40 - scope.deviceCredentials = credentials;  
41 - }  
42 - );  
43 - }  
44 if (scope.device.customerId && scope.device.customerId.id !== types.id.nullUid) { 35 if (scope.device.customerId && scope.device.customerId.id !== types.id.nullUid) {
45 scope.isAssignedToCustomer = true; 36 scope.isAssignedToCustomer = true;
46 customerService.getShortCustomerInfo(scope.device.customerId.id).then( 37 customerService.getShortCustomerInfo(scope.device.customerId.id).then(
@@ -61,8 +52,20 @@ export default function DeviceDirective($compile, $templateCache, toast, $transl @@ -61,8 +52,20 @@ export default function DeviceDirective($compile, $templateCache, toast, $transl
61 toast.showSuccess($translate.instant('device.idCopiedMessage'), 750, angular.element(element).parent().parent(), 'bottom left'); 52 toast.showSuccess($translate.instant('device.idCopiedMessage'), 750, angular.element(element).parent().parent(), 'bottom left');
62 }; 53 };
63 54
64 - scope.onAccessTokenCopied = function() {  
65 - toast.showSuccess($translate.instant('device.accessTokenCopiedMessage'), 750, angular.element(element).parent().parent(), 'bottom left'); 55 + scope.copyAccessToken = function(e) {
  56 + const trigger = e.delegateTarget || e.currentTarget;
  57 + if (scope.device.id) {
  58 + deviceService.getDeviceCredentials(scope.device.id.id, true).then(
  59 + function success(credentials) {
  60 + var credentialsId = credentials.credentialsId;
  61 + clipboardService.copyToClipboard(trigger, credentialsId).then(
  62 + () => {
  63 + toast.showSuccess($translate.instant('device.accessTokenCopiedMessage'), 750, angular.element(element).parent().parent(), 'bottom left');
  64 + }
  65 + );
  66 + }
  67 + );
  68 + }
66 }; 69 };
67 70
68 $compile(element.contents())(scope); 71 $compile(element.contents())(scope);
@@ -74,4 +74,10 @@ @@ -74,4 +74,10 @@
74 entity-type="{{vm.types.entityType.device}}"> 74 entity-type="{{vm.types.entityType.device}}">
75 </tb-extension-table> 75 </tb-extension-table>
76 </md-tab> 76 </md-tab>
  77 + <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.grid.isTenantAdmin()" md-on-select="vm.grid.triggerResize()" label="{{ 'audit-log.audit-logs' | translate }}">
  78 + <tb-audit-log-table flex entity-type="vm.types.entityType.device"
  79 + entity-id="vm.grid.operatingItem().id.id"
  80 + audit-log-mode="{{vm.types.auditLogMode.entity}}">
  81 + </tb-audit-log-table>
  82 + </md-tab>
77 </tb-grid> 83 </tb-grid>
@@ -26,9 +26,16 @@ @@ -26,9 +26,16 @@
26 </md-select> 26 </md-select>
27 </md-input-container> 27 </md-input-container>
28 <tb-timewindow flex ng-model="timewindow" history-only as-button="true"></tb-timewindow> 28 <tb-timewindow flex ng-model="timewindow" history-only as-button="true"></tb-timewindow>
  29 + <md-button ng-disabled="$root.loading"
  30 + class="md-icon-button" ng-click="reload()">
  31 + <md-icon>refresh</md-icon>
  32 + <md-tooltip md-direction="top">
  33 + {{ 'action.refresh' | translate }}
  34 + </md-tooltip>
  35 + </md-button>
29 </section> 36 </section>
30 <md-list flex layout="column" class="md-whiteframe-z1 tb-event-table"> 37 <md-list flex layout="column" class="md-whiteframe-z1 tb-event-table">
31 - <md-list class="tb-row tb-header" layout="row" tb-event-header event-type="{{eventType}}"> 38 + <md-list class="tb-row tb-header" layout="row" layout-align="start center" tb-event-header event-type="{{eventType}}">
32 </md-list> 39 </md-list>
33 <md-progress-linear style="max-height: 0px;" md-mode="indeterminate" ng-disabled="!$root.loading" 40 <md-progress-linear style="max-height: 0px;" md-mode="indeterminate" ng-disabled="!$root.loading"
34 ng-show="$root.loading"></md-progress-linear> 41 ng-show="$root.loading"></md-progress-linear>
@@ -38,7 +45,7 @@ @@ -38,7 +45,7 @@
38 class="tb-prompt" ng-show="noData()">event.no-events-prompt</span> 45 class="tb-prompt" ng-show="noData()">event.no-events-prompt</span>
39 <md-virtual-repeat-container ng-show="hasData()" flex md-top-index="topIndex" tb-scope-element="repeatContainer"> 46 <md-virtual-repeat-container ng-show="hasData()" flex md-top-index="topIndex" tb-scope-element="repeatContainer">
40 <md-list-item md-virtual-repeat="event in theEvents" md-on-demand flex ng-style="hasScroll() ? {'margin-right':'-15px'} : {}"> 47 <md-list-item md-virtual-repeat="event in theEvents" md-on-demand flex ng-style="hasScroll() ? {'margin-right':'-15px'} : {}">
41 - <md-list class="tb-row" flex layout="row" tb-event-row event-type="{{eventType}}" event="{{event}}"> 48 + <md-list class="tb-row" flex layout="row" layout-align="start center" tb-event-row event-type="{{eventType}}" event="{{event}}">
42 </md-list> 49 </md-list>
43 <md-divider flex></md-divider> 50 <md-divider flex></md-divider>
44 </md-list-item> 51 </md-list-item>
@@ -35,6 +35,7 @@ import thingsboardUserMenu from './user-menu.directive'; @@ -35,6 +35,7 @@ import thingsboardUserMenu from './user-menu.directive';
35 import thingsboardEntity from '../entity'; 35 import thingsboardEntity from '../entity';
36 import thingsboardEvent from '../event'; 36 import thingsboardEvent from '../event';
37 import thingsboardAlarm from '../alarm'; 37 import thingsboardAlarm from '../alarm';
  38 +import thingsboardAuditLog from '../audit';
38 import thingsboardExtension from '../extension'; 39 import thingsboardExtension from '../extension';
39 import thingsboardTenant from '../tenant'; 40 import thingsboardTenant from '../tenant';
40 import thingsboardCustomer from '../customer'; 41 import thingsboardCustomer from '../customer';
@@ -67,6 +68,7 @@ export default angular.module('thingsboard.home', [ @@ -67,6 +68,7 @@ export default angular.module('thingsboard.home', [
67 thingsboardEntity, 68 thingsboardEntity,
68 thingsboardEvent, 69 thingsboardEvent,
69 thingsboardAlarm, 70 thingsboardAlarm,
  71 + thingsboardAuditLog,
70 thingsboardExtension, 72 thingsboardExtension,
71 thingsboardTenant, 73 thingsboardTenant,
72 thingsboardCustomer, 74 thingsboardCustomer,
@@ -286,6 +286,38 @@ export default angular.module('thingsboard.locale', []) @@ -286,6 +286,38 @@ export default angular.module('thingsboard.locale', [])
286 "selected-attributes": "{ count, select, 1 {1 attribute} other {# attributes} } selected", 286 "selected-attributes": "{ count, select, 1 {1 attribute} other {# attributes} } selected",
287 "selected-telemetry": "{ count, select, 1 {1 telemetry unit} other {# telemetry units} } selected" 287 "selected-telemetry": "{ count, select, 1 {1 telemetry unit} other {# telemetry units} } selected"
288 }, 288 },
  289 + "audit-log": {
  290 + "audit": "Audit",
  291 + "audit-logs": "Audit Logs",
  292 + "timestamp": "Timestamp",
  293 + "entity-type": "Entity Type",
  294 + "entity-name": "Entity Name",
  295 + "user": "User",
  296 + "type": "Type",
  297 + "status": "Status",
  298 + "details": "Details",
  299 + "type-added": "Added",
  300 + "type-deleted": "Deleted",
  301 + "type-updated": "Updated",
  302 + "type-attributes-updated": "Attributes updated",
  303 + "type-attributes-deleted": "Attributes deleted",
  304 + "type-rpc-call": "RPC call",
  305 + "type-credentials-updated": "Credentials updated",
  306 + "type-assigned-to-customer": "Assigned to Customer",
  307 + "type-unassigned-from-customer": "Unassigned from Customer",
  308 + "type-activated": "Activated",
  309 + "type-suspended": "Suspended",
  310 + "type-credentials-read": "Credentials read",
  311 + "type-attributes-read": "Attributes read",
  312 + "status-success": "Success",
  313 + "status-failure": "Failure",
  314 + "audit-log-details": "Audit log details",
  315 + "no-audit-logs-prompt": "No logs found",
  316 + "action-data": "Action data",
  317 + "failure-details": "Failure details",
  318 + "search": "Search audit logs",
  319 + "clear-search": "Clear search"
  320 + },
289 "confirm-on-exit": { 321 "confirm-on-exit": {
290 "message": "You have unsaved changes. Are you sure you want to leave this page?", 322 "message": "You have unsaved changes. Are you sure you want to leave this page?",
291 "html-message": "You have unsaved changes.<br/>Are you sure you want to leave this page?", 323 "html-message": "You have unsaved changes.<br/>Are you sure you want to leave this page?",
@@ -1183,7 +1215,8 @@ export default angular.module('thingsboard.locale', []) @@ -1183,7 +1215,8 @@ export default angular.module('thingsboard.locale', [])
1183 "activation-link": "User activation link", 1215 "activation-link": "User activation link",
1184 "activation-link-text": "In order to activate user use the following <a href='{{activationLink}}' target='_blank'>activation link</a> :", 1216 "activation-link-text": "In order to activate user use the following <a href='{{activationLink}}' target='_blank'>activation link</a> :",
1185 "copy-activation-link": "Copy activation link", 1217 "copy-activation-link": "Copy activation link",
1186 - "activation-link-copied-message": "User activation link has been copied to clipboard" 1218 + "activation-link-copied-message": "User activation link has been copied to clipboard",
  1219 + "details": "Details"
1187 }, 1220 },
1188 "value": { 1221 "value": {
1189 "type": "Value type", 1222 "type": "Value type",
@@ -66,5 +66,12 @@ @@ -66,5 +66,12 @@
66 entity-type="{{vm.types.entityType.plugin}}"> 66 entity-type="{{vm.types.entityType.plugin}}">
67 </tb-relation-table> 67 </tb-relation-table>
68 </md-tab> 68 </md-tab>
  69 + <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isPluginEditable(vm.grid.operatingItem()) && vm.grid.isTenantAdmin()"
  70 + md-on-select="vm.grid.triggerResize()" label="{{ 'audit-log.audit-logs' | translate }}">
  71 + <tb-audit-log-table flex entity-type="vm.types.entityType.plugin"
  72 + entity-id="vm.grid.operatingItem().id.id"
  73 + audit-log-mode="{{vm.types.auditLogMode.entity}}">
  74 + </tb-audit-log-table>
  75 + </md-tab>
69 </md-tabs> 76 </md-tabs>
70 </tb-grid> 77 </tb-grid>
@@ -66,5 +66,12 @@ @@ -66,5 +66,12 @@
66 entity-type="{{vm.types.entityType.rule}}"> 66 entity-type="{{vm.types.entityType.rule}}">
67 </tb-relation-table> 67 </tb-relation-table>
68 </md-tab> 68 </md-tab>
  69 + <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isRuleEditable(vm.grid.operatingItem()) && vm.grid.isTenantAdmin()"
  70 + md-on-select="vm.grid.triggerResize()" label="{{ 'audit-log.audit-logs' | translate }}">
  71 + <tb-audit-log-table flex entity-type="vm.types.entityType.rule"
  72 + entity-id="vm.grid.operatingItem().id.id"
  73 + audit-log-mode="{{vm.types.auditLogMode.entity}}">
  74 + </tb-audit-log-table>
  75 + </md-tab>
69 </md-tabs> 76 </md-tabs>
70 </tb-grid> 77 </tb-grid>
  1 +/*
  2 + * Copyright © 2016-2017 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +export default angular.module('thingsboard.clipboard', [])
  17 + .factory('clipboardService', ClipboardService)
  18 + .name;
  19 +
  20 +/*@ngInject*/
  21 +function ClipboardService($q) {
  22 +
  23 + var fakeHandler, fakeHandlerCallback, fakeElem;
  24 +
  25 + var service = {
  26 + copyToClipboard: copyToClipboard
  27 + };
  28 +
  29 + return service;
  30 +
  31 + /* eslint-disable */
  32 + function copyToClipboard(trigger, text) {
  33 + var deferred = $q.defer();
  34 + const isRTL = document.documentElement.getAttribute('dir') == 'rtl';
  35 + removeFake();
  36 + fakeHandlerCallback = () => removeFake();
  37 + fakeHandler = document.body.addEventListener('click', fakeHandlerCallback) || true;
  38 + fakeElem = document.createElement('textarea');
  39 + fakeElem.style.fontSize = '12pt';
  40 + fakeElem.style.border = '0';
  41 + fakeElem.style.padding = '0';
  42 + fakeElem.style.margin = '0';
  43 + fakeElem.style.position = 'absolute';
  44 + fakeElem.style[ isRTL ? 'right' : 'left' ] = '-9999px';
  45 + let yPosition = window.pageYOffset || document.documentElement.scrollTop;
  46 + fakeElem.style.top = `${yPosition}px`;
  47 + fakeElem.setAttribute('readonly', '');
  48 + fakeElem.value = text;
  49 + document.body.appendChild(fakeElem);
  50 + var selectedText = select(fakeElem);
  51 +
  52 + let succeeded;
  53 + try {
  54 + succeeded = document.execCommand('copy');
  55 + }
  56 + catch (err) {
  57 + succeeded = false;
  58 + }
  59 + if (trigger) {
  60 + trigger.focus();
  61 + }
  62 + window.getSelection().removeAllRanges();
  63 + removeFake();
  64 + if (succeeded) {
  65 + deferred.resolve(selectedText);
  66 + } else {
  67 + deferred.reject();
  68 + }
  69 + return deferred.promise;
  70 + }
  71 +
  72 + function removeFake() {
  73 + if (fakeHandler) {
  74 + document.body.removeEventListener('click', fakeHandlerCallback);
  75 + fakeHandler = null;
  76 + fakeHandlerCallback = null;
  77 + }
  78 + if (fakeElem) {
  79 + document.body.removeChild(fakeElem);
  80 + fakeElem = null;
  81 + }
  82 + }
  83 +
  84 + function select(element) {
  85 + var selectedText;
  86 +
  87 + if (element.nodeName === 'SELECT') {
  88 + element.focus();
  89 +
  90 + selectedText = element.value;
  91 + }
  92 + else if (element.nodeName === 'INPUT' || element.nodeName === 'TEXTAREA') {
  93 + var isReadOnly = element.hasAttribute('readonly');
  94 +
  95 + if (!isReadOnly) {
  96 + element.setAttribute('readonly', '');
  97 + }
  98 +
  99 + element.select();
  100 + element.setSelectionRange(0, element.value.length);
  101 +
  102 + if (!isReadOnly) {
  103 + element.removeAttribute('readonly');
  104 + }
  105 +
  106 + selectedText = element.value;
  107 + }
  108 + else {
  109 + if (element.hasAttribute('contenteditable')) {
  110 + element.focus();
  111 + }
  112 +
  113 + var selection = window.getSelection();
  114 + var range = document.createRange();
  115 +
  116 + range.selectNodeContents(element);
  117 + selection.removeAllRanges();
  118 + selection.addRange(range);
  119 +
  120 + selectedText = selection.toString();
  121 + }
  122 +
  123 + return selectedText;
  124 + }
  125 +
  126 + /* eslint-enable */
  127 +
  128 +}
@@ -211,6 +211,12 @@ function Menu(userService, $state, $rootScope) { @@ -211,6 +211,12 @@ function Menu(userService, $state, $rootScope) {
211 type: 'link', 211 type: 'link',
212 state: 'home.dashboards', 212 state: 'home.dashboards',
213 icon: 'dashboards' 213 icon: 'dashboards'
  214 + },
  215 + {
  216 + name: 'audit-log.audit-logs',
  217 + type: 'link',
  218 + state: 'home.auditLogs',
  219 + icon: 'track_changes'
214 }]; 220 }];
215 221
216 homeSections = 222 homeSections =
@@ -273,6 +279,16 @@ function Menu(userService, $state, $rootScope) { @@ -273,6 +279,16 @@ function Menu(userService, $state, $rootScope) {
273 state: 'home.dashboards' 279 state: 'home.dashboards'
274 } 280 }
275 ] 281 ]
  282 + },
  283 + {
  284 + name: 'audit-log.audit',
  285 + places: [
  286 + {
  287 + name: 'audit-log.audit-logs',
  288 + icon: 'track_changes',
  289 + state: 'home.auditLogs'
  290 + }
  291 + ]
276 }]; 292 }];
277 293
278 } else if (authority === 'CUSTOMER_USER') { 294 } else if (authority === 'CUSTOMER_USER') {
@@ -42,6 +42,8 @@ export default function UserController(userService, toast, $scope, $mdDialog, $d @@ -42,6 +42,8 @@ export default function UserController(userService, toast, $scope, $mdDialog, $d
42 42
43 var vm = this; 43 var vm = this;
44 44
  45 + vm.types = types;
  46 +
45 vm.userGridConfig = { 47 vm.userGridConfig = {
46 deleteItemTitleFunc: deleteUserTitle, 48 deleteItemTitleFunc: deleteUserTitle,
47 deleteItemContentFunc: deleteUserText, 49 deleteItemContentFunc: deleteUserText,
@@ -19,10 +19,20 @@ @@ -19,10 +19,20 @@
19 <details-buttons tb-help="'users'" help-container-id="help-container"> 19 <details-buttons tb-help="'users'" help-container-id="help-container">
20 <div id="help-container"></div> 20 <div id="help-container"></div>
21 </details-buttons> 21 </details-buttons>
22 - <tb-user user="vm.grid.operatingItem()"  
23 - is-edit="vm.grid.detailsConfig.isDetailsEditMode"  
24 - the-form="vm.grid.detailsForm"  
25 - on-display-activation-link="vm.displayActivationLink(event, vm.grid.detailsConfig.currentItem)"  
26 - on-resend-activation="vm.resendActivation(vm.grid.detailsConfig.currentItem)"  
27 - on-delete-user="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"></tb-user> 22 + <md-tabs ng-class="{'tb-headless': vm.grid.detailsConfig.isDetailsEditMode}"
  23 + id="tabs" md-border-bottom flex class="tb-absolute-fill">
  24 + <md-tab label="{{ 'user.details' | translate }}">
  25 + <tb-user user="vm.grid.operatingItem()"
  26 + is-edit="vm.grid.detailsConfig.isDetailsEditMode"
  27 + the-form="vm.grid.detailsForm"
  28 + on-display-activation-link="vm.displayActivationLink(event, vm.grid.detailsConfig.currentItem)"
  29 + on-resend-activation="vm.resendActivation(vm.grid.detailsConfig.currentItem)"
  30 + on-delete-user="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"></tb-user>
  31 + </md-tab>
  32 + <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.grid.isTenantAdmin()" md-on-select="vm.grid.triggerResize()" label="{{ 'audit-log.audit-logs' | translate }}">
  33 + <tb-audit-log-table flex user-id="vm.grid.operatingItem().id.id"
  34 + audit-log-mode="{{vm.types.auditLogMode.user}}">
  35 + </tb-audit-log-table>
  36 + </md-tab>
  37 + </md-tabs>
28 </tb-grid> 38 </tb-grid>
@@ -203,6 +203,19 @@ md-sidenav { @@ -203,6 +203,19 @@ md-sidenav {
203 * THINGSBOARD SPECIFIC 203 * THINGSBOARD SPECIFIC
204 ***********************/ 204 ***********************/
205 205
  206 +label {
  207 + &.tb-title {
  208 + pointer-events: none;
  209 + color: #666;
  210 + font-size: 13px;
  211 + font-weight: 400;
  212 + padding-bottom: 15px;
  213 + &.no-padding {
  214 + padding-bottom: 0px;
  215 + }
  216 + }
  217 +}
  218 +
206 .tb-noselect { 219 .tb-noselect {
207 -webkit-touch-callout: none; /* iOS Safari */ 220 -webkit-touch-callout: none; /* iOS Safari */
208 -webkit-user-select: none; /* Safari */ 221 -webkit-user-select: none; /* Safari */