thingsboard-aplcache
Changes
application/src/main/java/org/thingsboard/server/actors/plugin/PluginProcessingContext.java 74(+71 -3)
application/src/main/java/org/thingsboard/server/actors/plugin/SharedPluginProcessingContext.java 3(+3 -0)
application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java 6(+6 -0)
application/src/main/java/org/thingsboard/server/controller/plugin/PluginApiController.java 6(+5 -1)
application/src/main/java/org/thingsboard/server/controller/plugin/PluginWebSocketHandler.java 5(+4 -1)
application/src/main/java/org/thingsboard/server/install/ThingsboardInstallConfiguration.java 34(+34 -0)
application/src/main/java/org/thingsboard/server/service/install/CassandraDatabaseUpgradeService.java 6(+6 -0)
application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java 9(+9 -0)
application/src/test/java/org/thingsboard/server/controller/BaseAuditLogControllerTest.java 148(+148 -0)
application/src/test/java/org/thingsboard/server/controller/nosql/AuditLogControllerNoSqlTest.java 23(+23 -0)
application/src/test/java/org/thingsboard/server/controller/sql/AuditLogControllerSqlTest.java 23(+23 -0)
dao/src/main/resources/cassandra/schema.cql 75(+75 -0)
dao/src/main/resources/sql/schema.sql 15(+15 -0)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/ToDeviceRpcRequest.java 2(+2 -0)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginApiCallSecurityContext.java 20(+15 -5)
extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginContext.java 11(+8 -3)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/messaging/DeviceMessagingRuleMsgHandler.java 2(+1 -1)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/handlers/RpcRestMsgHandler.java 10(+8 -2)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/handlers/RpcRuleMsgHandler.java 2(+1 -1)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/RpcManager.java 4(+2 -2)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRestMsgHandler.java 27(+19 -8)
ui/src/app/api/audit-log.service.js 116(+116 -0)
ui/src/app/api/device.service.js 27(+20 -7)
ui/src/app/api/user.service.js 9(+9 -0)
ui/src/app/app.js 4(+4 -0)
ui/src/app/asset/assets.tpl.html 6(+6 -0)
ui/src/app/audit/audit-log.routes.js 44(+44 -0)
ui/src/app/audit/audit-log.scss 91(+91 -0)
ui/src/app/audit/audit-log-header.tpl.html 24(+24 -0)
ui/src/app/audit/audit-log-row.directive.js 67(+67 -0)
ui/src/app/audit/audit-log-row.tpl.html 36(+36 -0)
ui/src/app/audit/audit-logs.controller.js 24(+24 -0)
ui/src/app/audit/audit-logs.tpl.html 23(+23 -0)
ui/src/app/audit/audit-log-table.directive.js 262(+262 -0)
ui/src/app/audit/audit-log-table.tpl.html 68(+68 -0)
ui/src/app/audit/index.js 31(+31 -0)
ui/src/app/common/types.constant.js 57(+57 -0)
ui/src/app/dashboard/dashboards.tpl.html 29(+20 -9)
ui/src/app/device/device.directive.js 27(+15 -12)
ui/src/app/device/devices.tpl.html 6(+6 -0)
ui/src/app/event/event-table.tpl.html 11(+9 -2)
ui/src/app/layout/index.js 2(+2 -0)
ui/src/app/locale/locale.constant.js 35(+34 -1)
ui/src/app/plugin/plugins.tpl.html 7(+7 -0)
ui/src/app/rule/rules.tpl.html 7(+7 -0)
ui/src/app/services/clipboard.service.js 128(+128 -0)
ui/src/app/services/menu.service.js 16(+16 -0)
ui/src/app/user/user.controller.js 2(+2 -0)
ui/src/app/user/users.tpl.html 22(+16 -6)
ui/src/scss/main.scss 13(+13 -0)
Details
diff --git a/application/src/main/data/upgrade/1.4.0/schema_update.cql b/application/src/main/data/upgrade/1.4.0/schema_update.cql
new file mode 100644
index 0000000..c5b656f
--- /dev/null
+++ b/application/src/main/data/upgrade/1.4.0/schema_update.cql
@@ -0,0 +1,89 @@
+--
+-- Copyright © 2016-2017 The Thingsboard Authors
+--
+-- Licensed under the Apache License, Version 2.0 (the "License");
+-- you may not use this file except in compliance with the License.
+-- You may obtain a copy of the License at
+--
+-- http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+
+CREATE TABLE IF NOT EXISTS thingsboard.audit_log_by_entity_id (
+ tenant_id timeuuid,
+ id timeuuid,
+ customer_id timeuuid,
+ entity_id timeuuid,
+ entity_type text,
+ entity_name text,
+ user_id timeuuid,
+ user_name text,
+ action_type text,
+ action_data text,
+ action_status text,
+ action_failure_details text,
+ PRIMARY KEY ((tenant_id, entity_id, entity_type), id)
+);
+
+CREATE TABLE IF NOT EXISTS thingsboard.audit_log_by_customer_id (
+ tenant_id timeuuid,
+ id timeuuid,
+ customer_id timeuuid,
+ entity_id timeuuid,
+ entity_type text,
+ entity_name text,
+ user_id timeuuid,
+ user_name text,
+ action_type text,
+ action_data text,
+ action_status text,
+ action_failure_details text,
+ PRIMARY KEY ((tenant_id, customer_id), id)
+);
+
+CREATE TABLE IF NOT EXISTS thingsboard.audit_log_by_user_id (
+ tenant_id timeuuid,
+ id timeuuid,
+ customer_id timeuuid,
+ entity_id timeuuid,
+ entity_type text,
+ entity_name text,
+ user_id timeuuid,
+ user_name text,
+ action_type text,
+ action_data text,
+ action_status text,
+ action_failure_details text,
+ PRIMARY KEY ((tenant_id, user_id), id)
+);
+
+
+
+CREATE TABLE IF NOT EXISTS thingsboard.audit_log_by_tenant_id (
+ tenant_id timeuuid,
+ id timeuuid,
+ partition bigint,
+ customer_id timeuuid,
+ entity_id timeuuid,
+ entity_type text,
+ entity_name text,
+ user_id timeuuid,
+ user_name text,
+ action_type text,
+ action_data text,
+ action_status text,
+ action_failure_details text,
+ PRIMARY KEY ((tenant_id, partition), id)
+);
+
+CREATE TABLE IF NOT EXISTS thingsboard.audit_log_by_tenant_id_partitions (
+ tenant_id timeuuid,
+ partition bigint,
+ PRIMARY KEY (( tenant_id ), partition)
+) WITH CLUSTERING ORDER BY ( partition ASC )
+AND compaction = { 'class' : 'LeveledCompactionStrategy' };
diff --git a/application/src/main/data/upgrade/1.4.0/schema_update.sql b/application/src/main/data/upgrade/1.4.0/schema_update.sql
new file mode 100644
index 0000000..b2e3a97
--- /dev/null
+++ b/application/src/main/data/upgrade/1.4.0/schema_update.sql
@@ -0,0 +1,31 @@
+--
+-- Copyright © 2016-2017 The Thingsboard Authors
+--
+-- Licensed under the Apache License, Version 2.0 (the "License");
+-- you may not use this file except in compliance with the License.
+-- You may obtain a copy of the License at
+--
+-- http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+
+CREATE TABLE IF NOT EXISTS audit_log (
+ id varchar(31) NOT NULL CONSTRAINT audit_log_pkey PRIMARY KEY,
+ tenant_id varchar(31),
+ customer_id varchar(31),
+ entity_id varchar(31),
+ entity_type varchar(255),
+ entity_name varchar(255),
+ user_id varchar(31),
+ user_name varchar(255),
+ action_type varchar(255),
+ action_data varchar(1000000),
+ action_status varchar(255),
+ action_failure_details varchar(1000000)
+);
+
diff --git a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
index f45028a..a42bcab 100644
--- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
+++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
@@ -40,6 +40,7 @@ import org.thingsboard.server.controller.plugin.PluginWebSocketMsgEndpoint;
import org.thingsboard.server.dao.alarm.AlarmService;
import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.attributes.AttributesService;
+import org.thingsboard.server.dao.audit.AuditLogService;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.event.EventService;
@@ -114,6 +115,9 @@ public class ActorSystemContext {
@Getter private RelationService relationService;
@Autowired
+ @Getter private AuditLogService auditLogService;
+
+ @Autowired
@Getter @Setter private PluginWebSocketMsgEndpoint wsMsgEndpoint;
@Value("${actors.session.sync.timeout}")
diff --git a/application/src/main/java/org/thingsboard/server/actors/plugin/PluginProcessingContext.java b/application/src/main/java/org/thingsboard/server/actors/plugin/PluginProcessingContext.java
index 3f0d39f..762bb64 100644
--- a/application/src/main/java/org/thingsboard/server/actors/plugin/PluginProcessingContext.java
+++ b/application/src/main/java/org/thingsboard/server/actors/plugin/PluginProcessingContext.java
@@ -26,6 +26,7 @@ import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.asset.Asset;
+import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.id.*;
import org.thingsboard.server.common.data.kv.AttributeKey;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
@@ -41,9 +42,7 @@ import org.thingsboard.server.extensions.api.device.DeviceAttributesEventNotific
import org.thingsboard.server.extensions.api.plugins.PluginApiCallSecurityContext;
import org.thingsboard.server.extensions.api.plugins.PluginCallback;
import org.thingsboard.server.extensions.api.plugins.PluginContext;
-import org.thingsboard.server.extensions.api.plugins.msg.PluginToRuleMsg;
-import org.thingsboard.server.extensions.api.plugins.msg.TimeoutMsg;
-import org.thingsboard.server.extensions.api.plugins.msg.ToDeviceRpcRequest;
+import org.thingsboard.server.extensions.api.plugins.msg.*;
import org.thingsboard.server.extensions.api.plugins.rpc.PluginRpcMsg;
import org.thingsboard.server.extensions.api.plugins.rpc.RpcMsg;
import org.thingsboard.server.extensions.api.plugins.ws.PluginWebsocketSessionRef;
@@ -197,6 +196,52 @@ public final class PluginProcessingContext implements PluginContext {
}
@Override
+ public void logAttributesUpdated(PluginApiCallSecurityContext ctx, EntityId entityId, String attributeType,
+ List<AttributeKvEntry> attributes, Exception e) {
+ pluginCtx.auditLogService.logEntityAction(
+ ctx.getTenantId(),
+ ctx.getCustomerId(),
+ ctx.getUserId(),
+ ctx.getUserName(),
+ (UUIDBased & EntityId)entityId,
+ null,
+ ActionType.ATTRIBUTES_UPDATED,
+ e,
+ attributeType,
+ attributes);
+ }
+
+ @Override
+ public void logAttributesDeleted(PluginApiCallSecurityContext ctx, EntityId entityId, String attributeType, List<String> keys, Exception e) {
+ pluginCtx.auditLogService.logEntityAction(
+ ctx.getTenantId(),
+ ctx.getCustomerId(),
+ ctx.getUserId(),
+ ctx.getUserName(),
+ (UUIDBased & EntityId)entityId,
+ null,
+ ActionType.ATTRIBUTES_DELETED,
+ e,
+ attributeType,
+ keys);
+ }
+
+ @Override
+ public void logAttributesRead(PluginApiCallSecurityContext ctx, EntityId entityId, String attributeType, List<String> keys, Exception e) {
+ pluginCtx.auditLogService.logEntityAction(
+ ctx.getTenantId(),
+ ctx.getCustomerId(),
+ ctx.getUserId(),
+ ctx.getUserName(),
+ (UUIDBased & EntityId)entityId,
+ null,
+ ActionType.ATTRIBUTES_READ,
+ e,
+ attributeType,
+ keys);
+ }
+
+ @Override
public void loadLatestTimeseries(final EntityId entityId, final Collection<String> keys, final PluginCallback<List<TsKvEntry>> callback) {
validate(entityId, new ValidationCallback(callback, ctx -> {
ListenableFuture<List<TsKvEntry>> rsListFuture = pluginCtx.tsService.findLatest(entityId, keys);
@@ -461,6 +506,29 @@ public final class PluginProcessingContext implements PluginContext {
}
@Override
+ public void logRpcRequest(PluginApiCallSecurityContext ctx, DeviceId deviceId, ToDeviceRpcRequestBody body, boolean oneWay, Optional<RpcError> rpcError, Exception e) {
+ String rpcErrorStr = "";
+ if (rpcError.isPresent()) {
+ rpcErrorStr = "RPC Error: " + rpcError.get().name();
+ }
+ String method = body.getMethod();
+ String params = body.getParams();
+ pluginCtx.auditLogService.logEntityAction(
+ ctx.getTenantId(),
+ ctx.getCustomerId(),
+ ctx.getUserId(),
+ ctx.getUserName(),
+ deviceId,
+ null,
+ ActionType.RPC_CALL,
+ e,
+ rpcErrorStr,
+ new Boolean(oneWay),
+ method,
+ params);
+ }
+
+ @Override
public void scheduleTimeoutMsg(TimeoutMsg msg) {
pluginCtx.scheduleTimeoutMsg(msg);
}
diff --git a/application/src/main/java/org/thingsboard/server/actors/plugin/SharedPluginProcessingContext.java b/application/src/main/java/org/thingsboard/server/actors/plugin/SharedPluginProcessingContext.java
index de859a2..a1cc835 100644
--- a/application/src/main/java/org/thingsboard/server/actors/plugin/SharedPluginProcessingContext.java
+++ b/application/src/main/java/org/thingsboard/server/actors/plugin/SharedPluginProcessingContext.java
@@ -27,6 +27,7 @@ import org.thingsboard.server.controller.plugin.PluginWebSocketMsgEndpoint;
import org.thingsboard.server.common.data.id.PluginId;
import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.attributes.AttributesService;
+import org.thingsboard.server.dao.audit.AuditLogService;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.plugin.PluginService;
@@ -63,6 +64,7 @@ public final class SharedPluginProcessingContext {
final ClusterRpcService rpcService;
final ClusterRoutingService routingService;
final RelationService relationService;
+ final AuditLogService auditLogService;
final PluginId pluginId;
final TenantId tenantId;
@@ -86,6 +88,7 @@ public final class SharedPluginProcessingContext {
this.customerService = sysContext.getCustomerService();
this.tenantService = sysContext.getTenantService();
this.relationService = sysContext.getRelationService();
+ this.auditLogService = sysContext.getAuditLogService();
}
public PluginId getPluginId() {
diff --git a/application/src/main/java/org/thingsboard/server/actors/rpc/BasicRpcSessionListener.java b/application/src/main/java/org/thingsboard/server/actors/rpc/BasicRpcSessionListener.java
index 6250897..72887cf 100644
--- a/application/src/main/java/org/thingsboard/server/actors/rpc/BasicRpcSessionListener.java
+++ b/application/src/main/java/org/thingsboard/server/actors/rpc/BasicRpcSessionListener.java
@@ -148,7 +148,7 @@ public class BasicRpcSessionListener implements GrpcSessionListener {
DeviceId deviceId = new DeviceId(toUUID(msg.getDeviceId()));
ToDeviceRpcRequestBody requestBody = new ToDeviceRpcRequestBody(msg.getMethod(), msg.getParams());
- ToDeviceRpcRequest request = new ToDeviceRpcRequest(toUUID(msg.getMsgId()), deviceTenantId, deviceId, msg.getOneway(), msg.getExpTime(), requestBody);
+ ToDeviceRpcRequest request = new ToDeviceRpcRequest(toUUID(msg.getMsgId()), null, deviceTenantId, deviceId, msg.getOneway(), msg.getExpTime(), requestBody);
return new ToDeviceRpcRequestPluginMsg(serverAddress, pluginId, pluginTenantId, request);
}
diff --git a/application/src/main/java/org/thingsboard/server/config/AuditLogLevelProperties.java b/application/src/main/java/org/thingsboard/server/config/AuditLogLevelProperties.java
new file mode 100644
index 0000000..ce622aa
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/config/AuditLogLevelProperties.java
@@ -0,0 +1,43 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.config;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.audit.ActionType;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@Configuration
+@ConfigurationProperties(prefix = "audit_log.logging_level")
+public class AuditLogLevelProperties {
+
+ private Map<String, String> mask = new HashMap<>();
+
+ public AuditLogLevelProperties() {
+ super();
+ }
+
+ public void setMask(Map<String, String> mask) {
+ this.mask = mask;
+ }
+
+ public Map<String, String> getMask() {
+ return this.mask;
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java
index 6755408..833c337 100644
--- a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java
+++ b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java
@@ -40,6 +40,7 @@ import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.web.cors.CorsUtils;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
+import org.thingsboard.server.dao.audit.AuditLogLevelFilter;
import org.thingsboard.server.exception.ThingsboardErrorResponseHandler;
import org.thingsboard.server.service.security.auth.rest.RestAuthenticationProvider;
import org.thingsboard.server.service.security.auth.rest.RestLoginProcessingFilter;
@@ -198,4 +199,9 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt
return new CorsFilter(source);
}
}
+
+ @Bean
+ public AuditLogLevelFilter auditLogLevelFilter(@Autowired AuditLogLevelProperties auditLogLevelProperties) {
+ return new AuditLogLevelFilter(auditLogLevelProperties.getMask());
+ }
}
diff --git a/application/src/main/java/org/thingsboard/server/controller/AssetController.java b/application/src/main/java/org/thingsboard/server/controller/AssetController.java
index 194b7d8..4790b75 100644
--- a/application/src/main/java/org/thingsboard/server/controller/AssetController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/AssetController.java
@@ -21,7 +21,9 @@ import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.EntitySubtype;
+import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.asset.Asset;
+import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId;
@@ -73,8 +75,16 @@ public class AssetController extends BaseController {
checkCustomerId(asset.getCustomerId());
}
}
- return checkNotNull(assetService.saveAsset(asset));
+ Asset savedAsset = checkNotNull(assetService.saveAsset(asset));
+
+ logEntityAction(savedAsset.getId(), savedAsset,
+ savedAsset.getCustomerId(),
+ asset.getId() == null ? ActionType.ADDED : ActionType.UPDATED, null);
+
+ return savedAsset;
} catch (Exception e) {
+ logEntityAction(emptyId(EntityType.ASSET), asset,
+ null, asset.getId() == null ? ActionType.ADDED : ActionType.UPDATED, e);
throw handleException(e);
}
}
@@ -86,9 +96,18 @@ public class AssetController extends BaseController {
checkParameter(ASSET_ID, strAssetId);
try {
AssetId assetId = new AssetId(toUUID(strAssetId));
- checkAssetId(assetId);
+ Asset asset = checkAssetId(assetId);
assetService.deleteAsset(assetId);
+
+ logEntityAction(assetId, asset,
+ asset.getCustomerId(),
+ ActionType.DELETED, null, strAssetId);
+
} catch (Exception e) {
+ logEntityAction(emptyId(EntityType.ASSET),
+ null,
+ null,
+ ActionType.DELETED, e, strAssetId);
throw handleException(e);
}
}
@@ -102,13 +121,24 @@ public class AssetController extends BaseController {
checkParameter(ASSET_ID, strAssetId);
try {
CustomerId customerId = new CustomerId(toUUID(strCustomerId));
- checkCustomerId(customerId);
+ Customer customer = checkCustomerId(customerId);
AssetId assetId = new AssetId(toUUID(strAssetId));
checkAssetId(assetId);
- return checkNotNull(assetService.assignAssetToCustomer(assetId, customerId));
+ Asset savedAsset = checkNotNull(assetService.assignAssetToCustomer(assetId, customerId));
+
+ logEntityAction(assetId, savedAsset,
+ savedAsset.getCustomerId(),
+ ActionType.ASSIGNED_TO_CUSTOMER, null, strAssetId, strCustomerId, customer.getName());
+
+ return savedAsset;
} catch (Exception e) {
+
+ logEntityAction(emptyId(EntityType.ASSET), null,
+ null,
+ ActionType.ASSIGNED_TO_CUSTOMER, e, strAssetId, strCustomerId);
+
throw handleException(e);
}
}
@@ -124,8 +154,22 @@ public class AssetController extends BaseController {
if (asset.getCustomerId() == null || asset.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) {
throw new IncorrectParameterException("Asset isn't assigned to any customer!");
}
- return checkNotNull(assetService.unassignAssetFromCustomer(assetId));
+
+ Customer customer = checkCustomerId(asset.getCustomerId());
+
+ Asset savedAsset = checkNotNull(assetService.unassignAssetFromCustomer(assetId));
+
+ logEntityAction(assetId, asset,
+ asset.getCustomerId(),
+ ActionType.UNASSIGNED_FROM_CUSTOMER, null, strAssetId, customer.getId().toString(), customer.getName());
+
+ return savedAsset;
} catch (Exception e) {
+
+ logEntityAction(emptyId(EntityType.ASSET), null,
+ null,
+ ActionType.UNASSIGNED_FROM_CUSTOMER, e, strAssetId);
+
throw handleException(e);
}
}
@@ -139,8 +183,19 @@ public class AssetController extends BaseController {
AssetId assetId = new AssetId(toUUID(strAssetId));
Asset asset = checkAssetId(assetId);
Customer publicCustomer = customerService.findOrCreatePublicCustomer(asset.getTenantId());
- return checkNotNull(assetService.assignAssetToCustomer(assetId, publicCustomer.getId()));
+ Asset savedAsset = checkNotNull(assetService.assignAssetToCustomer(assetId, publicCustomer.getId()));
+
+ logEntityAction(assetId, savedAsset,
+ savedAsset.getCustomerId(),
+ ActionType.ASSIGNED_TO_CUSTOMER, null, strAssetId, publicCustomer.getId().toString(), publicCustomer.getName());
+
+ return savedAsset;
} catch (Exception e) {
+
+ logEntityAction(emptyId(EntityType.ASSET), null,
+ null,
+ ActionType.ASSIGNED_TO_CUSTOMER, e, strAssetId);
+
throw handleException(e);
}
}
diff --git a/application/src/main/java/org/thingsboard/server/controller/AuditLogController.java b/application/src/main/java/org/thingsboard/server/controller/AuditLogController.java
new file mode 100644
index 0000000..4d1d50e
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/controller/AuditLogController.java
@@ -0,0 +1,114 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.controller;
+
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+import org.thingsboard.server.common.data.audit.AuditLog;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.EntityIdFactory;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.id.UserId;
+import org.thingsboard.server.common.data.page.TimePageData;
+import org.thingsboard.server.common.data.page.TimePageLink;
+import org.thingsboard.server.exception.ThingsboardException;
+
+import java.util.UUID;
+
+@RestController
+@RequestMapping("/api")
+public class AuditLogController extends BaseController {
+
+ @PreAuthorize("hasAuthority('TENANT_ADMIN')")
+ @RequestMapping(value = "/audit/logs/customer/{customerId}", params = {"limit"}, method = RequestMethod.GET)
+ @ResponseBody
+ public TimePageData<AuditLog> getAuditLogsByCustomerId(
+ @PathVariable("customerId") String strCustomerId,
+ @RequestParam int limit,
+ @RequestParam(required = false) Long startTime,
+ @RequestParam(required = false) Long endTime,
+ @RequestParam(required = false, defaultValue = "false") boolean ascOrder,
+ @RequestParam(required = false) String offset) throws ThingsboardException {
+ try {
+ checkParameter("CustomerId", strCustomerId);
+ TenantId tenantId = getCurrentUser().getTenantId();
+ TimePageLink pageLink = createPageLink(limit, startTime, endTime, ascOrder, offset);
+ return checkNotNull(auditLogService.findAuditLogsByTenantIdAndCustomerId(tenantId, new CustomerId(UUID.fromString(strCustomerId)), pageLink));
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @PreAuthorize("hasAuthority('TENANT_ADMIN')")
+ @RequestMapping(value = "/audit/logs/user/{userId}", params = {"limit"}, method = RequestMethod.GET)
+ @ResponseBody
+ public TimePageData<AuditLog> getAuditLogsByUserId(
+ @PathVariable("userId") String strUserId,
+ @RequestParam int limit,
+ @RequestParam(required = false) Long startTime,
+ @RequestParam(required = false) Long endTime,
+ @RequestParam(required = false, defaultValue = "false") boolean ascOrder,
+ @RequestParam(required = false) String offset) throws ThingsboardException {
+ try {
+ checkParameter("UserId", strUserId);
+ TenantId tenantId = getCurrentUser().getTenantId();
+ TimePageLink pageLink = createPageLink(limit, startTime, endTime, ascOrder, offset);
+ return checkNotNull(auditLogService.findAuditLogsByTenantIdAndUserId(tenantId, new UserId(UUID.fromString(strUserId)), pageLink));
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @PreAuthorize("hasAuthority('TENANT_ADMIN')")
+ @RequestMapping(value = "/audit/logs/entity/{entityType}/{entityId}", params = {"limit"}, method = RequestMethod.GET)
+ @ResponseBody
+ public TimePageData<AuditLog> getAuditLogsByEntityId(
+ @PathVariable("entityType") String strEntityType,
+ @PathVariable("entityId") String strEntityId,
+ @RequestParam int limit,
+ @RequestParam(required = false) Long startTime,
+ @RequestParam(required = false) Long endTime,
+ @RequestParam(required = false, defaultValue = "false") boolean ascOrder,
+ @RequestParam(required = false) String offset) throws ThingsboardException {
+ try {
+ checkParameter("EntityId", strEntityId);
+ checkParameter("EntityType", strEntityType);
+ TenantId tenantId = getCurrentUser().getTenantId();
+ TimePageLink pageLink = createPageLink(limit, startTime, endTime, ascOrder, offset);
+ return checkNotNull(auditLogService.findAuditLogsByTenantIdAndEntityId(tenantId, EntityIdFactory.getByTypeAndId(strEntityType, strEntityId), pageLink));
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @PreAuthorize("hasAuthority('TENANT_ADMIN')")
+ @RequestMapping(value = "/audit/logs", params = {"limit"}, method = RequestMethod.GET)
+ @ResponseBody
+ public TimePageData<AuditLog> getAuditLogs(
+ @RequestParam int limit,
+ @RequestParam(required = false) Long startTime,
+ @RequestParam(required = false) Long endTime,
+ @RequestParam(required = false, defaultValue = "false") boolean ascOrder,
+ @RequestParam(required = false) String offset) throws ThingsboardException {
+ try {
+ TenantId tenantId = getCurrentUser().getTenantId();
+ TimePageLink pageLink = createPageLink(limit, startTime, endTime, ascOrder, offset);
+ return checkNotNull(auditLogService.findAuditLogsByTenantId(tenantId, pageLink));
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/controller/BaseController.java b/application/src/main/java/org/thingsboard/server/controller/BaseController.java
index f581cc2..28489ca 100644
--- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java
@@ -15,9 +15,12 @@
*/
package org.thingsboard.server.controller;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.ExceptionHandler;
@@ -27,6 +30,8 @@ import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmId;
import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.asset.Asset;
+import org.thingsboard.server.common.data.audit.ActionStatus;
+import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.id.*;
import org.thingsboard.server.common.data.page.TextPageLink;
import org.thingsboard.server.common.data.page.TimePageLink;
@@ -39,6 +44,7 @@ import org.thingsboard.server.common.data.widget.WidgetType;
import org.thingsboard.server.common.data.widget.WidgetsBundle;
import org.thingsboard.server.dao.alarm.AlarmService;
import org.thingsboard.server.dao.asset.AssetService;
+import org.thingsboard.server.dao.audit.AuditLogService;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.dashboard.DashboardService;
import org.thingsboard.server.dao.device.DeviceCredentialsService;
@@ -72,6 +78,7 @@ public abstract class BaseController {
public static final String INCORRECT_TENANT_ID = "Incorrect tenantId ";
public static final String YOU_DON_T_HAVE_PERMISSION_TO_PERFORM_THIS_OPERATION = "You don't have permission to perform this operation!";
+
@Autowired
private ThingsboardErrorResponseHandler errorResponseHandler;
@@ -117,6 +124,9 @@ public abstract class BaseController {
@Autowired
protected RelationService relationService;
+ @Autowired
+ protected AuditLogService auditLogService;
+
@ExceptionHandler(ThingsboardException.class)
public void handleThingsboardException(ThingsboardException ex, HttpServletResponse response) {
errorResponseHandler.handle(ex, response);
@@ -540,4 +550,20 @@ public abstract class BaseController {
serverPort);
return baseUrl;
}
+
+ protected <I extends UUIDBased & EntityId> I emptyId(EntityType entityType) {
+ return (I)EntityIdFactory.getByTypeAndUuid(entityType, ModelConstants.NULL_UUID);
+ }
+
+ protected <E extends BaseData<I> & HasName,
+ I extends UUIDBased & EntityId> void logEntityAction(I entityId, E entity, CustomerId customerId,
+ ActionType actionType, Exception e, Object... additionalInfo) throws ThingsboardException {
+ User user = getCurrentUser();
+ if (customerId == null || customerId.isNullUid()) {
+ customerId = user.getCustomerId();
+ }
+ auditLogService.logEntityAction(user.getTenantId(), customerId, user.getId(), user.getName(), entityId, entity, actionType, e, additionalInfo);
+ }
+
+
}
diff --git a/application/src/main/java/org/thingsboard/server/controller/CustomerController.java b/application/src/main/java/org/thingsboard/server/controller/CustomerController.java
index d87ea5c..ee3fbfc 100644
--- a/application/src/main/java/org/thingsboard/server/controller/CustomerController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/CustomerController.java
@@ -22,6 +22,8 @@ import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import org.thingsboard.server.common.data.Customer;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.TextPageData;
@@ -86,8 +88,18 @@ public class CustomerController extends BaseController {
public Customer saveCustomer(@RequestBody Customer customer) throws ThingsboardException {
try {
customer.setTenantId(getCurrentUser().getTenantId());
- return checkNotNull(customerService.saveCustomer(customer));
+ Customer savedCustomer = checkNotNull(customerService.saveCustomer(customer));
+
+ logEntityAction(savedCustomer.getId(), savedCustomer,
+ savedCustomer.getId(),
+ customer.getId() == null ? ActionType.ADDED : ActionType.UPDATED, null);
+
+ return savedCustomer;
} catch (Exception e) {
+
+ logEntityAction(emptyId(EntityType.CUSTOMER), customer,
+ null, customer.getId() == null ? ActionType.ADDED : ActionType.UPDATED, e);
+
throw handleException(e);
}
}
@@ -99,9 +111,20 @@ public class CustomerController extends BaseController {
checkParameter(CUSTOMER_ID, strCustomerId);
try {
CustomerId customerId = new CustomerId(toUUID(strCustomerId));
- checkCustomerId(customerId);
+ Customer customer = checkCustomerId(customerId);
customerService.deleteCustomer(customerId);
+
+ logEntityAction(customerId, customer,
+ customer.getId(),
+ ActionType.DELETED, null, strCustomerId);
+
} catch (Exception e) {
+
+ logEntityAction(emptyId(EntityType.CUSTOMER),
+ null,
+ null,
+ ActionType.DELETED, e, strCustomerId);
+
throw handleException(e);
}
}
diff --git a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java
index 7bc05d7..34320b1 100644
--- a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java
@@ -21,6 +21,8 @@ import org.springframework.web.bind.annotation.*;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.common.data.DashboardInfo;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.TenantId;
@@ -75,8 +77,17 @@ public class DashboardController extends BaseController {
public Dashboard saveDashboard(@RequestBody Dashboard dashboard) throws ThingsboardException {
try {
dashboard.setTenantId(getCurrentUser().getTenantId());
- return checkNotNull(dashboardService.saveDashboard(dashboard));
+ Dashboard savedDashboard = checkNotNull(dashboardService.saveDashboard(dashboard));
+
+ logEntityAction(savedDashboard.getId(), savedDashboard,
+ savedDashboard.getCustomerId(),
+ dashboard.getId() == null ? ActionType.ADDED : ActionType.UPDATED, null);
+
+ return savedDashboard;
} catch (Exception e) {
+ logEntityAction(emptyId(EntityType.DASHBOARD), dashboard,
+ null, dashboard.getId() == null ? ActionType.ADDED : ActionType.UPDATED, e);
+
throw handleException(e);
}
}
@@ -88,9 +99,20 @@ public class DashboardController extends BaseController {
checkParameter(DASHBOARD_ID, strDashboardId);
try {
DashboardId dashboardId = new DashboardId(toUUID(strDashboardId));
- checkDashboardId(dashboardId);
+ Dashboard dashboard = checkDashboardId(dashboardId);
dashboardService.deleteDashboard(dashboardId);
+
+ logEntityAction(dashboardId, dashboard,
+ dashboard.getCustomerId(),
+ ActionType.DELETED, null, strDashboardId);
+
} catch (Exception e) {
+
+ logEntityAction(emptyId(EntityType.DASHBOARD),
+ null,
+ null,
+ ActionType.DELETED, e, strDashboardId);
+
throw handleException(e);
}
}
@@ -104,13 +126,25 @@ public class DashboardController extends BaseController {
checkParameter(DASHBOARD_ID, strDashboardId);
try {
CustomerId customerId = new CustomerId(toUUID(strCustomerId));
- checkCustomerId(customerId);
+ Customer customer = checkCustomerId(customerId);
DashboardId dashboardId = new DashboardId(toUUID(strDashboardId));
checkDashboardId(dashboardId);
- return checkNotNull(dashboardService.assignDashboardToCustomer(dashboardId, customerId));
+ Dashboard savedDashboard = checkNotNull(dashboardService.assignDashboardToCustomer(dashboardId, customerId));
+
+ logEntityAction(dashboardId, savedDashboard,
+ savedDashboard.getCustomerId(),
+ ActionType.ASSIGNED_TO_CUSTOMER, null, strDashboardId, strCustomerId, customer.getName());
+
+
+ return savedDashboard;
} catch (Exception e) {
+
+ logEntityAction(emptyId(EntityType.DASHBOARD), null,
+ null,
+ ActionType.ASSIGNED_TO_CUSTOMER, e, strDashboardId, strCustomerId);
+
throw handleException(e);
}
}
@@ -126,8 +160,22 @@ public class DashboardController extends BaseController {
if (dashboard.getCustomerId() == null || dashboard.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) {
throw new IncorrectParameterException("Dashboard isn't assigned to any customer!");
}
- return checkNotNull(dashboardService.unassignDashboardFromCustomer(dashboardId));
+
+ Customer customer = checkCustomerId(dashboard.getCustomerId());
+
+ Dashboard savedDashboard = checkNotNull(dashboardService.unassignDashboardFromCustomer(dashboardId));
+
+ logEntityAction(dashboardId, dashboard,
+ dashboard.getCustomerId(),
+ ActionType.UNASSIGNED_FROM_CUSTOMER, null, strDashboardId, customer.getId().toString(), customer.getName());
+
+ return savedDashboard;
} catch (Exception e) {
+
+ logEntityAction(emptyId(EntityType.DASHBOARD), null,
+ null,
+ ActionType.UNASSIGNED_FROM_CUSTOMER, e, strDashboardId);
+
throw handleException(e);
}
}
@@ -141,8 +189,19 @@ public class DashboardController extends BaseController {
DashboardId dashboardId = new DashboardId(toUUID(strDashboardId));
Dashboard dashboard = checkDashboardId(dashboardId);
Customer publicCustomer = customerService.findOrCreatePublicCustomer(dashboard.getTenantId());
- return checkNotNull(dashboardService.assignDashboardToCustomer(dashboardId, publicCustomer.getId()));
+ Dashboard savedDashboard = checkNotNull(dashboardService.assignDashboardToCustomer(dashboardId, publicCustomer.getId()));
+
+ logEntityAction(dashboardId, savedDashboard,
+ savedDashboard.getCustomerId(),
+ ActionType.ASSIGNED_TO_CUSTOMER, null, strDashboardId, publicCustomer.getId().toString(), publicCustomer.getName());
+
+ return savedDashboard;
} catch (Exception e) {
+
+ logEntityAction(emptyId(EntityType.DASHBOARD), null,
+ null,
+ ActionType.ASSIGNED_TO_CUSTOMER, e, strDashboardId);
+
throw handleException(e);
}
}
diff --git a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java
index eeb10c8..18ddfe5 100644
--- a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java
@@ -22,6 +22,10 @@ import org.springframework.web.bind.annotation.*;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntitySubtype;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.audit.ActionStatus;
+import org.thingsboard.server.common.data.audit.ActionType;
+import org.thingsboard.server.common.data.device.DeviceSearchQuery;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.TenantId;
@@ -29,7 +33,6 @@ import org.thingsboard.server.common.data.page.TextPageData;
import org.thingsboard.server.common.data.page.TextPageLink;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.security.DeviceCredentials;
-import org.thingsboard.server.common.data.device.DeviceSearchQuery;
import org.thingsboard.server.dao.exception.IncorrectParameterException;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.exception.ThingsboardErrorCode;
@@ -75,14 +78,22 @@ public class DeviceController extends BaseController {
}
}
Device savedDevice = checkNotNull(deviceService.saveDevice(device));
+
actorService
.onDeviceNameOrTypeUpdate(
savedDevice.getTenantId(),
savedDevice.getId(),
savedDevice.getName(),
savedDevice.getType());
+
+ logEntityAction(savedDevice.getId(), savedDevice,
+ savedDevice.getCustomerId(),
+ device.getId() == null ? ActionType.ADDED : ActionType.UPDATED, null);
+
return savedDevice;
} catch (Exception e) {
+ logEntityAction(emptyId(EntityType.DEVICE), device,
+ null, device.getId() == null ? ActionType.ADDED : ActionType.UPDATED, e);
throw handleException(e);
}
}
@@ -94,9 +105,18 @@ public class DeviceController extends BaseController {
checkParameter(DEVICE_ID, strDeviceId);
try {
DeviceId deviceId = new DeviceId(toUUID(strDeviceId));
- checkDeviceId(deviceId);
+ Device device = checkDeviceId(deviceId);
deviceService.deleteDevice(deviceId);
+
+ logEntityAction(deviceId, device,
+ device.getCustomerId(),
+ ActionType.DELETED, null, strDeviceId);
+
} catch (Exception e) {
+ logEntityAction(emptyId(EntityType.DEVICE),
+ null,
+ null,
+ ActionType.DELETED, e, strDeviceId);
throw handleException(e);
}
}
@@ -110,13 +130,22 @@ public class DeviceController extends BaseController {
checkParameter(DEVICE_ID, strDeviceId);
try {
CustomerId customerId = new CustomerId(toUUID(strCustomerId));
- checkCustomerId(customerId);
+ Customer customer = checkCustomerId(customerId);
DeviceId deviceId = new DeviceId(toUUID(strDeviceId));
checkDeviceId(deviceId);
- return checkNotNull(deviceService.assignDeviceToCustomer(deviceId, customerId));
+ Device savedDevice = checkNotNull(deviceService.assignDeviceToCustomer(deviceId, customerId));
+
+ logEntityAction(deviceId, savedDevice,
+ savedDevice.getCustomerId(),
+ ActionType.ASSIGNED_TO_CUSTOMER, null, strDeviceId, strCustomerId, customer.getName());
+
+ return savedDevice;
} catch (Exception e) {
+ logEntityAction(emptyId(EntityType.DEVICE), null,
+ null,
+ ActionType.ASSIGNED_TO_CUSTOMER, e, strDeviceId, strCustomerId);
throw handleException(e);
}
}
@@ -132,8 +161,19 @@ public class DeviceController extends BaseController {
if (device.getCustomerId() == null || device.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) {
throw new IncorrectParameterException("Device isn't assigned to any customer!");
}
- return checkNotNull(deviceService.unassignDeviceFromCustomer(deviceId));
+ Customer customer = checkCustomerId(device.getCustomerId());
+
+ Device savedDevice = checkNotNull(deviceService.unassignDeviceFromCustomer(deviceId));
+
+ logEntityAction(deviceId, device,
+ device.getCustomerId(),
+ ActionType.UNASSIGNED_FROM_CUSTOMER, null, strDeviceId, customer.getId().toString(), customer.getName());
+
+ return savedDevice;
} catch (Exception e) {
+ logEntityAction(emptyId(EntityType.DEVICE), null,
+ null,
+ ActionType.UNASSIGNED_FROM_CUSTOMER, e, strDeviceId);
throw handleException(e);
}
}
@@ -147,8 +187,17 @@ public class DeviceController extends BaseController {
DeviceId deviceId = new DeviceId(toUUID(strDeviceId));
Device device = checkDeviceId(deviceId);
Customer publicCustomer = customerService.findOrCreatePublicCustomer(device.getTenantId());
- return checkNotNull(deviceService.assignDeviceToCustomer(deviceId, publicCustomer.getId()));
+ Device savedDevice = checkNotNull(deviceService.assignDeviceToCustomer(deviceId, publicCustomer.getId()));
+
+ logEntityAction(deviceId, savedDevice,
+ savedDevice.getCustomerId(),
+ ActionType.ASSIGNED_TO_CUSTOMER, null, strDeviceId, publicCustomer.getId().toString(), publicCustomer.getName());
+
+ return savedDevice;
} catch (Exception e) {
+ logEntityAction(emptyId(EntityType.DEVICE), null,
+ null,
+ ActionType.ASSIGNED_TO_CUSTOMER, e, strDeviceId);
throw handleException(e);
}
}
@@ -160,9 +209,16 @@ public class DeviceController extends BaseController {
checkParameter(DEVICE_ID, strDeviceId);
try {
DeviceId deviceId = new DeviceId(toUUID(strDeviceId));
- checkDeviceId(deviceId);
- return checkNotNull(deviceCredentialsService.findDeviceCredentialsByDeviceId(deviceId));
+ Device device = checkDeviceId(deviceId);
+ DeviceCredentials deviceCredentials = checkNotNull(deviceCredentialsService.findDeviceCredentialsByDeviceId(deviceId));
+ logEntityAction(deviceId, device,
+ device.getCustomerId(),
+ ActionType.CREDENTIALS_READ, null, strDeviceId);
+ return deviceCredentials;
} catch (Exception e) {
+ logEntityAction(emptyId(EntityType.DEVICE), null,
+ null,
+ ActionType.CREDENTIALS_READ, e, strDeviceId);
throw handleException(e);
}
}
@@ -173,11 +229,17 @@ public class DeviceController extends BaseController {
public DeviceCredentials saveDeviceCredentials(@RequestBody DeviceCredentials deviceCredentials) throws ThingsboardException {
checkNotNull(deviceCredentials);
try {
- checkDeviceId(deviceCredentials.getDeviceId());
+ Device device = checkDeviceId(deviceCredentials.getDeviceId());
DeviceCredentials result = checkNotNull(deviceCredentialsService.updateDeviceCredentials(deviceCredentials));
actorService.onCredentialsUpdate(getCurrentUser().getTenantId(), deviceCredentials.getDeviceId());
+ logEntityAction(device.getId(), device,
+ device.getCustomerId(),
+ ActionType.CREDENTIALS_UPDATED, null, deviceCredentials);
return result;
} catch (Exception e) {
+ logEntityAction(emptyId(EntityType.DEVICE), null,
+ null,
+ ActionType.CREDENTIALS_UPDATED, e, deviceCredentials);
throw handleException(e);
}
}
@@ -306,5 +368,4 @@ public class DeviceController extends BaseController {
throw handleException(e);
}
}
-
}
diff --git a/application/src/main/java/org/thingsboard/server/controller/plugin/PluginApiController.java b/application/src/main/java/org/thingsboard/server/controller/plugin/PluginApiController.java
index 1789da6..145b046 100644
--- a/application/src/main/java/org/thingsboard/server/controller/plugin/PluginApiController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/plugin/PluginApiController.java
@@ -30,6 +30,7 @@ import org.springframework.web.context.request.async.DeferredResult;
import org.thingsboard.server.actors.service.ActorService;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.plugin.PluginMetaData;
import org.thingsboard.server.controller.BaseController;
import org.thingsboard.server.dao.model.ModelConstants;
@@ -68,7 +69,10 @@ public class PluginApiController extends BaseController {
if(tenantId != null && ModelConstants.NULL_UUID.equals(tenantId.getId())){
tenantId = null;
}
- PluginApiCallSecurityContext securityCtx = new PluginApiCallSecurityContext(pluginMd.getTenantId(), pluginMd.getId(), tenantId, customerId);
+ UserId userId = getCurrentUser().getId();
+ String userName = getCurrentUser().getName();
+ PluginApiCallSecurityContext securityCtx = new PluginApiCallSecurityContext(pluginMd.getTenantId(), pluginMd.getId(),
+ tenantId, customerId, userId, userName);
actorService.process(new BasicPluginRestMsg(securityCtx, new RestRequest(requestEntity, request), result));
} else {
result.setResult(new ResponseEntity<>(HttpStatus.FORBIDDEN));
diff --git a/application/src/main/java/org/thingsboard/server/controller/plugin/PluginWebSocketHandler.java b/application/src/main/java/org/thingsboard/server/controller/plugin/PluginWebSocketHandler.java
index 727fd30..1312373 100644
--- a/application/src/main/java/org/thingsboard/server/controller/plugin/PluginWebSocketHandler.java
+++ b/application/src/main/java/org/thingsboard/server/controller/plugin/PluginWebSocketHandler.java
@@ -28,6 +28,7 @@ import org.springframework.context.annotation.Lazy;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.server.actors.service.ActorService;
+import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.config.WebSocketConfiguration;
import org.thingsboard.server.extensions.api.plugins.PluginConstants;
import org.thingsboard.server.service.security.model.SecurityUser;
@@ -151,8 +152,10 @@ public class PluginWebSocketHandler extends TextWebSocketHandler implements Plug
TenantId tenantId = currentUser.getTenantId();
CustomerId customerId = currentUser.getCustomerId();
if (PluginApiController.validatePluginAccess(pluginMd, tenantId, customerId)) {
+ UserId userId = currentUser.getId();
+ String userName = currentUser.getName();
PluginApiCallSecurityContext securityCtx = new PluginApiCallSecurityContext(pluginMd.getTenantId(), pluginMd.getId(), tenantId,
- currentUser.getCustomerId());
+ currentUser.getCustomerId(), userId, userName);
return new BasicPluginWebsocketSessionRef(UUID.randomUUID().toString(), securityCtx, session.getUri(), session.getAttributes(),
session.getLocalAddress(), session.getRemoteAddress());
} else {
diff --git a/application/src/main/java/org/thingsboard/server/controller/PluginController.java b/application/src/main/java/org/thingsboard/server/controller/PluginController.java
index 191fc6b..6217fef 100644
--- a/application/src/main/java/org/thingsboard/server/controller/PluginController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/PluginController.java
@@ -18,6 +18,8 @@ package org.thingsboard.server.controller;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.id.PluginId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.TextPageData;
@@ -71,8 +73,17 @@ public class PluginController extends BaseController {
PluginMetaData plugin = checkNotNull(pluginService.savePlugin(source));
actorService.onPluginStateChange(plugin.getTenantId(), plugin.getId(),
created ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED);
+
+ logEntityAction(plugin.getId(), plugin,
+ null,
+ created ? ActionType.ADDED : ActionType.UPDATED, null);
+
return plugin;
} catch (Exception e) {
+
+ logEntityAction(emptyId(EntityType.PLUGIN), source,
+ null, source.getId() == null ? ActionType.ADDED : ActionType.UPDATED, e);
+
throw handleException(e);
}
}
@@ -87,7 +98,18 @@ public class PluginController extends BaseController {
PluginMetaData plugin = checkPlugin(pluginService.findPluginById(pluginId));
pluginService.activatePluginById(pluginId);
actorService.onPluginStateChange(plugin.getTenantId(), plugin.getId(), ComponentLifecycleEvent.ACTIVATED);
+
+ logEntityAction(plugin.getId(), plugin,
+ null,
+ ActionType.ACTIVATED, null, strPluginId);
+
} catch (Exception e) {
+
+ logEntityAction(emptyId(EntityType.PLUGIN),
+ null,
+ null,
+ ActionType.ACTIVATED, e, strPluginId);
+
throw handleException(e);
}
}
@@ -102,7 +124,18 @@ public class PluginController extends BaseController {
PluginMetaData plugin = checkPlugin(pluginService.findPluginById(pluginId));
pluginService.suspendPluginById(pluginId);
actorService.onPluginStateChange(plugin.getTenantId(), plugin.getId(), ComponentLifecycleEvent.SUSPENDED);
+
+ logEntityAction(plugin.getId(), plugin,
+ null,
+ ActionType.SUSPENDED, null, strPluginId);
+
} catch (Exception e) {
+
+ logEntityAction(emptyId(EntityType.PLUGIN),
+ null,
+ null,
+ ActionType.SUSPENDED, e, strPluginId);
+
throw handleException(e);
}
}
@@ -189,7 +222,16 @@ public class PluginController extends BaseController {
PluginMetaData plugin = checkPlugin(pluginService.findPluginById(pluginId));
pluginService.deletePluginById(pluginId);
actorService.onPluginStateChange(plugin.getTenantId(), plugin.getId(), ComponentLifecycleEvent.DELETED);
+
+ logEntityAction(pluginId, plugin,
+ null,
+ ActionType.DELETED, null, strPluginId);
+
} catch (Exception e) {
+ logEntityAction(emptyId(EntityType.PLUGIN),
+ null,
+ null,
+ ActionType.DELETED, e, strPluginId);
throw handleException(e);
}
}
diff --git a/application/src/main/java/org/thingsboard/server/controller/RuleController.java b/application/src/main/java/org/thingsboard/server/controller/RuleController.java
index 84d1c8f..83e3b4b 100644
--- a/application/src/main/java/org/thingsboard/server/controller/RuleController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/RuleController.java
@@ -18,6 +18,8 @@ package org.thingsboard.server.controller;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.id.RuleId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.TextPageData;
@@ -73,8 +75,17 @@ public class RuleController extends BaseController {
RuleMetaData rule = checkNotNull(ruleService.saveRule(source));
actorService.onRuleStateChange(rule.getTenantId(), rule.getId(),
created ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED);
+
+ logEntityAction(rule.getId(), rule,
+ null,
+ created ? ActionType.ADDED : ActionType.UPDATED, null);
+
return rule;
} catch (Exception e) {
+
+ logEntityAction(emptyId(EntityType.RULE), source,
+ null, source.getId() == null ? ActionType.ADDED : ActionType.UPDATED, e);
+
throw handleException(e);
}
}
@@ -89,7 +100,18 @@ public class RuleController extends BaseController {
RuleMetaData rule = checkRule(ruleService.findRuleById(ruleId));
ruleService.activateRuleById(ruleId);
actorService.onRuleStateChange(rule.getTenantId(), rule.getId(), ComponentLifecycleEvent.ACTIVATED);
+
+ logEntityAction(rule.getId(), rule,
+ null,
+ ActionType.ACTIVATED, null, strRuleId);
+
} catch (Exception e) {
+
+ logEntityAction(emptyId(EntityType.RULE),
+ null,
+ null,
+ ActionType.ACTIVATED, e, strRuleId);
+
throw handleException(e);
}
}
@@ -104,7 +126,18 @@ public class RuleController extends BaseController {
RuleMetaData rule = checkRule(ruleService.findRuleById(ruleId));
ruleService.suspendRuleById(ruleId);
actorService.onRuleStateChange(rule.getTenantId(), rule.getId(), ComponentLifecycleEvent.SUSPENDED);
+
+ logEntityAction(rule.getId(), rule,
+ null,
+ ActionType.SUSPENDED, null, strRuleId);
+
} catch (Exception e) {
+
+ logEntityAction(emptyId(EntityType.RULE),
+ null,
+ null,
+ ActionType.SUSPENDED, e, strRuleId);
+
throw handleException(e);
}
}
@@ -187,7 +220,18 @@ public class RuleController extends BaseController {
RuleMetaData rule = checkRule(ruleService.findRuleById(ruleId));
ruleService.deleteRuleById(ruleId);
actorService.onRuleStateChange(rule.getTenantId(), rule.getId(), ComponentLifecycleEvent.DELETED);
+
+ logEntityAction(ruleId, rule,
+ null,
+ ActionType.DELETED, null, strRuleId);
+
} catch (Exception e) {
+
+ logEntityAction(emptyId(EntityType.RULE),
+ null,
+ null,
+ ActionType.DELETED, e, strRuleId);
+
throw handleException(e);
}
}
diff --git a/application/src/main/java/org/thingsboard/server/controller/UserController.java b/application/src/main/java/org/thingsboard/server/controller/UserController.java
index 05fca0b..95f5fdd 100644
--- a/application/src/main/java/org/thingsboard/server/controller/UserController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/UserController.java
@@ -19,7 +19,9 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
+import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.User;
+import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
@@ -92,8 +94,17 @@ public class UserController extends BaseController {
throw e;
}
}
+
+ logEntityAction(savedUser.getId(), savedUser,
+ savedUser.getCustomerId(),
+ user.getId() == null ? ActionType.ADDED : ActionType.UPDATED, null);
+
return savedUser;
} catch (Exception e) {
+
+ logEntityAction(emptyId(EntityType.USER), user,
+ null, user.getId() == null ? ActionType.ADDED : ActionType.UPDATED, e);
+
throw handleException(e);
}
}
@@ -156,9 +167,18 @@ public class UserController extends BaseController {
checkParameter(USER_ID, strUserId);
try {
UserId userId = new UserId(toUUID(strUserId));
- checkUserId(userId);
+ User user = checkUserId(userId);
userService.deleteUser(userId);
+
+ logEntityAction(userId, user,
+ user.getCustomerId(),
+ ActionType.DELETED, null, strUserId);
+
} catch (Exception e) {
+ logEntityAction(emptyId(EntityType.USER),
+ null,
+ null,
+ ActionType.DELETED, e, strUserId);
throw handleException(e);
}
}
diff --git a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallConfiguration.java b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallConfiguration.java
new file mode 100644
index 0000000..d0132f8
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallConfiguration.java
@@ -0,0 +1,34 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.thingsboard.server.install;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Profile;
+import org.thingsboard.server.dao.audit.AuditLogLevelFilter;
+
+import java.util.HashMap;
+
+@Configuration
+@Profile("install")
+public class ThingsboardInstallConfiguration {
+
+ @Bean
+ public AuditLogLevelFilter emptyAuditLogLevelFilter() {
+ return new AuditLogLevelFilter(new HashMap<>());
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java
index bec7809..d918b22 100644
--- a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java
+++ b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java
@@ -81,6 +81,8 @@ public class ThingsboardInstallService {
case "1.3.1":
log.info("Upgrading ThingsBoard from version 1.3.1 to 1.4.0 ...");
+ databaseUpgradeService.upgradeDatabase("1.3.1");
+
log.info("Updating system data...");
systemDataLoaderService.deleteSystemWidgetBundle("charts");
diff --git a/application/src/main/java/org/thingsboard/server/service/install/CassandraDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/CassandraDatabaseUpgradeService.java
index 5b32374..dacf453 100644
--- a/application/src/main/java/org/thingsboard/server/service/install/CassandraDatabaseUpgradeService.java
+++ b/application/src/main/java/org/thingsboard/server/service/install/CassandraDatabaseUpgradeService.java
@@ -159,6 +159,12 @@ public class CassandraDatabaseUpgradeService implements DatabaseUpgradeService {
break;
case "1.3.0":
break;
+ case "1.3.1":
+ log.info("Updating schema ...");
+ schemaUpdateFile = Paths.get(this.dataDir, "upgrade", "1.4.0", SCHEMA_UPDATE_CQL);
+ loadCql(schemaUpdateFile);
+ log.info("Schema updated.");
+ break;
default:
throw new RuntimeException("Unable to upgrade Cassandra database, unsupported fromVersion: " + fromVersion);
}
diff --git a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java
index a8ffa99..098b4fe 100644
--- a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java
+++ b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java
@@ -61,6 +61,15 @@ public class SqlDatabaseUpgradeService implements DatabaseUpgradeService {
}
log.info("Schema updated.");
break;
+ case "1.3.1":
+ log.info("Updating schema ...");
+ schemaUpdateFile = Paths.get(this.dataDir, "upgrade", "1.4.0", SCHEMA_UPDATE_SQL);
+ try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) {
+ String sql = new String(Files.readAllBytes(schemaUpdateFile), Charset.forName("UTF-8"));
+ conn.createStatement().execute(sql); //NOSONAR, ignoring because method used to execute thingsboard database upgrade script
+ }
+ log.info("Schema updated.");
+ break;
default:
throw new RuntimeException("Unable to upgrade SQL database, unsupported fromVersion: " + fromVersion);
}
diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml
index de10135..f7a3a80 100644
--- a/application/src/main/resources/thingsboard.yml
+++ b/application/src/main/resources/thingsboard.yml
@@ -282,7 +282,6 @@ spring:
username: "${SPRING_DATASOURCE_USERNAME:sa}"
password: "${SPRING_DATASOURCE_PASSWORD:}"
-
# PostgreSQL DAO Configuration
#spring:
# data:
@@ -298,3 +297,23 @@ spring:
# url: "${SPRING_DATASOURCE_URL:jdbc:postgresql://localhost:5432/thingsboard}"
# username: "${SPRING_DATASOURCE_USERNAME:postgres}"
# password: "${SPRING_DATASOURCE_PASSWORD:postgres}"
+
+# Audit log parameters
+audit_log:
+ # Enable/disable audit log functionality.
+ enabled: "${AUDIT_LOG_ENABLED:true}"
+ # Specify partitioning size for audit log by tenant id storage. Example MINUTES, HOURS, DAYS, MONTHS
+ by_tenant_partitioning: "${AUDIT_LOG_BY_TENANT_PARTITIONING:MONTHS}"
+ # Number of days as history period if startTime and endTime are not specified
+ default_query_period: "${AUDIT_LOG_DEFAULT_QUERY_PERIOD:30}"
+ # Logging levels per each entity type.
+ # Allowed values: OFF (disable), W (log write operations), RW (log read and write operations)
+ logging_level:
+ mask:
+ "device": "${AUDIT_LOG_MASK_DEVICE:W}"
+ "asset": "${AUDIT_LOG_MASK_ASSET:W}"
+ "dashboard": "${AUDIT_LOG_MASK_DASHBOARD:W}"
+ "customer": "${AUDIT_LOG_MASK_CUSTOMER:W}"
+ "user": "${AUDIT_LOG_MASK_USER:W}"
+ "rule": "${AUDIT_LOG_MASK_RULE:W}"
+ "plugin": "${AUDIT_LOG_MASK_PLUGIN:W}"
diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java
index 8d68bf8..2204fff 100644
--- a/application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java
+++ b/application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java
@@ -66,6 +66,7 @@ import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UUIDBased;
import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.common.data.page.TimePageLink;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.config.ThingsboardSecurityConfiguration;
import org.thingsboard.server.service.mail.TestMailService;
@@ -336,6 +337,35 @@ public abstract class AbstractControllerTest {
return readResponse(doGet(urlTemplate, vars).andExpect(status().isOk()), responseType);
}
+ protected <T> T doGetTypedWithTimePageLink(String urlTemplate, TypeReference<T> responseType,
+ TimePageLink pageLink,
+ Object... urlVariables) throws Exception {
+ List<Object> pageLinkVariables = new ArrayList<>();
+ urlTemplate += "limit={limit}";
+ pageLinkVariables.add(pageLink.getLimit());
+ if (pageLink.getStartTime() != null) {
+ urlTemplate += "&startTime={startTime}";
+ pageLinkVariables.add(pageLink.getStartTime());
+ }
+ if (pageLink.getEndTime() != null) {
+ urlTemplate += "&endTime={endTime}";
+ pageLinkVariables.add(pageLink.getEndTime());
+ }
+ if (pageLink.getIdOffset() != null) {
+ urlTemplate += "&offset={offset}";
+ pageLinkVariables.add(pageLink.getIdOffset().toString());
+ }
+ if (pageLink.isAscOrder()) {
+ urlTemplate += "&ascOrder={ascOrder}";
+ pageLinkVariables.add(pageLink.isAscOrder());
+ }
+ Object[] vars = new Object[urlVariables.length + pageLinkVariables.size()];
+ System.arraycopy(urlVariables, 0, vars, 0, urlVariables.length);
+ System.arraycopy(pageLinkVariables.toArray(), 0, vars, urlVariables.length, pageLinkVariables.size());
+
+ return readResponse(doGet(urlTemplate, vars).andExpect(status().isOk()), responseType);
+ }
+
protected <T> T doPost(String urlTemplate, Class<T> responseClass, String... params) throws Exception {
return readResponse(doPost(urlTemplate, params).andExpect(status().isOk()), responseClass);
}
diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseAuditLogControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseAuditLogControllerTest.java
new file mode 100644
index 0000000..a826ee8
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/controller/BaseAuditLogControllerTest.java
@@ -0,0 +1,148 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.controller;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.Tenant;
+import org.thingsboard.server.common.data.User;
+import org.thingsboard.server.common.data.audit.AuditLog;
+import org.thingsboard.server.common.data.page.TimePageData;
+import org.thingsboard.server.common.data.page.TimePageLink;
+import org.thingsboard.server.common.data.security.Authority;
+import org.thingsboard.server.dao.model.ModelConstants;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+public abstract class BaseAuditLogControllerTest extends AbstractControllerTest {
+
+ private Tenant savedTenant;
+ private User tenantAdmin;
+
+ @Before
+ public void beforeTest() throws Exception {
+ loginSysAdmin();
+
+ Tenant tenant = new Tenant();
+ tenant.setTitle("My tenant");
+ savedTenant = doPost("/api/tenant", tenant, Tenant.class);
+ Assert.assertNotNull(savedTenant);
+
+ tenantAdmin = new User();
+ tenantAdmin.setAuthority(Authority.TENANT_ADMIN);
+ tenantAdmin.setTenantId(savedTenant.getId());
+ tenantAdmin.setEmail("tenant2@thingsboard.org");
+ tenantAdmin.setFirstName("Joe");
+ tenantAdmin.setLastName("Downs");
+
+ tenantAdmin = createUserAndLogin(tenantAdmin, "testPassword1");
+ }
+
+ @After
+ public void afterTest() throws Exception {
+ loginSysAdmin();
+
+ doDelete("/api/tenant/" + savedTenant.getId().getId().toString())
+ .andExpect(status().isOk());
+ }
+
+ @Test
+ public void testAuditLogs() throws Exception {
+ for (int i = 0; i < 178; i++) {
+ Device device = new Device();
+ device.setName("Device" + i);
+ device.setType("default");
+ doPost("/api/device", device, Device.class);
+ }
+
+ List<AuditLog> loadedAuditLogs = new ArrayList<>();
+ TimePageLink pageLink = new TimePageLink(23);
+ TimePageData<AuditLog> pageData;
+ do {
+ pageData = doGetTypedWithTimePageLink("/api/audit/logs?",
+ new TypeReference<TimePageData<AuditLog>>() {
+ }, pageLink);
+ loadedAuditLogs.addAll(pageData.getData());
+ if (pageData.hasNext()) {
+ pageLink = pageData.getNextPageLink();
+ }
+ } while (pageData.hasNext());
+
+ Assert.assertEquals(178, loadedAuditLogs.size());
+
+ loadedAuditLogs = new ArrayList<>();
+ pageLink = new TimePageLink(23);
+ do {
+ pageData = doGetTypedWithTimePageLink("/api/audit/logs/customer/" + ModelConstants.NULL_UUID + "?",
+ new TypeReference<TimePageData<AuditLog>>() {
+ }, pageLink);
+ loadedAuditLogs.addAll(pageData.getData());
+ if (pageData.hasNext()) {
+ pageLink = pageData.getNextPageLink();
+ }
+ } while (pageData.hasNext());
+
+ Assert.assertEquals(178, loadedAuditLogs.size());
+
+ loadedAuditLogs = new ArrayList<>();
+ pageLink = new TimePageLink(23);
+ do {
+ pageData = doGetTypedWithTimePageLink("/api/audit/logs/user/" + tenantAdmin.getId().getId().toString() + "?",
+ new TypeReference<TimePageData<AuditLog>>() {
+ }, pageLink);
+ loadedAuditLogs.addAll(pageData.getData());
+ if (pageData.hasNext()) {
+ pageLink = pageData.getNextPageLink();
+ }
+ } while (pageData.hasNext());
+
+ Assert.assertEquals(178, loadedAuditLogs.size());
+ }
+
+ @Test
+ public void testAuditLogs_byTenantIdAndEntityId() throws Exception {
+ Device device = new Device();
+ device.setName("Device name");
+ device.setType("default");
+ Device savedDevice = doPost("/api/device", device, Device.class);
+ for (int i = 0; i < 178; i++) {
+ savedDevice.setName("Device name" + i);
+ doPost("/api/device", savedDevice, Device.class);
+ }
+
+ List<AuditLog> loadedAuditLogs = new ArrayList<>();
+ TimePageLink pageLink = new TimePageLink(23);
+ TimePageData<AuditLog> pageData;
+ do {
+ pageData = doGetTypedWithTimePageLink("/api/audit/logs/entity/DEVICE/" + savedDevice.getId().getId() + "?",
+ new TypeReference<TimePageData<AuditLog>>() {
+ }, pageLink);
+ loadedAuditLogs.addAll(pageData.getData());
+ if (pageData.hasNext()) {
+ pageLink = pageData.getNextPageLink();
+ }
+ } while (pageData.hasNext());
+
+ Assert.assertEquals(179, loadedAuditLogs.size());
+ }
+}
diff --git a/application/src/test/java/org/thingsboard/server/controller/nosql/AuditLogControllerNoSqlTest.java b/application/src/test/java/org/thingsboard/server/controller/nosql/AuditLogControllerNoSqlTest.java
new file mode 100644
index 0000000..4692afe
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/controller/nosql/AuditLogControllerNoSqlTest.java
@@ -0,0 +1,23 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.controller.nosql;
+
+import org.thingsboard.server.controller.BaseAuditLogControllerTest;
+import org.thingsboard.server.dao.service.DaoNoSqlTest;
+
+@DaoNoSqlTest
+public class AuditLogControllerNoSqlTest extends BaseAuditLogControllerTest {
+}
diff --git a/application/src/test/java/org/thingsboard/server/controller/sql/AuditLogControllerSqlTest.java b/application/src/test/java/org/thingsboard/server/controller/sql/AuditLogControllerSqlTest.java
new file mode 100644
index 0000000..df6804e
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/controller/sql/AuditLogControllerSqlTest.java
@@ -0,0 +1,23 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.controller.sql;
+
+import org.thingsboard.server.controller.BaseAuditLogControllerTest;
+import org.thingsboard.server.dao.service.DaoSqlTest;
+
+@DaoSqlTest
+public class AuditLogControllerSqlTest extends BaseAuditLogControllerTest {
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionStatus.java b/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionStatus.java
new file mode 100644
index 0000000..5ee8beb
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionStatus.java
@@ -0,0 +1,20 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data.audit;
+
+public enum ActionStatus {
+ SUCCESS, FAILURE
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java b/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java
new file mode 100644
index 0000000..7e1a976
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java
@@ -0,0 +1,41 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data.audit;
+
+import lombok.Getter;
+
+@Getter
+public enum ActionType {
+ ADDED(false), // log entity
+ DELETED(false), // log string id
+ UPDATED(false), // log entity
+ ATTRIBUTES_UPDATED(false), // log attributes/values
+ ATTRIBUTES_DELETED(false), // log attributes
+ RPC_CALL(false), // log method and params
+ CREDENTIALS_UPDATED(false), // log new credentials
+ ASSIGNED_TO_CUSTOMER(false), // log customer name
+ UNASSIGNED_FROM_CUSTOMER(false), // log customer name
+ ACTIVATED(false), // log string id
+ SUSPENDED(false), // log string id
+ CREDENTIALS_READ(true), // log device id
+ ATTRIBUTES_READ(true); // log attributes
+
+ private final boolean isRead;
+
+ ActionType(boolean isRead) {
+ this.isRead = isRead;
+ }
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/audit/AuditLog.java b/common/data/src/main/java/org/thingsboard/server/common/data/audit/AuditLog.java
new file mode 100644
index 0000000..62d4bfa
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/audit/AuditLog.java
@@ -0,0 +1,60 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data.audit;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.thingsboard.server.common.data.BaseData;
+import org.thingsboard.server.common.data.id.*;
+
+@EqualsAndHashCode(callSuper = true)
+@Data
+public class AuditLog extends BaseData<AuditLogId> {
+
+ private TenantId tenantId;
+ private CustomerId customerId;
+ private EntityId entityId;
+ private String entityName;
+ private UserId userId;
+ private String userName;
+ private ActionType actionType;
+ private JsonNode actionData;
+ private ActionStatus actionStatus;
+ private String actionFailureDetails;
+
+ public AuditLog() {
+ super();
+ }
+
+ public AuditLog(AuditLogId id) {
+ super(id);
+ }
+
+ public AuditLog(AuditLog auditLog) {
+ super(auditLog);
+ this.tenantId = auditLog.getTenantId();
+ this.customerId = auditLog.getCustomerId();
+ this.entityId = auditLog.getEntityId();
+ this.entityName = auditLog.getEntityName();
+ this.userId = auditLog.getUserId();
+ this.userName = auditLog.getUserName();
+ this.actionType = auditLog.getActionType();
+ this.actionData = auditLog.getActionData();
+ this.actionStatus = auditLog.getActionStatus();
+ this.actionFailureDetails = auditLog.getActionFailureDetails();
+ }
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/AuditLogId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/AuditLogId.java
new file mode 100644
index 0000000..5327212
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/AuditLogId.java
@@ -0,0 +1,35 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data.id;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.util.UUID;
+
+public class AuditLogId extends UUIDBased {
+
+ private static final long serialVersionUID = 1L;
+
+ @JsonCreator
+ public AuditLogId(@JsonProperty("id") UUID id) {
+ super(id);
+ }
+
+ public static AuditLogId fromString(String auditLogId) {
+ return new AuditLogId(UUID.fromString(auditLogId));
+ }
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogDao.java b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogDao.java
new file mode 100644
index 0000000..8562b6a
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogDao.java
@@ -0,0 +1,47 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.audit;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import org.thingsboard.server.common.data.audit.AuditLog;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.UserId;
+import org.thingsboard.server.common.data.page.TimePageLink;
+
+import java.util.List;
+import java.util.UUID;
+
+public interface AuditLogDao {
+
+ ListenableFuture<Void> saveByTenantId(AuditLog auditLog);
+
+ ListenableFuture<Void> saveByTenantIdAndEntityId(AuditLog auditLog);
+
+ ListenableFuture<Void> saveByTenantIdAndCustomerId(AuditLog auditLog);
+
+ ListenableFuture<Void> saveByTenantIdAndUserId(AuditLog auditLog);
+
+ ListenableFuture<Void> savePartitionsByTenantId(AuditLog auditLog);
+
+ List<AuditLog> findAuditLogsByTenantIdAndEntityId(UUID tenantId, EntityId entityId, TimePageLink pageLink);
+
+ List<AuditLog> findAuditLogsByTenantIdAndCustomerId(UUID tenantId, CustomerId customerId, TimePageLink pageLink);
+
+ List<AuditLog> findAuditLogsByTenantIdAndUserId(UUID tenantId, UserId userId, TimePageLink pageLink);
+
+ List<AuditLog> findAuditLogsByTenantId(UUID tenantId, TimePageLink pageLink);
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogLevelFilter.java b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogLevelFilter.java
new file mode 100644
index 0000000..0ae22e2
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogLevelFilter.java
@@ -0,0 +1,46 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.audit;
+
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.audit.ActionType;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class AuditLogLevelFilter {
+
+ private Map<EntityType, AuditLogLevelMask> entityTypeMask = new HashMap<>();
+
+ public AuditLogLevelFilter(Map<String, String> mask) {
+ entityTypeMask.clear();
+ mask.forEach((entityTypeStr, logLevelMaskStr) -> {
+ EntityType entityType = EntityType.valueOf(entityTypeStr.toUpperCase());
+ AuditLogLevelMask logLevelMask = AuditLogLevelMask.valueOf(logLevelMaskStr.toUpperCase());
+ entityTypeMask.put(entityType, logLevelMask);
+ });
+ }
+
+ public boolean logEnabled(EntityType entityType, ActionType actionType) {
+ AuditLogLevelMask logLevelMask = entityTypeMask.get(entityType);
+ if (logLevelMask != null) {
+ return actionType.isRead() ? logLevelMask.isRead() : logLevelMask.isWrite();
+ } else {
+ return false;
+ }
+ }
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogLevelMask.java b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogLevelMask.java
new file mode 100644
index 0000000..ba5efb4
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogLevelMask.java
@@ -0,0 +1,34 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.audit;
+
+import lombok.Getter;
+
+@Getter
+public enum AuditLogLevelMask {
+
+ OFF(false, false),
+ W(true, false),
+ RW(true, true);
+
+ private final boolean write;
+ private final boolean read;
+
+ AuditLogLevelMask(boolean write, boolean read) {
+ this.write = write;
+ this.read = read;
+ }
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogQueryCursor.java b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogQueryCursor.java
new file mode 100644
index 0000000..78b043b
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogQueryCursor.java
@@ -0,0 +1,70 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.audit;
+
+import lombok.Getter;
+import org.thingsboard.server.common.data.page.TimePageLink;
+import org.thingsboard.server.dao.model.nosql.AuditLogEntity;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+public class AuditLogQueryCursor {
+ @Getter
+ private final UUID tenantId;
+ @Getter
+ private final List<AuditLogEntity> data;
+ @Getter
+ private final TimePageLink pageLink;
+
+ private final List<Long> partitions;
+
+ private int partitionIndex;
+ private int currentLimit;
+
+ public AuditLogQueryCursor(UUID tenantId, TimePageLink pageLink, List<Long> partitions) {
+ this.tenantId = tenantId;
+ this.partitions = partitions;
+ this.partitionIndex = partitions.size() - 1;
+ this.data = new ArrayList<>();
+ this.currentLimit = pageLink.getLimit();
+ this.pageLink = pageLink;
+ }
+
+ public boolean hasNextPartition() {
+ return partitionIndex >= 0;
+ }
+
+ public boolean isFull() {
+ return currentLimit <= 0;
+ }
+
+ public long getNextPartition() {
+ long partition = partitions.get(partitionIndex);
+ partitionIndex--;
+ return partition;
+ }
+
+ public int getCurrentLimit() {
+ return currentLimit;
+ }
+
+ public void addData(List<AuditLogEntity> newData) {
+ currentLimit -= newData.size();
+ data.addAll(newData);
+ }
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogService.java b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogService.java
new file mode 100644
index 0000000..86cbbba
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogService.java
@@ -0,0 +1,53 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.audit;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.google.common.util.concurrent.ListenableFuture;
+import org.thingsboard.server.common.data.BaseData;
+import org.thingsboard.server.common.data.HasName;
+import org.thingsboard.server.common.data.User;
+import org.thingsboard.server.common.data.audit.ActionStatus;
+import org.thingsboard.server.common.data.audit.ActionType;
+import org.thingsboard.server.common.data.audit.AuditLog;
+import org.thingsboard.server.common.data.id.*;
+import org.thingsboard.server.common.data.page.TimePageData;
+import org.thingsboard.server.common.data.page.TimePageLink;
+
+import java.util.List;
+
+public interface AuditLogService {
+
+ TimePageData<AuditLog> findAuditLogsByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, TimePageLink pageLink);
+
+ TimePageData<AuditLog> findAuditLogsByTenantIdAndUserId(TenantId tenantId, UserId userId, TimePageLink pageLink);
+
+ TimePageData<AuditLog> findAuditLogsByTenantIdAndEntityId(TenantId tenantId, EntityId entityId, TimePageLink pageLink);
+
+ TimePageData<AuditLog> findAuditLogsByTenantId(TenantId tenantId, TimePageLink pageLink);
+
+ <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);
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java
new file mode 100644
index 0000000..ab1c313
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java
@@ -0,0 +1,316 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.audit;
+
+import com.datastax.driver.core.utils.UUIDs;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.google.common.collect.Lists;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+import org.thingsboard.server.common.data.BaseData;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.HasName;
+import org.thingsboard.server.common.data.audit.ActionStatus;
+import org.thingsboard.server.common.data.audit.ActionType;
+import org.thingsboard.server.common.data.audit.AuditLog;
+import org.thingsboard.server.common.data.id.*;
+import org.thingsboard.server.common.data.kv.AttributeKvEntry;
+import org.thingsboard.server.common.data.page.TimePageData;
+import org.thingsboard.server.common.data.page.TimePageLink;
+import org.thingsboard.server.common.data.security.DeviceCredentials;
+import org.thingsboard.server.dao.entity.EntityService;
+import org.thingsboard.server.dao.exception.DataValidationException;
+import org.thingsboard.server.dao.service.DataValidator;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.List;
+
+import static org.thingsboard.server.dao.service.Validator.validateEntityId;
+import static org.thingsboard.server.dao.service.Validator.validateId;
+
+@Slf4j
+@Service
+@ConditionalOnProperty(prefix = "audit_log", value = "enabled", havingValue = "true")
+public class AuditLogServiceImpl implements AuditLogService {
+
+ private static final ObjectMapper objectMapper = new ObjectMapper();
+
+ private static final String INCORRECT_TENANT_ID = "Incorrect tenantId ";
+ private static final int INSERTS_PER_ENTRY = 3;
+
+ @Autowired
+ private AuditLogLevelFilter auditLogLevelFilter;
+
+ @Autowired
+ private AuditLogDao auditLogDao;
+
+ @Autowired
+ private EntityService entityService;
+
+ @Override
+ public TimePageData<AuditLog> findAuditLogsByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, TimePageLink pageLink) {
+ log.trace("Executing findAuditLogsByTenantIdAndCustomerId [{}], [{}], [{}]", tenantId, customerId, pageLink);
+ validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
+ validateId(customerId, "Incorrect customerId " + customerId);
+ List<AuditLog> auditLogs = auditLogDao.findAuditLogsByTenantIdAndCustomerId(tenantId.getId(), customerId, pageLink);
+ return new TimePageData<>(auditLogs, pageLink);
+ }
+
+ @Override
+ public TimePageData<AuditLog> findAuditLogsByTenantIdAndUserId(TenantId tenantId, UserId userId, TimePageLink pageLink) {
+ log.trace("Executing findAuditLogsByTenantIdAndUserId [{}], [{}], [{}]", tenantId, userId, pageLink);
+ validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
+ validateId(userId, "Incorrect userId" + userId);
+ List<AuditLog> auditLogs = auditLogDao.findAuditLogsByTenantIdAndUserId(tenantId.getId(), userId, pageLink);
+ return new TimePageData<>(auditLogs, pageLink);
+ }
+
+ @Override
+ public TimePageData<AuditLog> findAuditLogsByTenantIdAndEntityId(TenantId tenantId, EntityId entityId, TimePageLink pageLink) {
+ log.trace("Executing findAuditLogsByTenantIdAndEntityId [{}], [{}], [{}]", tenantId, entityId, pageLink);
+ validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
+ validateEntityId(entityId, INCORRECT_TENANT_ID + entityId);
+ List<AuditLog> auditLogs = auditLogDao.findAuditLogsByTenantIdAndEntityId(tenantId.getId(), entityId, pageLink);
+ return new TimePageData<>(auditLogs, pageLink);
+ }
+
+ @Override
+ public TimePageData<AuditLog> findAuditLogsByTenantId(TenantId tenantId, TimePageLink pageLink) {
+ log.trace("Executing findAuditLogs [{}]", pageLink);
+ validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
+ List<AuditLog> auditLogs = auditLogDao.findAuditLogsByTenantId(tenantId.getId(), pageLink);
+ return new TimePageData<>(auditLogs, pageLink);
+ }
+
+ @Override
+ public <E extends BaseData<I> & HasName, I extends UUIDBased & EntityId> ListenableFuture<List<Void>>
+ logEntityAction(TenantId tenantId, CustomerId customerId, UserId userId, String userName, I entityId, E entity,
+ ActionType actionType, Exception e, Object... additionalInfo) {
+ if (canLog(entityId.getEntityType(), actionType)) {
+ JsonNode actionData = constructActionData(entityId, entity, actionType, additionalInfo);
+ ActionStatus actionStatus = ActionStatus.SUCCESS;
+ String failureDetails = "";
+ String entityName = "";
+ if (entity != null) {
+ entityName = entity.getName();
+ } else {
+ try {
+ entityName = entityService.fetchEntityNameAsync(entityId).get();
+ } catch (Exception ex) {}
+ }
+ if (e != null) {
+ actionStatus = ActionStatus.FAILURE;
+ failureDetails = getFailureStack(e);
+ }
+ if (actionType == ActionType.RPC_CALL) {
+ String rpcErrorString = extractParameter(String.class, additionalInfo);
+ if (!StringUtils.isEmpty(rpcErrorString)) {
+ actionStatus = ActionStatus.FAILURE;
+ failureDetails = rpcErrorString;
+ }
+ }
+ return logAction(tenantId,
+ entityId,
+ entityName,
+ customerId,
+ userId,
+ userName,
+ actionType,
+ actionData,
+ actionStatus,
+ failureDetails);
+ } else {
+ return null;
+ }
+ }
+
+ private <E extends BaseData<I> & HasName, I extends UUIDBased & EntityId> JsonNode constructActionData(I entityId,
+ E entity,
+ ActionType actionType,
+ Object... additionalInfo) {
+ ObjectNode actionData = objectMapper.createObjectNode();
+ switch(actionType) {
+ case ADDED:
+ case UPDATED:
+ ObjectNode entityNode = objectMapper.valueToTree(entity);
+ if (entityId.getEntityType() == EntityType.DASHBOARD) {
+ entityNode.put("configuration", "");
+ }
+ actionData.set("entity", entityNode);
+ break;
+ case DELETED:
+ case ACTIVATED:
+ case SUSPENDED:
+ case CREDENTIALS_READ:
+ String strEntityId = extractParameter(String.class, additionalInfo);
+ actionData.put("entityId", strEntityId);
+ break;
+ case ATTRIBUTES_UPDATED:
+ actionData.put("entityId", entityId.toString());
+ String scope = extractParameter(String.class, 0, additionalInfo);
+ List<AttributeKvEntry> attributes = extractParameter(List.class, 1, additionalInfo);
+ actionData.put("scope", scope);
+ ObjectNode attrsNode = objectMapper.createObjectNode();
+ if (attributes != null) {
+ for (AttributeKvEntry attr : attributes) {
+ attrsNode.put(attr.getKey(), attr.getValueAsString());
+ }
+ }
+ actionData.set("attributes", attrsNode);
+ break;
+ case ATTRIBUTES_DELETED:
+ case ATTRIBUTES_READ:
+ actionData.put("entityId", entityId.toString());
+ scope = extractParameter(String.class, 0, additionalInfo);
+ actionData.put("scope", scope);
+ List<String> keys = extractParameter(List.class, 1, additionalInfo);
+ ArrayNode attrsArrayNode = actionData.putArray("attributes");
+ if (keys != null) {
+ keys.forEach(attrsArrayNode::add);
+ }
+ break;
+ case RPC_CALL:
+ actionData.put("entityId", entityId.toString());
+ Boolean oneWay = extractParameter(Boolean.class, 1, additionalInfo);
+ String method = extractParameter(String.class, 2, additionalInfo);
+ String params = extractParameter(String.class, 3, additionalInfo);
+ actionData.put("oneWay", oneWay);
+ actionData.put("method", method);
+ actionData.put("params", params);
+ break;
+ case CREDENTIALS_UPDATED:
+ actionData.put("entityId", entityId.toString());
+ DeviceCredentials deviceCredentials = extractParameter(DeviceCredentials.class, additionalInfo);
+ actionData.set("credentials", objectMapper.valueToTree(deviceCredentials));
+ break;
+ case ASSIGNED_TO_CUSTOMER:
+ strEntityId = extractParameter(String.class, 0, additionalInfo);
+ String strCustomerId = extractParameter(String.class, 1, additionalInfo);
+ String strCustomerName = extractParameter(String.class, 2, additionalInfo);
+ actionData.put("entityId", strEntityId);
+ actionData.put("assignedCustomerId", strCustomerId);
+ actionData.put("assignedCustomerName", strCustomerName);
+ break;
+ case UNASSIGNED_FROM_CUSTOMER:
+ strEntityId = extractParameter(String.class, 0, additionalInfo);
+ strCustomerId = extractParameter(String.class, 1, additionalInfo);
+ strCustomerName = extractParameter(String.class, 2, additionalInfo);
+ actionData.put("entityId", strEntityId);
+ actionData.put("unassignedCustomerId", strCustomerId);
+ actionData.put("unassignedCustomerName", strCustomerName);
+ break;
+ }
+ return actionData;
+ }
+
+ private <T> T extractParameter(Class<T> clazz, Object... additionalInfo) {
+ return extractParameter(clazz, 0, additionalInfo);
+ }
+
+ private <T> T extractParameter(Class<T> clazz, int index, Object... additionalInfo) {
+ T result = null;
+ if (additionalInfo != null && additionalInfo.length > index) {
+ Object paramObject = additionalInfo[index];
+ if (clazz.isInstance(paramObject)) {
+ result = clazz.cast(paramObject);
+ }
+ }
+ return result;
+ }
+
+ private String getFailureStack(Exception e) {
+ StringWriter sw = new StringWriter();
+ e.printStackTrace(new PrintWriter(sw));
+ return sw.toString();
+ }
+
+ private boolean canLog(EntityType entityType, ActionType actionType) {
+ return auditLogLevelFilter.logEnabled(entityType, actionType);
+ }
+
+ private AuditLog createAuditLogEntry(TenantId tenantId,
+ EntityId entityId,
+ String entityName,
+ CustomerId customerId,
+ UserId userId,
+ String userName,
+ ActionType actionType,
+ JsonNode actionData,
+ ActionStatus actionStatus,
+ String actionFailureDetails) {
+ AuditLog result = new AuditLog();
+ result.setId(new AuditLogId(UUIDs.timeBased()));
+ result.setTenantId(tenantId);
+ result.setEntityId(entityId);
+ result.setEntityName(entityName);
+ result.setCustomerId(customerId);
+ result.setUserId(userId);
+ result.setUserName(userName);
+ result.setActionType(actionType);
+ result.setActionData(actionData);
+ result.setActionStatus(actionStatus);
+ result.setActionFailureDetails(actionFailureDetails);
+ return result;
+ }
+
+ private ListenableFuture<List<Void>> logAction(TenantId tenantId,
+ EntityId entityId,
+ String entityName,
+ CustomerId customerId,
+ UserId userId,
+ String userName,
+ ActionType actionType,
+ JsonNode actionData,
+ ActionStatus actionStatus,
+ String actionFailureDetails) {
+ AuditLog auditLogEntry = createAuditLogEntry(tenantId, entityId, entityName, customerId, userId, userName,
+ actionType, actionData, actionStatus, actionFailureDetails);
+ log.trace("Executing logAction [{}]", auditLogEntry);
+ auditLogValidator.validate(auditLogEntry);
+ List<ListenableFuture<Void>> futures = Lists.newArrayListWithExpectedSize(INSERTS_PER_ENTRY);
+ futures.add(auditLogDao.savePartitionsByTenantId(auditLogEntry));
+ futures.add(auditLogDao.saveByTenantId(auditLogEntry));
+ futures.add(auditLogDao.saveByTenantIdAndEntityId(auditLogEntry));
+ futures.add(auditLogDao.saveByTenantIdAndCustomerId(auditLogEntry));
+ futures.add(auditLogDao.saveByTenantIdAndUserId(auditLogEntry));
+ return Futures.allAsList(futures);
+ }
+
+ private DataValidator<AuditLog> auditLogValidator =
+ new DataValidator<AuditLog>() {
+ @Override
+ protected void validateDataImpl(AuditLog auditLog) {
+ if (auditLog.getEntityId() == null) {
+ throw new DataValidationException("Entity Id should be specified!");
+ }
+ if (auditLog.getTenantId() == null) {
+ throw new DataValidationException("Tenant Id should be specified!");
+ }
+ if (auditLog.getUserId() == null) {
+ throw new DataValidationException("User Id should be specified!");
+ }
+ }
+ };
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/CassandraAuditLogDao.java b/dao/src/main/java/org/thingsboard/server/dao/audit/CassandraAuditLogDao.java
new file mode 100644
index 0000000..fa32b5d
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/audit/CassandraAuditLogDao.java
@@ -0,0 +1,349 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.audit;
+
+import com.datastax.driver.core.BoundStatement;
+import com.datastax.driver.core.PreparedStatement;
+import com.datastax.driver.core.ResultSet;
+import com.datastax.driver.core.ResultSetFuture;
+import com.datastax.driver.core.querybuilder.QueryBuilder;
+import com.datastax.driver.core.querybuilder.Select;
+import com.google.common.base.Function;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.core.env.Environment;
+import org.springframework.stereotype.Component;
+import org.thingsboard.server.common.data.audit.AuditLog;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.UserId;
+import org.thingsboard.server.common.data.page.TimePageLink;
+import org.thingsboard.server.dao.DaoUtil;
+import org.thingsboard.server.dao.model.ModelConstants;
+import org.thingsboard.server.dao.model.nosql.AuditLogEntity;
+import org.thingsboard.server.dao.nosql.CassandraAbstractSearchTimeDao;
+import org.thingsboard.server.dao.timeseries.TsPartitionDate;
+import org.thingsboard.server.dao.util.NoSqlDao;
+
+import javax.annotation.Nullable;
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.ZoneOffset;
+import java.util.*;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.stream.Collectors;
+
+import static com.datastax.driver.core.querybuilder.QueryBuilder.eq;
+import static org.thingsboard.server.dao.model.ModelConstants.*;
+
+@Component
+@Slf4j
+@NoSqlDao
+public class CassandraAuditLogDao extends CassandraAbstractSearchTimeDao<AuditLogEntity, AuditLog> implements AuditLogDao {
+
+ private static final String INSERT_INTO = "INSERT INTO ";
+
+ @Autowired
+ private Environment environment;
+
+ @Override
+ protected Class<AuditLogEntity> getColumnFamilyClass() {
+ return AuditLogEntity.class;
+ }
+
+ @Override
+ protected String getColumnFamilyName() {
+ return AUDIT_LOG_COLUMN_FAMILY_NAME;
+ }
+
+ protected ExecutorService readResultsProcessingExecutor;
+
+ @Value("${audit_log.by_tenant_partitioning}")
+ private String partitioning;
+ private TsPartitionDate tsFormat;
+
+ @Value("${audit_log.default_query_period}")
+ private Integer defaultQueryPeriodInDays;
+
+ private PreparedStatement partitionInsertStmt;
+ private PreparedStatement saveByTenantStmt;
+ private PreparedStatement saveByTenantIdAndUserIdStmt;
+ private PreparedStatement saveByTenantIdAndEntityIdStmt;
+ private PreparedStatement saveByTenantIdAndCustomerIdStmt;
+
+ private boolean isInstall() {
+ return environment.acceptsProfiles("install");
+ }
+
+ @PostConstruct
+ public void init() {
+ if (!isInstall()) {
+ Optional<TsPartitionDate> partition = TsPartitionDate.parse(partitioning);
+ if (partition.isPresent()) {
+ tsFormat = partition.get();
+ } else {
+ log.warn("Incorrect configuration of partitioning {}", partitioning);
+ throw new RuntimeException("Failed to parse partitioning property: " + partitioning + "!");
+ }
+ }
+ readResultsProcessingExecutor = Executors.newCachedThreadPool();
+ }
+
+ @PreDestroy
+ public void stopExecutor() {
+ if (readResultsProcessingExecutor != null) {
+ readResultsProcessingExecutor.shutdownNow();
+ }
+ }
+
+ private <T> ListenableFuture<T> getFuture(ResultSetFuture future, java.util.function.Function<ResultSet, T> transformer) {
+ return Futures.transform(future, new Function<ResultSet, T>() {
+ @Nullable
+ @Override
+ public T apply(@Nullable ResultSet input) {
+ return transformer.apply(input);
+ }
+ }, readResultsProcessingExecutor);
+ }
+
+ @Override
+ public ListenableFuture<Void> saveByTenantId(AuditLog auditLog) {
+ log.debug("Save saveByTenantId [{}] ", auditLog);
+
+ long partition = toPartitionTs(LocalDate.now().atStartOfDay().toInstant(ZoneOffset.UTC).toEpochMilli());
+ BoundStatement stmt = getSaveByTenantStmt().bind();
+ stmt = setSaveStmtVariables(stmt, auditLog, partition);
+ return getFuture(executeAsyncWrite(stmt), rs -> null);
+ }
+
+ @Override
+ public ListenableFuture<Void> saveByTenantIdAndEntityId(AuditLog auditLog) {
+ log.debug("Save saveByTenantIdAndEntityId [{}] ", auditLog);
+
+ BoundStatement stmt = getSaveByTenantIdAndEntityIdStmt().bind();
+ stmt = setSaveStmtVariables(stmt, auditLog, -1);
+ return getFuture(executeAsyncWrite(stmt), rs -> null);
+ }
+
+ @Override
+ public ListenableFuture<Void> saveByTenantIdAndCustomerId(AuditLog auditLog) {
+ log.debug("Save saveByTenantIdAndCustomerId [{}] ", auditLog);
+
+ BoundStatement stmt = getSaveByTenantIdAndCustomerIdStmt().bind();
+ stmt = setSaveStmtVariables(stmt, auditLog, -1);
+ return getFuture(executeAsyncWrite(stmt), rs -> null);
+ }
+
+ @Override
+ public ListenableFuture<Void> saveByTenantIdAndUserId(AuditLog auditLog) {
+ log.debug("Save saveByTenantIdAndUserId [{}] ", auditLog);
+
+ BoundStatement stmt = getSaveByTenantIdAndUserIdStmt().bind();
+ stmt = setSaveStmtVariables(stmt, auditLog, -1);
+ return getFuture(executeAsyncWrite(stmt), rs -> null);
+ }
+
+ private BoundStatement setSaveStmtVariables(BoundStatement stmt, AuditLog auditLog, long partition) {
+ stmt.setUUID(0, auditLog.getId().getId())
+ .setUUID(1, auditLog.getTenantId().getId())
+ .setUUID(2, auditLog.getCustomerId().getId())
+ .setUUID(3, auditLog.getEntityId().getId())
+ .setString(4, auditLog.getEntityId().getEntityType().name())
+ .setString(5, auditLog.getEntityName())
+ .setUUID(6, auditLog.getUserId().getId())
+ .setString(7, auditLog.getUserName())
+ .setString(8, auditLog.getActionType().name())
+ .setString(9, auditLog.getActionData() != null ? auditLog.getActionData().toString() : null)
+ .setString(10, auditLog.getActionStatus().name())
+ .setString(11, auditLog.getActionFailureDetails());
+ if (partition > -1) {
+ stmt.setLong(12, partition);
+ }
+ return stmt;
+ }
+
+ @Override
+ public ListenableFuture<Void> savePartitionsByTenantId(AuditLog auditLog) {
+ log.debug("Save savePartitionsByTenantId [{}] ", auditLog);
+
+ long partition = toPartitionTs(LocalDate.now().atStartOfDay().toInstant(ZoneOffset.UTC).toEpochMilli());
+
+ BoundStatement stmt = getPartitionInsertStmt().bind();
+ stmt = stmt.setUUID(0, auditLog.getTenantId().getId())
+ .setLong(1, partition);
+ return getFuture(executeAsyncWrite(stmt), rs -> null);
+ }
+
+ private PreparedStatement getSaveByTenantStmt() {
+ if (saveByTenantStmt == null) {
+ saveByTenantStmt = getSaveByTenantIdAndCFName(ModelConstants.AUDIT_LOG_BY_TENANT_ID_CF, true);
+ }
+ return saveByTenantStmt;
+ }
+
+ private PreparedStatement getSaveByTenantIdAndEntityIdStmt() {
+ if (saveByTenantIdAndEntityIdStmt == null) {
+ saveByTenantIdAndEntityIdStmt = getSaveByTenantIdAndCFName(ModelConstants.AUDIT_LOG_BY_ENTITY_ID_CF, false);
+ }
+ return saveByTenantIdAndEntityIdStmt;
+ }
+
+ private PreparedStatement getSaveByTenantIdAndCustomerIdStmt() {
+ if (saveByTenantIdAndCustomerIdStmt == null) {
+ saveByTenantIdAndCustomerIdStmt = getSaveByTenantIdAndCFName(ModelConstants.AUDIT_LOG_BY_CUSTOMER_ID_CF, false);
+ }
+ return saveByTenantIdAndCustomerIdStmt;
+ }
+
+ private PreparedStatement getSaveByTenantIdAndUserIdStmt() {
+ if (saveByTenantIdAndUserIdStmt == null) {
+ saveByTenantIdAndUserIdStmt = getSaveByTenantIdAndCFName(ModelConstants.AUDIT_LOG_BY_USER_ID_CF, false);
+ }
+ return saveByTenantIdAndUserIdStmt;
+ }
+
+ private PreparedStatement getSaveByTenantIdAndCFName(String cfName, boolean hasPartition) {
+ List columnsList = new ArrayList();
+ columnsList.add(ModelConstants.AUDIT_LOG_ID_PROPERTY);
+ columnsList.add(ModelConstants.AUDIT_LOG_TENANT_ID_PROPERTY);
+ columnsList.add(ModelConstants.AUDIT_LOG_CUSTOMER_ID_PROPERTY);
+ columnsList.add(ModelConstants.AUDIT_LOG_ENTITY_ID_PROPERTY);
+ columnsList.add(ModelConstants.AUDIT_LOG_ENTITY_TYPE_PROPERTY);
+ columnsList.add(ModelConstants.AUDIT_LOG_ENTITY_NAME_PROPERTY);
+ columnsList.add(ModelConstants.AUDIT_LOG_USER_ID_PROPERTY);
+ columnsList.add(ModelConstants.AUDIT_LOG_USER_NAME_PROPERTY);
+ columnsList.add(ModelConstants.AUDIT_LOG_ACTION_TYPE_PROPERTY);
+ columnsList.add(ModelConstants.AUDIT_LOG_ACTION_DATA_PROPERTY);
+ columnsList.add(ModelConstants.AUDIT_LOG_ACTION_STATUS_PROPERTY);
+ columnsList.add(ModelConstants.AUDIT_LOG_ACTION_FAILURE_DETAILS_PROPERTY);
+ if (hasPartition) {
+ columnsList.add(ModelConstants.AUDIT_LOG_PARTITION_PROPERTY);
+ }
+ StringJoiner values = new StringJoiner(",");
+ for (int i=0;i<columnsList.size();i++) {
+ values.add("?");
+ }
+ String statementString = INSERT_INTO + cfName + " (" + String.join(",", columnsList) + ") VALUES (" + values.toString() + ")";
+ return getSession().prepare(statementString);
+ }
+
+ private PreparedStatement getPartitionInsertStmt() {
+ if (partitionInsertStmt == null) {
+ partitionInsertStmt = getSession().prepare(INSERT_INTO + ModelConstants.AUDIT_LOG_BY_TENANT_ID_PARTITIONS_CF +
+ "(" + ModelConstants.AUDIT_LOG_TENANT_ID_PROPERTY +
+ "," + ModelConstants.AUDIT_LOG_PARTITION_PROPERTY + ")" +
+ " VALUES(?, ?)");
+ }
+ return partitionInsertStmt;
+ }
+
+ private long toPartitionTs(long ts) {
+ LocalDateTime time = LocalDateTime.ofInstant(Instant.ofEpochMilli(ts), ZoneOffset.UTC);
+ return tsFormat.truncatedTo(time).toInstant(ZoneOffset.UTC).toEpochMilli();
+ }
+
+ @Override
+ public List<AuditLog> findAuditLogsByTenantIdAndEntityId(UUID tenantId, EntityId entityId, TimePageLink pageLink) {
+ log.trace("Try to find audit logs by tenant [{}], entity [{}] and pageLink [{}]", tenantId, entityId, pageLink);
+ List<AuditLogEntity> entities = findPageWithTimeSearch(AUDIT_LOG_BY_ENTITY_ID_CF,
+ Arrays.asList(eq(ModelConstants.AUDIT_LOG_TENANT_ID_PROPERTY, tenantId),
+ eq(ModelConstants.AUDIT_LOG_ENTITY_TYPE_PROPERTY, entityId.getEntityType()),
+ eq(ModelConstants.AUDIT_LOG_ENTITY_ID_PROPERTY, entityId.getId())),
+ pageLink);
+ log.trace("Found audit logs by tenant [{}], entity [{}] and pageLink [{}]", tenantId, entityId, pageLink);
+ return DaoUtil.convertDataList(entities);
+ }
+
+ @Override
+ public List<AuditLog> findAuditLogsByTenantIdAndCustomerId(UUID tenantId, CustomerId customerId, TimePageLink pageLink) {
+ log.trace("Try to find audit logs by tenant [{}], customer [{}] and pageLink [{}]", tenantId, customerId, pageLink);
+ List<AuditLogEntity> entities = findPageWithTimeSearch(AUDIT_LOG_BY_CUSTOMER_ID_CF,
+ Arrays.asList(eq(ModelConstants.AUDIT_LOG_TENANT_ID_PROPERTY, tenantId),
+ eq(ModelConstants.AUDIT_LOG_CUSTOMER_ID_PROPERTY, customerId.getId())),
+ pageLink);
+ log.trace("Found audit logs by tenant [{}], customer [{}] and pageLink [{}]", tenantId, customerId, pageLink);
+ return DaoUtil.convertDataList(entities);
+ }
+
+ @Override
+ public List<AuditLog> findAuditLogsByTenantIdAndUserId(UUID tenantId, UserId userId, TimePageLink pageLink) {
+ log.trace("Try to find audit logs by tenant [{}], user [{}] and pageLink [{}]", tenantId, userId, pageLink);
+ List<AuditLogEntity> entities = findPageWithTimeSearch(AUDIT_LOG_BY_USER_ID_CF,
+ Arrays.asList(eq(ModelConstants.AUDIT_LOG_TENANT_ID_PROPERTY, tenantId),
+ eq(ModelConstants.AUDIT_LOG_USER_ID_PROPERTY, userId.getId())),
+ pageLink);
+ log.trace("Found audit logs by tenant [{}], user [{}] and pageLink [{}]", tenantId, userId, pageLink);
+ return DaoUtil.convertDataList(entities);
+ }
+
+ @Override
+ public List<AuditLog> findAuditLogsByTenantId(UUID tenantId, TimePageLink pageLink) {
+ log.trace("Try to find audit logs by tenant [{}] and pageLink [{}]", tenantId, pageLink);
+
+ long minPartition;
+ if (pageLink.getStartTime() != null && pageLink.getStartTime() != 0) {
+ minPartition = toPartitionTs(pageLink.getStartTime());
+ } else {
+ minPartition = toPartitionTs(LocalDate.now().minusDays(defaultQueryPeriodInDays).atStartOfDay().toInstant(ZoneOffset.UTC).toEpochMilli());
+ }
+
+ long maxPartition;
+ if (pageLink.getEndTime() != null && pageLink.getEndTime() != 0) {
+ maxPartition = toPartitionTs(pageLink.getEndTime());
+ } else {
+ maxPartition = toPartitionTs(LocalDate.now().atStartOfDay().toInstant(ZoneOffset.UTC).toEpochMilli());
+ }
+
+ List<Long> partitions = fetchPartitions(tenantId, minPartition, maxPartition)
+ .all()
+ .stream()
+ .map(row -> row.getLong(ModelConstants.PARTITION_COLUMN))
+ .collect(Collectors.toList());
+
+ AuditLogQueryCursor cursor = new AuditLogQueryCursor(tenantId, pageLink, partitions);
+ List<AuditLogEntity> entities = fetchSequentiallyWithLimit(cursor);
+ log.trace("Found audit logs by tenant [{}] and pageLink [{}]", tenantId, pageLink);
+ return DaoUtil.convertDataList(entities);
+ }
+
+ private List<AuditLogEntity> fetchSequentiallyWithLimit(AuditLogQueryCursor cursor) {
+ if (cursor.isFull() || !cursor.hasNextPartition()) {
+ return cursor.getData();
+ } else {
+ cursor.addData(findPageWithTimeSearch(AUDIT_LOG_BY_TENANT_ID_CF,
+ Arrays.asList(eq(ModelConstants.AUDIT_LOG_TENANT_ID_PROPERTY, cursor.getTenantId()),
+ eq(ModelConstants.AUDIT_LOG_PARTITION_PROPERTY, cursor.getNextPartition())),
+ cursor.getPageLink()));
+ return fetchSequentiallyWithLimit(cursor);
+ }
+ }
+
+ private ResultSet fetchPartitions(UUID tenantId, long minPartition, long maxPartition) {
+ Select.Where select = QueryBuilder.select(ModelConstants.AUDIT_LOG_PARTITION_PROPERTY).from(ModelConstants.AUDIT_LOG_BY_TENANT_ID_PARTITIONS_CF)
+ .where(eq(ModelConstants.AUDIT_LOG_TENANT_ID_PROPERTY, tenantId));
+ select.and(QueryBuilder.gte(ModelConstants.PARTITION_COLUMN, minPartition));
+ select.and(QueryBuilder.lte(ModelConstants.PARTITION_COLUMN, maxPartition));
+ return getSession().execute(select);
+ }
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/DummyAuditLogServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/audit/DummyAuditLogServiceImpl.java
new file mode 100644
index 0000000..885cd2f
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/audit/DummyAuditLogServiceImpl.java
@@ -0,0 +1,63 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.audit;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.thingsboard.server.common.data.BaseData;
+import org.thingsboard.server.common.data.HasName;
+import org.thingsboard.server.common.data.User;
+import org.thingsboard.server.common.data.audit.ActionStatus;
+import org.thingsboard.server.common.data.audit.ActionType;
+import org.thingsboard.server.common.data.audit.AuditLog;
+import org.thingsboard.server.common.data.id.*;
+import org.thingsboard.server.common.data.page.TimePageData;
+import org.thingsboard.server.common.data.page.TimePageLink;
+
+import java.util.Collections;
+import java.util.List;
+
+@ConditionalOnProperty(prefix = "audit_log", value = "enabled", havingValue = "false")
+public class DummyAuditLogServiceImpl implements AuditLogService {
+
+ @Override
+ public TimePageData<AuditLog> findAuditLogsByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, TimePageLink pageLink) {
+ return new TimePageData<>(null, pageLink);
+ }
+
+ @Override
+ public TimePageData<AuditLog> findAuditLogsByTenantIdAndUserId(TenantId tenantId, UserId userId, TimePageLink pageLink) {
+ return new TimePageData<>(null, pageLink);
+ }
+
+ @Override
+ public TimePageData<AuditLog> findAuditLogsByTenantIdAndEntityId(TenantId tenantId, EntityId entityId, TimePageLink pageLink) {
+ return new TimePageData<>(null, pageLink);
+ }
+
+ @Override
+ public TimePageData<AuditLog> findAuditLogsByTenantId(TenantId tenantId, TimePageLink pageLink) {
+ return new TimePageData<>(null, pageLink);
+ }
+
+ @Override
+ public <E extends BaseData<I> & HasName, I extends UUIDBased & EntityId> ListenableFuture<List<Void>> logEntityAction(TenantId tenantId, CustomerId customerId, UserId userId, String userName, I entityId, E entity, ActionType actionType, Exception e, Object... additionalInfo) {
+ return null;
+ }
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/cassandra/AbstractCassandraCluster.java b/dao/src/main/java/org/thingsboard/server/dao/cassandra/AbstractCassandraCluster.java
index f37fa93..5d6c8e6 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/cassandra/AbstractCassandraCluster.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/cassandra/AbstractCassandraCluster.java
@@ -20,9 +20,6 @@ import com.datastax.driver.core.*;
import com.datastax.driver.core.ProtocolOptions.Compression;
import com.datastax.driver.mapping.Mapper;
import com.datastax.driver.mapping.MappingManager;
-import lombok.AccessLevel;
-import lombok.Data;
-import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
@@ -36,7 +33,6 @@ import java.util.Collections;
import java.util.List;
@Slf4j
-@Data
public abstract class AbstractCassandraCluster {
private static final String COMMA = ",";
@@ -77,7 +73,7 @@ public abstract class AbstractCassandraCluster {
private Cluster cluster;
private Cluster.Builder clusterBuilder;
- @Getter(AccessLevel.NONE) private Session session;
+ private Session session;
private MappingManager mappingManager;
@@ -115,6 +111,10 @@ public abstract class AbstractCassandraCluster {
}
}
+ public Cluster getCluster() {
+ return cluster;
+ }
+
public Session getSession() {
if (!isInstall()) {
return session;
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
index 9b596fc..66e03b0 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
@@ -44,6 +44,13 @@ public class ModelConstants {
public static final String ADDITIONAL_INFO_PROPERTY = "additional_info";
public static final String ENTITY_TYPE_PROPERTY = "entity_type";
+ public static final String ENTITY_TYPE_COLUMN = ENTITY_TYPE_PROPERTY;
+ public static final String ENTITY_ID_COLUMN = "entity_id";
+ public static final String ATTRIBUTE_TYPE_COLUMN = "attribute_type";
+ public static final String ATTRIBUTE_KEY_COLUMN = "attribute_key";
+ public static final String LAST_UPDATE_TS_COLUMN = "last_update_ts";
+
+
/**
* Cassandra user constants.
*/
@@ -135,6 +142,31 @@ public class ModelConstants {
public static final String DEVICE_TYPES_BY_TENANT_VIEW_NAME = "device_types_by_tenant";
/**
+ * Cassandra audit log constants.
+ */
+ public static final String AUDIT_LOG_COLUMN_FAMILY_NAME = "audit_log";
+
+ public static final String AUDIT_LOG_BY_ENTITY_ID_CF = "audit_log_by_entity_id";
+ public static final String AUDIT_LOG_BY_CUSTOMER_ID_CF = "audit_log_by_customer_id";
+ public static final String AUDIT_LOG_BY_USER_ID_CF = "audit_log_by_user_id";
+ public static final String AUDIT_LOG_BY_TENANT_ID_CF = "audit_log_by_tenant_id";
+ public static final String AUDIT_LOG_BY_TENANT_ID_PARTITIONS_CF = "audit_log_by_tenant_id_partitions";
+
+ public static final String AUDIT_LOG_ID_PROPERTY = ID_PROPERTY;
+ public static final String AUDIT_LOG_TENANT_ID_PROPERTY = TENANT_ID_PROPERTY;
+ public static final String AUDIT_LOG_CUSTOMER_ID_PROPERTY = CUSTOMER_ID_PROPERTY;
+ public static final String AUDIT_LOG_ENTITY_TYPE_PROPERTY = ENTITY_TYPE_PROPERTY;
+ public static final String AUDIT_LOG_ENTITY_ID_PROPERTY = ENTITY_ID_COLUMN;
+ public static final String AUDIT_LOG_ENTITY_NAME_PROPERTY = "entity_name";
+ public static final String AUDIT_LOG_USER_ID_PROPERTY = USER_ID_PROPERTY;
+ public static final String AUDIT_LOG_PARTITION_PROPERTY = "partition";
+ public static final String AUDIT_LOG_USER_NAME_PROPERTY = "user_name";
+ public static final String AUDIT_LOG_ACTION_TYPE_PROPERTY = "action_type";
+ public static final String AUDIT_LOG_ACTION_DATA_PROPERTY = "action_data";
+ public static final String AUDIT_LOG_ACTION_STATUS_PROPERTY = "action_status";
+ public static final String AUDIT_LOG_ACTION_FAILURE_DETAILS_PROPERTY = "action_failure_details";
+
+ /**
* Cassandra asset constants.
*/
public static final String ASSET_COLUMN_FAMILY_NAME = "asset";
@@ -310,13 +342,6 @@ public class ModelConstants {
public static final String TS_KV_PARTITIONS_CF = "ts_kv_partitions_cf";
public static final String TS_KV_LATEST_CF = "ts_kv_latest_cf";
-
- public static final String ENTITY_TYPE_COLUMN = ENTITY_TYPE_PROPERTY;
- public static final String ENTITY_ID_COLUMN = "entity_id";
- public static final String ATTRIBUTE_TYPE_COLUMN = "attribute_type";
- public static final String ATTRIBUTE_KEY_COLUMN = "attribute_key";
- public static final String LAST_UPDATE_TS_COLUMN = "last_update_ts";
-
public static final String PARTITION_COLUMN = "partition";
public static final String KEY_COLUMN = "key";
public static final String TS_COLUMN = "ts";
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/AuditLogEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/AuditLogEntity.java
new file mode 100644
index 0000000..4ad54f5
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/AuditLogEntity.java
@@ -0,0 +1,139 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.model.nosql;
+
+import com.datastax.driver.core.utils.UUIDs;
+import com.datastax.driver.mapping.annotations.Column;
+import com.datastax.driver.mapping.annotations.Table;
+import com.fasterxml.jackson.databind.JsonNode;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.audit.ActionStatus;
+import org.thingsboard.server.common.data.audit.ActionType;
+import org.thingsboard.server.common.data.audit.AuditLog;
+import org.thingsboard.server.common.data.id.*;
+import org.thingsboard.server.dao.model.BaseEntity;
+import org.thingsboard.server.dao.model.type.ActionStatusCodec;
+import org.thingsboard.server.dao.model.type.ActionTypeCodec;
+import org.thingsboard.server.dao.model.type.EntityTypeCodec;
+import org.thingsboard.server.dao.model.type.JsonCodec;
+
+import java.util.UUID;
+
+import static org.thingsboard.server.dao.model.ModelConstants.*;
+
+@Table(name = AUDIT_LOG_COLUMN_FAMILY_NAME)
+@Data
+@NoArgsConstructor
+public class AuditLogEntity implements BaseEntity<AuditLog> {
+
+ @Column(name = ID_PROPERTY)
+ private UUID id;
+
+ @Column(name = AUDIT_LOG_TENANT_ID_PROPERTY)
+ private UUID tenantId;
+
+ @Column(name = AUDIT_LOG_CUSTOMER_ID_PROPERTY)
+ private UUID customerId;
+
+ @Column(name = AUDIT_LOG_ENTITY_TYPE_PROPERTY, codec = EntityTypeCodec.class)
+ private EntityType entityType;
+
+ @Column(name = AUDIT_LOG_ENTITY_ID_PROPERTY)
+ private UUID entityId;
+
+ @Column(name = AUDIT_LOG_ENTITY_NAME_PROPERTY)
+ private String entityName;
+
+ @Column(name = AUDIT_LOG_USER_ID_PROPERTY)
+ private UUID userId;
+
+ @Column(name = AUDIT_LOG_USER_NAME_PROPERTY)
+ private String userName;
+
+ @Column(name = AUDIT_LOG_ACTION_TYPE_PROPERTY, codec = ActionTypeCodec.class)
+ private ActionType actionType;
+
+ @Column(name = AUDIT_LOG_ACTION_DATA_PROPERTY, codec = JsonCodec.class)
+ private JsonNode actionData;
+
+ @Column(name = AUDIT_LOG_ACTION_STATUS_PROPERTY, codec = ActionStatusCodec.class)
+ private ActionStatus actionStatus;
+
+ @Column(name = AUDIT_LOG_ACTION_FAILURE_DETAILS_PROPERTY)
+ private String actionFailureDetails;
+
+ @Override
+ public UUID getId() {
+ return id;
+ }
+
+ @Override
+ public void setId(UUID id) {
+ this.id = id;
+ }
+
+ public AuditLogEntity(AuditLog auditLog) {
+ if (auditLog.getId() != null) {
+ this.id = auditLog.getId().getId();
+ }
+ if (auditLog.getTenantId() != null) {
+ this.tenantId = auditLog.getTenantId().getId();
+ }
+ if (auditLog.getEntityId() != null) {
+ this.entityType = auditLog.getEntityId().getEntityType();
+ this.entityId = auditLog.getEntityId().getId();
+ }
+ if (auditLog.getCustomerId() != null) {
+ this.customerId = auditLog.getCustomerId().getId();
+ }
+ if (auditLog.getUserId() != null) {
+ this.userId = auditLog.getUserId().getId();
+ }
+ this.entityName = auditLog.getEntityName();
+ this.userName = auditLog.getUserName();
+ this.actionType = auditLog.getActionType();
+ this.actionData = auditLog.getActionData();
+ this.actionStatus = auditLog.getActionStatus();
+ this.actionFailureDetails = auditLog.getActionFailureDetails();
+ }
+
+ @Override
+ public AuditLog toData() {
+ AuditLog auditLog = new AuditLog(new AuditLogId(id));
+ auditLog.setCreatedTime(UUIDs.unixTimestamp(id));
+ if (tenantId != null) {
+ auditLog.setTenantId(new TenantId(tenantId));
+ }
+ if (entityId != null && entityType != null) {
+ auditLog.setEntityId(EntityIdFactory.getByTypeAndUuid(entityType, entityId));
+ }
+ if (customerId != null) {
+ auditLog.setCustomerId(new CustomerId(customerId));
+ }
+ if (userId != null) {
+ auditLog.setUserId(new UserId(userId));
+ }
+ auditLog.setEntityName(this.entityName);
+ auditLog.setUserName(this.userName);
+ auditLog.setActionType(this.actionType);
+ auditLog.setActionData(this.actionData);
+ auditLog.setActionStatus(this.actionStatus);
+ auditLog.setActionFailureDetails(this.actionFailureDetails);
+ return auditLog;
+ }
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AuditLogEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AuditLogEntity.java
new file mode 100644
index 0000000..6dd70b2
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AuditLogEntity.java
@@ -0,0 +1,135 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.model.sql;
+
+import com.datastax.driver.core.utils.UUIDs;
+import com.fasterxml.jackson.databind.JsonNode;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.hibernate.annotations.Type;
+import org.hibernate.annotations.TypeDef;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.audit.ActionStatus;
+import org.thingsboard.server.common.data.audit.ActionType;
+import org.thingsboard.server.common.data.audit.AuditLog;
+import org.thingsboard.server.common.data.id.*;
+import org.thingsboard.server.dao.model.BaseEntity;
+import org.thingsboard.server.dao.model.BaseSqlEntity;
+import org.thingsboard.server.dao.model.ModelConstants;
+import org.thingsboard.server.dao.util.mapping.JsonStringType;
+
+import javax.persistence.*;
+
+import static org.thingsboard.server.dao.model.ModelConstants.*;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+@Entity
+@TypeDef(name = "json", typeClass = JsonStringType.class)
+@Table(name = ModelConstants.AUDIT_LOG_COLUMN_FAMILY_NAME)
+public class AuditLogEntity extends BaseSqlEntity<AuditLog> implements BaseEntity<AuditLog> {
+
+ @Column(name = AUDIT_LOG_TENANT_ID_PROPERTY)
+ private String tenantId;
+
+ @Column(name = AUDIT_LOG_CUSTOMER_ID_PROPERTY)
+ private String customerId;
+
+ @Enumerated(EnumType.STRING)
+ @Column(name = AUDIT_LOG_ENTITY_TYPE_PROPERTY)
+ private EntityType entityType;
+
+ @Column(name = AUDIT_LOG_ENTITY_ID_PROPERTY)
+ private String entityId;
+
+ @Column(name = AUDIT_LOG_ENTITY_NAME_PROPERTY)
+ private String entityName;
+
+ @Column(name = AUDIT_LOG_USER_ID_PROPERTY)
+ private String userId;
+
+ @Column(name = AUDIT_LOG_USER_NAME_PROPERTY)
+ private String userName;
+
+ @Enumerated(EnumType.STRING)
+ @Column(name = AUDIT_LOG_ACTION_TYPE_PROPERTY)
+ private ActionType actionType;
+
+ @Type(type = "json")
+ @Column(name = AUDIT_LOG_ACTION_DATA_PROPERTY)
+ private JsonNode actionData;
+
+ @Enumerated(EnumType.STRING)
+ @Column(name = AUDIT_LOG_ACTION_STATUS_PROPERTY)
+ private ActionStatus actionStatus;
+
+ @Column(name = AUDIT_LOG_ACTION_FAILURE_DETAILS_PROPERTY)
+ private String actionFailureDetails;
+
+ public AuditLogEntity() {
+ super();
+ }
+
+ public AuditLogEntity(AuditLog auditLog) {
+ if (auditLog.getId() != null) {
+ this.setId(auditLog.getId().getId());
+ }
+ if (auditLog.getTenantId() != null) {
+ this.tenantId = toString(auditLog.getTenantId().getId());
+ }
+ if (auditLog.getCustomerId() != null) {
+ this.customerId = toString(auditLog.getCustomerId().getId());
+ }
+ if (auditLog.getEntityId() != null) {
+ this.entityId = toString(auditLog.getEntityId().getId());
+ this.entityType = auditLog.getEntityId().getEntityType();
+ }
+ if (auditLog.getUserId() != null) {
+ this.userId = toString(auditLog.getUserId().getId());
+ }
+ this.entityName = auditLog.getEntityName();
+ this.userName = auditLog.getUserName();
+ this.actionType = auditLog.getActionType();
+ this.actionData = auditLog.getActionData();
+ this.actionStatus = auditLog.getActionStatus();
+ this.actionFailureDetails = auditLog.getActionFailureDetails();
+ }
+
+ @Override
+ public AuditLog toData() {
+ AuditLog auditLog = new AuditLog(new AuditLogId(getId()));
+ auditLog.setCreatedTime(UUIDs.unixTimestamp(getId()));
+ if (tenantId != null) {
+ auditLog.setTenantId(new TenantId(toUUID(tenantId)));
+ }
+ if (customerId != null) {
+ auditLog.setCustomerId(new CustomerId(toUUID(customerId)));
+ }
+ if (entityId != null) {
+ auditLog.setEntityId(EntityIdFactory.getByTypeAndId(entityType.name(), toUUID(entityId).toString()));
+ }
+ if (userId != null) {
+ auditLog.setUserId(new UserId(toUUID(entityId)));
+ }
+ auditLog.setEntityName(this.entityName);
+ auditLog.setUserName(this.userName);
+ auditLog.setActionType(this.actionType);
+ auditLog.setActionData(this.actionData);
+ auditLog.setActionStatus(this.actionStatus);
+ auditLog.setActionFailureDetails(this.actionFailureDetails);
+ return auditLog;
+ }
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/type/ActionStatusCodec.java b/dao/src/main/java/org/thingsboard/server/dao/model/type/ActionStatusCodec.java
new file mode 100644
index 0000000..a207c40
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/type/ActionStatusCodec.java
@@ -0,0 +1,26 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.model.type;
+
+import com.datastax.driver.extras.codecs.enums.EnumNameCodec;
+import org.thingsboard.server.common.data.audit.ActionStatus;
+
+public class ActionStatusCodec extends EnumNameCodec<ActionStatus> {
+
+ public ActionStatusCodec() {
+ super(ActionStatus.class);
+ }
+}
\ No newline at end of file
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/type/ActionTypeCodec.java b/dao/src/main/java/org/thingsboard/server/dao/model/type/ActionTypeCodec.java
new file mode 100644
index 0000000..9f22d5f
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/type/ActionTypeCodec.java
@@ -0,0 +1,26 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.model.type;
+
+import com.datastax.driver.extras.codecs.enums.EnumNameCodec;
+import org.thingsboard.server.common.data.audit.ActionType;
+
+public class ActionTypeCodec extends EnumNameCodec<ActionType> {
+
+ public ActionTypeCodec() {
+ super(ActionType.class);
+ }
+}
\ No newline at end of file
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/audit/AuditLogRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/audit/AuditLogRepository.java
new file mode 100644
index 0000000..52dbd67
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/audit/AuditLogRepository.java
@@ -0,0 +1,24 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.sql.audit;
+
+import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
+import org.springframework.data.repository.CrudRepository;
+import org.thingsboard.server.dao.model.sql.AuditLogEntity;
+
+public interface AuditLogRepository extends CrudRepository<AuditLogEntity, String>, JpaSpecificationExecutor<AuditLogEntity> {
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/audit/JpaAuditLogDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/audit/JpaAuditLogDao.java
new file mode 100644
index 0000000..14b5271
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/audit/JpaAuditLogDao.java
@@ -0,0 +1,155 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.sql.audit;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.common.util.concurrent.MoreExecutors;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.jpa.domain.Specification;
+import org.springframework.data.repository.CrudRepository;
+import org.springframework.stereotype.Component;
+import org.thingsboard.server.common.data.UUIDConverter;
+import org.thingsboard.server.common.data.audit.AuditLog;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.UserId;
+import org.thingsboard.server.common.data.page.TimePageLink;
+import org.thingsboard.server.dao.DaoUtil;
+import org.thingsboard.server.dao.audit.AuditLogDao;
+import org.thingsboard.server.dao.model.sql.AuditLogEntity;
+import org.thingsboard.server.dao.sql.JpaAbstractDao;
+import org.thingsboard.server.dao.sql.JpaAbstractSearchTimeDao;
+import org.thingsboard.server.dao.util.SqlDao;
+
+import javax.annotation.PreDestroy;
+import javax.persistence.criteria.Predicate;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.Executors;
+
+import static org.springframework.data.jpa.domain.Specifications.where;
+import static org.thingsboard.server.dao.model.ModelConstants.ID_PROPERTY;
+
+@Component
+@SqlDao
+public class JpaAuditLogDao extends JpaAbstractDao<AuditLogEntity, AuditLog> implements AuditLogDao {
+
+ private ListeningExecutorService insertService = MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor());
+
+ @Autowired
+ private AuditLogRepository auditLogRepository;
+
+ @Override
+ protected Class<AuditLogEntity> getEntityClass() {
+ return AuditLogEntity.class;
+ }
+
+ @Override
+ protected CrudRepository<AuditLogEntity, String> getCrudRepository() {
+ return auditLogRepository;
+ }
+
+ @PreDestroy
+ void onDestroy() {
+ insertService.shutdown();
+ }
+
+ @Override
+ public ListenableFuture<Void> saveByTenantId(AuditLog auditLog) {
+ return insertService.submit(() -> {
+ save(auditLog);
+ return null;
+ });
+ }
+
+ @Override
+ public ListenableFuture<Void> saveByTenantIdAndEntityId(AuditLog auditLog) {
+ return insertService.submit(() -> null);
+ }
+
+ @Override
+ public ListenableFuture<Void> saveByTenantIdAndCustomerId(AuditLog auditLog) {
+ return insertService.submit(() -> null);
+ }
+
+ @Override
+ public ListenableFuture<Void> saveByTenantIdAndUserId(AuditLog auditLog) {
+ return insertService.submit(() -> null);
+ }
+
+ @Override
+ public ListenableFuture<Void> savePartitionsByTenantId(AuditLog auditLog) {
+ return insertService.submit(() -> null);
+ }
+
+ @Override
+ public List<AuditLog> findAuditLogsByTenantIdAndEntityId(UUID tenantId, EntityId entityId, TimePageLink pageLink) {
+ return findAuditLogs(tenantId, entityId, null, null, pageLink);
+ }
+
+ @Override
+ public List<AuditLog> findAuditLogsByTenantIdAndCustomerId(UUID tenantId, CustomerId customerId, TimePageLink pageLink) {
+ return findAuditLogs(tenantId, null, customerId, null, pageLink);
+ }
+
+ @Override
+ public List<AuditLog> findAuditLogsByTenantIdAndUserId(UUID tenantId, UserId userId, TimePageLink pageLink) {
+ return findAuditLogs(tenantId, null, null, userId, pageLink);
+ }
+
+ @Override
+ public List<AuditLog> findAuditLogsByTenantId(UUID tenantId, TimePageLink pageLink) {
+ return findAuditLogs(tenantId, null, null, null, pageLink);
+ }
+
+ private List<AuditLog> findAuditLogs(UUID tenantId, EntityId entityId, CustomerId customerId, UserId userId, TimePageLink pageLink) {
+ Specification<AuditLogEntity> timeSearchSpec = JpaAbstractSearchTimeDao.getTimeSearchPageSpec(pageLink, "id");
+ Specification<AuditLogEntity> fieldsSpec = getEntityFieldsSpec(tenantId, entityId, customerId, userId);
+ Sort.Direction sortDirection = pageLink.isAscOrder() ? Sort.Direction.ASC : Sort.Direction.DESC;
+ Pageable pageable = new PageRequest(0, pageLink.getLimit(), sortDirection, ID_PROPERTY);
+ return DaoUtil.convertDataList(auditLogRepository.findAll(where(timeSearchSpec).and(fieldsSpec), pageable).getContent());
+ }
+
+ private Specification<AuditLogEntity> getEntityFieldsSpec(UUID tenantId, EntityId entityId, CustomerId customerId, UserId userId) {
+ return (root, criteriaQuery, criteriaBuilder) -> {
+ List<Predicate> predicates = new ArrayList<>();
+ if (tenantId != null) {
+ Predicate tenantIdPredicate = criteriaBuilder.equal(root.get("tenantId"), UUIDConverter.fromTimeUUID(tenantId));
+ predicates.add(tenantIdPredicate);
+ }
+ if (entityId != null) {
+ Predicate entityTypePredicate = criteriaBuilder.equal(root.get("entityType"), entityId.getEntityType());
+ predicates.add(entityTypePredicate);
+ Predicate entityIdPredicate = criteriaBuilder.equal(root.get("entityId"), UUIDConverter.fromTimeUUID(entityId.getId()));
+ predicates.add(entityIdPredicate);
+ }
+ if (customerId != null) {
+ Predicate tenantIdPredicate = criteriaBuilder.equal(root.get("customerId"), UUIDConverter.fromTimeUUID(customerId.getId()));
+ predicates.add(tenantIdPredicate);
+ }
+ if (userId != null) {
+ Predicate tenantIdPredicate = criteriaBuilder.equal(root.get("userId"), UUIDConverter.fromTimeUUID(userId.getId()));
+ predicates.add(tenantIdPredicate);
+ }
+ return criteriaBuilder.and(predicates.toArray(new Predicate[]{}));
+ };
+ }
+}
dao/src/main/resources/cassandra/schema.cql 75(+75 -0)
diff --git a/dao/src/main/resources/cassandra/schema.cql b/dao/src/main/resources/cassandra/schema.cql
index dda8067..bc7884d 100644
--- a/dao/src/main/resources/cassandra/schema.cql
+++ b/dao/src/main/resources/cassandra/schema.cql
@@ -548,3 +548,78 @@ CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.event_by_id AS
AND event_type IS NOT NULL AND event_uid IS NOT NULL
PRIMARY KEY ((tenant_id, entity_type, entity_id), id, event_type, event_uid)
WITH CLUSTERING ORDER BY (id ASC, event_type ASC, event_uid ASC);
+
+
+CREATE TABLE IF NOT EXISTS thingsboard.audit_log_by_entity_id (
+ tenant_id timeuuid,
+ id timeuuid,
+ customer_id timeuuid,
+ entity_id timeuuid,
+ entity_type text,
+ entity_name text,
+ user_id timeuuid,
+ user_name text,
+ action_type text,
+ action_data text,
+ action_status text,
+ action_failure_details text,
+ PRIMARY KEY ((tenant_id, entity_id, entity_type), id)
+);
+
+CREATE TABLE IF NOT EXISTS thingsboard.audit_log_by_customer_id (
+ tenant_id timeuuid,
+ id timeuuid,
+ customer_id timeuuid,
+ entity_id timeuuid,
+ entity_type text,
+ entity_name text,
+ user_id timeuuid,
+ user_name text,
+ action_type text,
+ action_data text,
+ action_status text,
+ action_failure_details text,
+ PRIMARY KEY ((tenant_id, customer_id), id)
+);
+
+CREATE TABLE IF NOT EXISTS thingsboard.audit_log_by_user_id (
+ tenant_id timeuuid,
+ id timeuuid,
+ customer_id timeuuid,
+ entity_id timeuuid,
+ entity_type text,
+ entity_name text,
+ user_id timeuuid,
+ user_name text,
+ action_type text,
+ action_data text,
+ action_status text,
+ action_failure_details text,
+ PRIMARY KEY ((tenant_id, user_id), id)
+);
+
+
+
+CREATE TABLE IF NOT EXISTS thingsboard.audit_log_by_tenant_id (
+ tenant_id timeuuid,
+ id timeuuid,
+ partition bigint,
+ customer_id timeuuid,
+ entity_id timeuuid,
+ entity_type text,
+ entity_name text,
+ user_id timeuuid,
+ user_name text,
+ action_type text,
+ action_data text,
+ action_status text,
+ action_failure_details text,
+ PRIMARY KEY ((tenant_id, partition), id)
+);
+
+CREATE TABLE IF NOT EXISTS thingsboard.audit_log_by_tenant_id_partitions (
+ tenant_id timeuuid,
+ partition bigint,
+ PRIMARY KEY (( tenant_id ), partition)
+) WITH CLUSTERING ORDER BY ( partition ASC )
+AND compaction = { 'class' : 'LeveledCompactionStrategy' };
\ No newline at end of file
dao/src/main/resources/sql/schema.sql 15(+15 -0)
diff --git a/dao/src/main/resources/sql/schema.sql b/dao/src/main/resources/sql/schema.sql
index 26b314c..1f739f8 100644
--- a/dao/src/main/resources/sql/schema.sql
+++ b/dao/src/main/resources/sql/schema.sql
@@ -47,6 +47,21 @@ CREATE TABLE IF NOT EXISTS asset (
type varchar(255)
);
+CREATE TABLE IF NOT EXISTS audit_log (
+ id varchar(31) NOT NULL CONSTRAINT audit_log_pkey PRIMARY KEY,
+ tenant_id varchar(31),
+ customer_id varchar(31),
+ entity_id varchar(31),
+ entity_type varchar(255),
+ entity_name varchar(255),
+ user_id varchar(31),
+ user_name varchar(255),
+ action_type varchar(255),
+ action_data varchar(1000000),
+ action_status varchar(255),
+ action_failure_details varchar(1000000)
+);
+
CREATE TABLE IF NOT EXISTS attribute_kv (
entity_type varchar(255),
entity_id varchar(31),
diff --git a/dao/src/test/java/org/thingsboard/server/dao/JpaDaoTestSuite.java b/dao/src/test/java/org/thingsboard/server/dao/JpaDaoTestSuite.java
index d936a38..817f6ee 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/JpaDaoTestSuite.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/JpaDaoTestSuite.java
@@ -24,7 +24,7 @@ import java.util.Arrays;
@RunWith(ClasspathSuite.class)
@ClassnameFilters({
- "org.thingsboard.server.dao.sql.*AAATest"
+ "org.thingsboard.server.dao.sql.*THIS_MUST_BE_FIXED_Test"
})
public class JpaDaoTestSuite {
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java
index e0b5f24..7fc9c67 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java
@@ -22,6 +22,7 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.TextNode;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.annotation.DirtiesContext;
@@ -29,6 +30,7 @@ import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.context.support.AnnotationConfigContextLoader;
import org.thingsboard.server.common.data.BaseData;
+import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.Event;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
@@ -40,6 +42,8 @@ import org.thingsboard.server.common.data.plugin.PluginMetaData;
import org.thingsboard.server.common.data.rule.RuleMetaData;
import org.thingsboard.server.dao.alarm.AlarmService;
import org.thingsboard.server.dao.asset.AssetService;
+import org.thingsboard.server.dao.audit.AuditLogLevelFilter;
+import org.thingsboard.server.dao.audit.AuditLogLevelMask;
import org.thingsboard.server.dao.component.ComponentDescriptorService;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.dashboard.DashboardService;
@@ -58,6 +62,8 @@ import org.thingsboard.server.dao.widget.WidgetsBundleService;
import java.io.IOException;
import java.util.Comparator;
+import java.util.HashMap;
+import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ThreadLocalRandom;
@@ -227,4 +233,14 @@ public abstract class AbstractServiceTest {
oNode.set("configuration", readFromResource(configuration));
return oNode;
}
+
+ @Bean
+ public AuditLogLevelFilter auditLogLevelFilter() {
+ Map<String,String> mask = new HashMap<>();
+ for (EntityType entityType : EntityType.values()) {
+ mask.put(entityType.name().toLowerCase(), AuditLogLevelMask.RW.name());
+ }
+ return new AuditLogLevelFilter(mask);
+ }
+
}
diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/audit/JpaAuditLogDaoTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/audit/JpaAuditLogDaoTest.java
new file mode 100644
index 0000000..ed174a7
--- /dev/null
+++ b/dao/src/test/java/org/thingsboard/server/dao/sql/audit/JpaAuditLogDaoTest.java
@@ -0,0 +1,21 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.sql.audit;
+
+import org.thingsboard.server.dao.AbstractJpaDaoTest;
+
+public class JpaAuditLogDaoTest extends AbstractJpaDaoTest {
+}
diff --git a/dao/src/test/resources/application-test.properties b/dao/src/test/resources/application-test.properties
index 3466d66..21f1794 100644
--- a/dao/src/test/resources/application-test.properties
+++ b/dao/src/test/resources/application-test.properties
@@ -4,6 +4,10 @@ zk.zk_dir=/thingsboard
updates.enabled=false
+audit_log.enabled=true
+audit_log.by_tenant_partitioning=MONTHS
+audit_log.default_query_period=30
+
cache.type=caffeine
#cache.type=redis
@@ -16,7 +20,10 @@ caffeine.specs.deviceCredentials.maxSize=100000
caffeine.specs.devices.timeToLiveInMinutes=1440
caffeine.specs.devices.maxSize=100000
+caching.specs.devices.timeToLiveInMinutes=1440
+caching.specs.devices.maxSize=100000
+
redis.connection.host=localhost
redis.connection.port=6379
redis.connection.db=0
-redis.connection.password=
\ No newline at end of file
+redis.connection.password=
diff --git a/dao/src/test/resources/sql/drop-all-tables.sql b/dao/src/test/resources/sql/drop-all-tables.sql
index 49a3774..dfdc90f 100644
--- a/dao/src/test/resources/sql/drop-all-tables.sql
+++ b/dao/src/test/resources/sql/drop-all-tables.sql
@@ -1,6 +1,7 @@
DROP TABLE IF EXISTS admin_settings;
DROP TABLE IF EXISTS alarm;
DROP TABLE IF EXISTS asset;
+DROP TABLE IF EXISTS audit_log;
DROP TABLE IF EXISTS attribute_kv;
DROP TABLE IF EXISTS component_descriptor;
DROP TABLE IF EXISTS customer;
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/ToDeviceRpcRequest.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/ToDeviceRpcRequest.java
index f9269de..b81780b 100644
--- a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/ToDeviceRpcRequest.java
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/ToDeviceRpcRequest.java
@@ -18,6 +18,7 @@ package org.thingsboard.server.extensions.api.plugins.msg;
import lombok.Data;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.extensions.api.plugins.PluginApiCallSecurityContext;
import java.io.Serializable;
import java.util.UUID;
@@ -28,6 +29,7 @@ import java.util.UUID;
@Data
public class ToDeviceRpcRequest implements Serializable {
private final UUID id;
+ private final PluginApiCallSecurityContext securityCtx;
private final TenantId tenantId;
private final DeviceId deviceId;
private final boolean oneway;
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginApiCallSecurityContext.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginApiCallSecurityContext.java
index 32600d3..3ca662d 100644
--- a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginApiCallSecurityContext.java
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginApiCallSecurityContext.java
@@ -15,10 +15,7 @@
*/
package org.thingsboard.server.extensions.api.plugins;
-import org.thingsboard.server.common.data.id.CustomerId;
-import org.thingsboard.server.common.data.id.EntityId;
-import org.thingsboard.server.common.data.id.PluginId;
-import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.id.*;
import java.io.Serializable;
@@ -30,13 +27,18 @@ public final class PluginApiCallSecurityContext implements Serializable {
private final PluginId pluginId;
private final TenantId tenantId;
private final CustomerId customerId;
+ private final UserId userId;
+ private final String userName;
- public PluginApiCallSecurityContext(TenantId pluginTenantId, PluginId pluginId, TenantId tenantId, CustomerId customerId) {
+ public PluginApiCallSecurityContext(TenantId pluginTenantId, PluginId pluginId, TenantId tenantId, CustomerId customerId,
+ UserId userId, String userName) {
super();
this.pluginTenantId = pluginTenantId;
this.pluginId = pluginId;
this.tenantId = tenantId;
this.customerId = customerId;
+ this.userId = userId;
+ this.userName = userName;
}
public TenantId getPluginTenantId(){
@@ -67,4 +69,12 @@ public final class PluginApiCallSecurityContext implements Serializable {
return customerId;
}
+ public UserId getUserId() {
+ return userId;
+ }
+
+ public String getUserName() {
+ return userName;
+ }
+
}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginContext.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginContext.java
index c825594..d1a2927 100644
--- a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginContext.java
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginContext.java
@@ -24,9 +24,7 @@ import org.thingsboard.server.common.data.kv.TsKvQuery;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import org.thingsboard.server.common.msg.cluster.ServerAddress;
-import org.thingsboard.server.extensions.api.plugins.msg.PluginToRuleMsg;
-import org.thingsboard.server.extensions.api.plugins.msg.TimeoutMsg;
-import org.thingsboard.server.extensions.api.plugins.msg.ToDeviceRpcRequest;
+import org.thingsboard.server.extensions.api.plugins.msg.*;
import org.thingsboard.server.extensions.api.plugins.rpc.RpcMsg;
import org.thingsboard.server.extensions.api.plugins.ws.PluginWebsocketSessionRef;
import org.thingsboard.server.extensions.api.plugins.ws.msg.PluginWebsocketMsg;
@@ -60,6 +58,7 @@ public interface PluginContext {
void scheduleTimeoutMsg(TimeoutMsg<?> timeoutMsg);
+ void logRpcRequest(PluginApiCallSecurityContext ctx, DeviceId deviceId, ToDeviceRpcRequestBody body, boolean oneWay, Optional<RpcError> rpcError, Exception e);
/*
Websocket API
@@ -96,6 +95,12 @@ public interface PluginContext {
Attributes API
*/
+ void logAttributesUpdated(PluginApiCallSecurityContext ctx, EntityId entityId, String attributeType, List<AttributeKvEntry> attributes, Exception e);
+
+ void logAttributesDeleted(PluginApiCallSecurityContext ctx, EntityId entityId, String attributeType, List<String> keys, Exception e);
+
+ void logAttributesRead(PluginApiCallSecurityContext ctx, EntityId entityId, String attributeType, List<String> keys, Exception e);
+
void saveAttributes(TenantId tenantId, EntityId entityId, String attributeType, List<AttributeKvEntry> attributes, PluginCallback<Void> callback);
void removeAttributes(TenantId tenantId, EntityId entityId, String scope, List<String> attributeKeys, PluginCallback<Void> callback);
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/messaging/DeviceMessagingRuleMsgHandler.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/messaging/DeviceMessagingRuleMsgHandler.java
index 4684337..7438a27 100644
--- a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/messaging/DeviceMessagingRuleMsgHandler.java
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/messaging/DeviceMessagingRuleMsgHandler.java
@@ -152,7 +152,7 @@ public class DeviceMessagingRuleMsgHandler implements RuleMsgHandler {
pendingMsgs.put(uid, requestMd);
log.trace("[{}] Forwarding {} to [{}]", uid, params, targetDeviceId);
ToDeviceRpcRequestBody requestBody = new ToDeviceRpcRequestBody(ON_MSG_METHOD_NAME, GSON.toJson(params.get("body")));
- ctx.sendRpcRequest(new ToDeviceRpcRequest(uid, targetDevice.getTenantId(), targetDeviceId, oneWay, System.currentTimeMillis() + timeout, requestBody));
+ ctx.sendRpcRequest(new ToDeviceRpcRequest(uid, null, targetDevice.getTenantId(), targetDeviceId, oneWay, System.currentTimeMillis() + timeout, requestBody));
} else {
replyWithError(ctx, requestMd, RpcError.FORBIDDEN);
}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/handlers/RpcRestMsgHandler.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/handlers/RpcRestMsgHandler.java
index f88b796..476154d 100644
--- a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/handlers/RpcRestMsgHandler.java
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/handlers/RpcRestMsgHandler.java
@@ -94,11 +94,12 @@ public class RpcRestMsgHandler extends DefaultRestMsgHandler {
private boolean handleDeviceRPCRequest(PluginContext ctx, final PluginRestMsg msg, TenantId tenantId, DeviceId deviceId, RpcRequest cmd, boolean oneWay) throws JsonProcessingException {
long timeout = System.currentTimeMillis() + (cmd.getTimeout() != null ? cmd.getTimeout() : defaultTimeout);
+ ToDeviceRpcRequestBody body = new ToDeviceRpcRequestBody(cmd.getMethodName(), cmd.getRequestData());
ctx.checkAccess(deviceId, new PluginCallback<Void>() {
@Override
public void onSuccess(PluginContext ctx, Void value) {
- ToDeviceRpcRequestBody body = new ToDeviceRpcRequestBody(cmd.getMethodName(), cmd.getRequestData());
ToDeviceRpcRequest rpcRequest = new ToDeviceRpcRequest(UUID.randomUUID(),
+ msg.getSecurityCtx(),
tenantId,
deviceId,
oneWay,
@@ -116,15 +117,17 @@ public class RpcRestMsgHandler extends DefaultRestMsgHandler {
} else {
response = new ResponseEntity(HttpStatus.UNAUTHORIZED);
}
+ ctx.logRpcRequest(msg.getSecurityCtx(), deviceId, body, oneWay, Optional.empty(), e);
msg.getResponseHolder().setResult(response);
}
});
return true;
}
- public void reply(PluginContext ctx, DeferredResult<ResponseEntity> responseWriter, FromDeviceRpcResponse response) {
+ public void reply(PluginContext ctx, ToDeviceRpcRequest rpcRequest, DeferredResult<ResponseEntity> responseWriter, FromDeviceRpcResponse response) {
Optional<RpcError> rpcError = response.getError();
if (rpcError.isPresent()) {
+ ctx.logRpcRequest(rpcRequest.getSecurityCtx(), rpcRequest.getDeviceId(), rpcRequest.getBody(), rpcRequest.isOneway(), rpcError, null);
RpcError error = rpcError.get();
switch (error) {
case TIMEOUT:
@@ -142,12 +145,15 @@ public class RpcRestMsgHandler extends DefaultRestMsgHandler {
if (responseData.isPresent() && !StringUtils.isEmpty(responseData.get())) {
String data = responseData.get();
try {
+ ctx.logRpcRequest(rpcRequest.getSecurityCtx(), rpcRequest.getDeviceId(), rpcRequest.getBody(), rpcRequest.isOneway(), rpcError, null);
responseWriter.setResult(new ResponseEntity<>(jsonMapper.readTree(data), HttpStatus.OK));
} catch (IOException e) {
log.debug("Failed to decode device response: {}", data, e);
+ ctx.logRpcRequest(rpcRequest.getSecurityCtx(), rpcRequest.getDeviceId(), rpcRequest.getBody(), rpcRequest.isOneway(), rpcError, e);
responseWriter.setResult(new ResponseEntity<>(HttpStatus.NOT_ACCEPTABLE));
}
} else {
+ ctx.logRpcRequest(rpcRequest.getSecurityCtx(), rpcRequest.getDeviceId(), rpcRequest.getBody(), rpcRequest.isOneway(), rpcError, null);
responseWriter.setResult(new ResponseEntity<>(HttpStatus.OK));
}
}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/handlers/RpcRuleMsgHandler.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/handlers/RpcRuleMsgHandler.java
index f193c01..7db2acb 100644
--- a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/handlers/RpcRuleMsgHandler.java
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/handlers/RpcRuleMsgHandler.java
@@ -77,7 +77,7 @@ public class RpcRuleMsgHandler implements RuleMsgHandler {
@Override
public void onSuccess(PluginContext ctx, Void value) {
ctx.sendRpcRequest(new ToDeviceRpcRequest(UUID.randomUUID(),
- tenantId, tmpId, true, expirationTime, body)
+ null, tenantId, tmpId, true, expirationTime, body)
);
log.trace("[{}] Sent RPC Call Action msg", tmpId);
}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/RpcManager.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/RpcManager.java
index 25f7238..193d528 100644
--- a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/RpcManager.java
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/RpcManager.java
@@ -49,7 +49,7 @@ public class RpcManager {
LocalRequestMetaData md = localRpcRequests.remove(requestId);
if (md != null) {
log.trace("[{}] Processing local rpc response from device [{}]", requestId, md.getRequest().getDeviceId());
- restHandler.reply(ctx, md.getResponseWriter(), response);
+ restHandler.reply(ctx, md.getRequest(), md.getResponseWriter(), response);
} else {
log.trace("[{}] Unknown or stale rpc response received [{}]", requestId, response);
}
@@ -62,7 +62,7 @@ public class RpcManager {
LocalRequestMetaData md = localRpcRequests.remove(requestId);
if (md != null) {
log.trace("[{}] Processing rpc timeout for local device [{}]", requestId, md.getRequest().getDeviceId());
- restHandler.reply(ctx, md.getResponseWriter(), timeoutReponse);
+ restHandler.reply(ctx, md.getRequest(), md.getResponseWriter(), timeoutReponse);
}
}
}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRestMsgHandler.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRestMsgHandler.java
index 7e92fc1..1fa034b 100644
--- a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRestMsgHandler.java
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRestMsgHandler.java
@@ -28,6 +28,7 @@ import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityIdFactory;
+import org.thingsboard.server.common.data.id.UUIDBased;
import org.thingsboard.server.common.data.kv.*;
import org.thingsboard.server.common.msg.core.TelemetryUploadRequest;
import org.thingsboard.server.common.transport.adaptor.JsonConverter;
@@ -150,18 +151,19 @@ public class TelemetryRestMsgHandler extends DefaultRestMsgHandler {
private void handleHttpGetAttributesValues(PluginContext ctx, PluginRestMsg msg,
RestRequest request, String scope, EntityId entityId) throws ServletException {
String keys = request.getParameter("keys", "");
-
- PluginCallback<List<AttributeKvEntry>> callback = getAttributeValuesPluginCallback(msg);
+ List<String> keyList = null;
+ if (!StringUtils.isEmpty(keys)) {
+ keyList = Arrays.asList(keys.split(","));
+ }
+ PluginCallback<List<AttributeKvEntry>> callback = getAttributeValuesPluginCallback(msg, scope, entityId, keyList);
if (!StringUtils.isEmpty(scope)) {
- if (!StringUtils.isEmpty(keys)) {
- List<String> keyList = Arrays.asList(keys.split(","));
+ if (keyList != null && !keyList.isEmpty()) {
ctx.loadAttributes(entityId, scope, keyList, callback);
} else {
ctx.loadAttributes(entityId, scope, callback);
}
} else {
- if (!StringUtils.isEmpty(keys)) {
- List<String> keyList = Arrays.asList(keys.split(","));
+ if (keyList != null && !keyList.isEmpty()) {
ctx.loadAttributes(entityId, Arrays.asList(DataConstants.allScopes()), keyList, callback);
} else {
ctx.loadAttributes(entityId, Arrays.asList(DataConstants.allScopes()), callback);
@@ -230,9 +232,11 @@ public class TelemetryRestMsgHandler extends DefaultRestMsgHandler {
if (attributes.isEmpty()) {
throw new IllegalArgumentException("No attributes data found in request body!");
}
+
ctx.saveAttributes(ctx.getSecurityCtx().orElseThrow(IllegalArgumentException::new).getTenantId(), entityId, scope, attributes, new PluginCallback<Void>() {
@Override
public void onSuccess(PluginContext ctx, Void value) {
+ ctx.logAttributesUpdated(msg.getSecurityCtx(), entityId, scope, attributes, null);
msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.OK));
subscriptionManager.onAttributesUpdateFromServer(ctx, entityId, scope, attributes);
}
@@ -240,6 +244,7 @@ public class TelemetryRestMsgHandler extends DefaultRestMsgHandler {
@Override
public void onFailure(PluginContext ctx, Exception e) {
log.error("Failed to save attributes", e);
+ ctx.logAttributesUpdated(msg.getSecurityCtx(), entityId, scope, attributes, e);
handleError(e, msg, HttpStatus.BAD_REQUEST);
}
});
@@ -334,15 +339,18 @@ public class TelemetryRestMsgHandler extends DefaultRestMsgHandler {
String keysParam = request.getParameter("keys");
if (!StringUtils.isEmpty(keysParam)) {
String[] keys = keysParam.split(",");
- ctx.removeAttributes(ctx.getSecurityCtx().orElseThrow(IllegalArgumentException::new).getTenantId(), entityId, scope, Arrays.asList(keys), new PluginCallback<Void>() {
+ List<String> keyList = Arrays.asList(keys);
+ ctx.removeAttributes(ctx.getSecurityCtx().orElseThrow(IllegalArgumentException::new).getTenantId(), entityId, scope, keyList, new PluginCallback<Void>() {
@Override
public void onSuccess(PluginContext ctx, Void value) {
+ ctx.logAttributesDeleted(msg.getSecurityCtx(), entityId, scope, keyList, null);
msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.OK));
}
@Override
public void onFailure(PluginContext ctx, Exception e) {
log.error("Failed to remove attributes", e);
+ ctx.logAttributesDeleted(msg.getSecurityCtx(), entityId, scope, keyList, e);
handleError(e, msg, HttpStatus.INTERNAL_SERVER_ERROR);
}
});
@@ -373,18 +381,21 @@ public class TelemetryRestMsgHandler extends DefaultRestMsgHandler {
};
}
- private PluginCallback<List<AttributeKvEntry>> getAttributeValuesPluginCallback(final PluginRestMsg msg) {
+ private PluginCallback<List<AttributeKvEntry>> getAttributeValuesPluginCallback(final PluginRestMsg msg, final String scope,
+ final EntityId entityId, final List<String> keyList) {
return new PluginCallback<List<AttributeKvEntry>>() {
@Override
public void onSuccess(PluginContext ctx, List<AttributeKvEntry> attributes) {
List<AttributeData> values = attributes.stream().map(attribute -> new AttributeData(attribute.getLastUpdateTs(),
attribute.getKey(), attribute.getValue())).collect(Collectors.toList());
+ ctx.logAttributesRead(msg.getSecurityCtx(), entityId, scope, keyList, null);
msg.getResponseHolder().setResult(new ResponseEntity<>(values, HttpStatus.OK));
}
@Override
public void onFailure(PluginContext ctx, Exception e) {
log.error("Failed to fetch attributes", e);
+ ctx.logAttributesRead(msg.getSecurityCtx(), entityId, scope, keyList, e);
handleError(e, msg, HttpStatus.INTERNAL_SERVER_ERROR);
}
};
diff --git a/ui/src/app/alarm/alarm-table.tpl.html b/ui/src/app/alarm/alarm-table.tpl.html
index 98d42a2..361b9ba 100644
--- a/ui/src/app/alarm/alarm-table.tpl.html
+++ b/ui/src/app/alarm/alarm-table.tpl.html
@@ -29,7 +29,7 @@
</section>
<div flex layout="column" class="tb-alarm-container md-whiteframe-z1">
<md-list flex layout="column" class="tb-alarm-table">
- <md-list class="tb-row tb-header" layout="row" tb-alarm-header>
+ <md-list class="tb-row tb-header" layout="row" layout-align="start center" tb-alarm-header>
</md-list>
<md-progress-linear style="max-height: 0px;" md-mode="indeterminate" ng-disabled="!$root.loading"
ng-show="$root.loading"></md-progress-linear>
@@ -39,7 +39,7 @@
class="tb-prompt" ng-show="noData()">alarm.no-alarms-prompt</span>
<md-virtual-repeat-container ng-show="hasData()" flex md-top-index="topIndex" tb-scope-element="repeatContainer">
<md-list-item md-virtual-repeat="alarm in theAlarms" md-on-demand flex ng-style="hasScroll() ? {'margin-right':'-15px'} : {}">
- <md-list class="tb-row" flex layout="row" tb-alarm-row alarm="{{alarm}}">
+ <md-list class="tb-row" flex layout="row" layout-align="start center" tb-alarm-row alarm="{{alarm}}">
</md-list>
<md-divider flex></md-divider>
</md-list-item>
ui/src/app/api/audit-log.service.js 116(+116 -0)
diff --git a/ui/src/app/api/audit-log.service.js b/ui/src/app/api/audit-log.service.js
new file mode 100644
index 0000000..b8f73b9
--- /dev/null
+++ b/ui/src/app/api/audit-log.service.js
@@ -0,0 +1,116 @@
+/*
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export default angular.module('thingsboard.api.auditLog', [])
+ .factory('auditLogService', AuditLogService)
+ .name;
+
+/*@ngInject*/
+function AuditLogService($http, $q) {
+
+ var service = {
+ getAuditLogsByEntityId: getAuditLogsByEntityId,
+ getAuditLogsByUserId: getAuditLogsByUserId,
+ getAuditLogsByCustomerId: getAuditLogsByCustomerId,
+ getAuditLogs: getAuditLogs
+ }
+
+ return service;
+
+ function getAuditLogsByEntityId (entityType, entityId, pageLink) {
+ var deferred = $q.defer();
+ var url = `/api/audit/logs/entity/${entityType}/${entityId}?limit=${pageLink.limit}`;
+
+ if (angular.isDefined(pageLink.startTime) && pageLink.startTime != null) {
+ url += '&startTime=' + pageLink.startTime;
+ }
+ if (angular.isDefined(pageLink.endTime) && pageLink.endTime != null) {
+ url += '&endTime=' + pageLink.endTime;
+ }
+ if (angular.isDefined(pageLink.idOffset) && pageLink.idOffset != null) {
+ url += '&offset=' + pageLink.idOffset;
+ }
+ $http.get(url, null).then(function success(response) {
+ deferred.resolve(response.data);
+ }, function fail() {
+ deferred.reject();
+ });
+ return deferred.promise;
+ }
+
+ function getAuditLogsByUserId (userId, pageLink) {
+ var deferred = $q.defer();
+ var url = `/api/audit/logs/user/${userId}?limit=${pageLink.limit}`;
+
+ if (angular.isDefined(pageLink.startTime) && pageLink.startTime != null) {
+ url += '&startTime=' + pageLink.startTime;
+ }
+ if (angular.isDefined(pageLink.endTime) && pageLink.endTime != null) {
+ url += '&endTime=' + pageLink.endTime;
+ }
+ if (angular.isDefined(pageLink.idOffset) && pageLink.idOffset != null) {
+ url += '&offset=' + pageLink.idOffset;
+ }
+ $http.get(url, null).then(function success(response) {
+ deferred.resolve(response.data);
+ }, function fail() {
+ deferred.reject();
+ });
+ return deferred.promise;
+ }
+
+ function getAuditLogsByCustomerId (customerId, pageLink) {
+ var deferred = $q.defer();
+ var url = `/api/audit/logs/customer/${customerId}?limit=${pageLink.limit}`;
+
+ if (angular.isDefined(pageLink.startTime) && pageLink.startTime != null) {
+ url += '&startTime=' + pageLink.startTime;
+ }
+ if (angular.isDefined(pageLink.endTime) && pageLink.endTime != null) {
+ url += '&endTime=' + pageLink.endTime;
+ }
+ if (angular.isDefined(pageLink.idOffset) && pageLink.idOffset != null) {
+ url += '&offset=' + pageLink.idOffset;
+ }
+ $http.get(url, null).then(function success(response) {
+ deferred.resolve(response.data);
+ }, function fail() {
+ deferred.reject();
+ });
+ return deferred.promise;
+ }
+
+ function getAuditLogs (pageLink) {
+ var deferred = $q.defer();
+ var url = `/api/audit/logs?limit=${pageLink.limit}`;
+
+ if (angular.isDefined(pageLink.startTime) && pageLink.startTime != null) {
+ url += '&startTime=' + pageLink.startTime;
+ }
+ if (angular.isDefined(pageLink.endTime) && pageLink.endTime != null) {
+ url += '&endTime=' + pageLink.endTime;
+ }
+ if (angular.isDefined(pageLink.idOffset) && pageLink.idOffset != null) {
+ url += '&offset=' + pageLink.idOffset;
+ }
+ $http.get(url, null).then(function success(response) {
+ deferred.resolve(response.data);
+ }, function fail() {
+ deferred.reject();
+ });
+ return deferred.promise;
+ }
+
+}
ui/src/app/api/device.service.js 27(+20 -7)
diff --git a/ui/src/app/api/device.service.js b/ui/src/app/api/device.service.js
index bb4249e..f2ae8cd 100644
--- a/ui/src/app/api/device.service.js
+++ b/ui/src/app/api/device.service.js
@@ -20,7 +20,7 @@ export default angular.module('thingsboard.api.device', [thingsboardTypes])
.name;
/*@ngInject*/
-function DeviceService($http, $q, attributeService, customerService, types) {
+function DeviceService($http, $q, $window, userService, attributeService, customerService, types) {
var service = {
assignDeviceToCustomer: assignDeviceToCustomer,
@@ -181,14 +181,27 @@ function DeviceService($http, $q, attributeService, customerService, types) {
return deferred.promise;
}
- function getDeviceCredentials(deviceId) {
+ function getDeviceCredentials(deviceId, sync) {
var deferred = $q.defer();
var url = '/api/device/' + deviceId + '/credentials';
- $http.get(url, null).then(function success(response) {
- deferred.resolve(response.data);
- }, function fail() {
- deferred.reject();
- });
+ if (sync) {
+ var request = new $window.XMLHttpRequest();
+ request.open('GET', url, false);
+ request.setRequestHeader("Accept", "application/json, text/plain, */*");
+ userService.setAuthorizationRequestHeader(request);
+ request.send(null);
+ if (request.status === 200) {
+ deferred.resolve(angular.fromJson(request.responseText));
+ } else {
+ deferred.reject();
+ }
+ } else {
+ $http.get(url, null).then(function success(response) {
+ deferred.resolve(response.data);
+ }, function fail() {
+ deferred.reject();
+ });
+ }
return deferred.promise;
}
ui/src/app/api/user.service.js 9(+9 -0)
diff --git a/ui/src/app/api/user.service.js b/ui/src/app/api/user.service.js
index 1a146b4..ce5e673 100644
--- a/ui/src/app/api/user.service.js
+++ b/ui/src/app/api/user.service.js
@@ -54,6 +54,7 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi
refreshJwtToken: refreshJwtToken,
refreshTokenPending: refreshTokenPending,
updateAuthorizationHeader: updateAuthorizationHeader,
+ setAuthorizationRequestHeader: setAuthorizationRequestHeader,
gotoDefaultPlace: gotoDefaultPlace,
forceDefaultPlace: forceDefaultPlace,
updateLastPublicDashboardId: updateLastPublicDashboardId,
@@ -367,6 +368,14 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi
return jwtToken;
}
+ function setAuthorizationRequestHeader(request) {
+ var jwtToken = store.get('jwt_token');
+ if (jwtToken) {
+ request.setRequestHeader('X-Authorization', 'Bearer ' + jwtToken);
+ }
+ return jwtToken;
+ }
+
function getTenantAdmins(tenantId, pageLink) {
var deferred = $q.defer();
var url = '/api/tenant/' + tenantId + '/users?limit=' + pageLink.limit;
ui/src/app/app.js 4(+4 -0)
diff --git a/ui/src/app/app.js b/ui/src/app/app.js
index 5b19089..23f6933 100644
--- a/ui/src/app/app.js
+++ b/ui/src/app/app.js
@@ -63,6 +63,7 @@ import thingsboardApiTime from './api/time.service';
import thingsboardKeyboardShortcut from './components/keyboard-shortcut.filter';
import thingsboardHelp from './help/help.directive';
import thingsboardToast from './services/toast';
+import thingsboardClipboard from './services/clipboard.service';
import thingsboardHome from './layout';
import thingsboardApiLogin from './api/login.service';
import thingsboardApiDevice from './api/device.service';
@@ -72,6 +73,7 @@ import thingsboardApiAsset from './api/asset.service';
import thingsboardApiAttribute from './api/attribute.service';
import thingsboardApiEntity from './api/entity.service';
import thingsboardApiAlarm from './api/alarm.service';
+import thingsboardApiAuditLog from './api/audit-log.service';
import 'typeface-roboto';
import 'font-awesome/css/font-awesome.min.css';
@@ -123,6 +125,7 @@ angular.module('thingsboard', [
thingsboardKeyboardShortcut,
thingsboardHelp,
thingsboardToast,
+ thingsboardClipboard,
thingsboardHome,
thingsboardApiLogin,
thingsboardApiDevice,
@@ -132,6 +135,7 @@ angular.module('thingsboard', [
thingsboardApiAttribute,
thingsboardApiEntity,
thingsboardApiAlarm,
+ thingsboardApiAuditLog,
uiRouter])
.config(AppConfig)
.factory('globalInterceptor', GlobalInterceptor)
ui/src/app/asset/assets.tpl.html 6(+6 -0)
diff --git a/ui/src/app/asset/assets.tpl.html b/ui/src/app/asset/assets.tpl.html
index c4c0267..f16e1cd 100644
--- a/ui/src/app/asset/assets.tpl.html
+++ b/ui/src/app/asset/assets.tpl.html
@@ -66,4 +66,10 @@
entity-type="{{vm.types.entityType.asset}}">
</tb-relation-table>
</md-tab>
+ <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.grid.isTenantAdmin()" md-on-select="vm.grid.triggerResize()" label="{{ 'audit-log.audit-logs' | translate }}">
+ <tb-audit-log-table flex entity-type="vm.types.entityType.asset"
+ entity-id="vm.grid.operatingItem().id.id"
+ audit-log-mode="{{vm.types.auditLogMode.entity}}">
+ </tb-audit-log-table>
+ </md-tab>
</tb-grid>
ui/src/app/audit/audit-log.routes.js 44(+44 -0)
diff --git a/ui/src/app/audit/audit-log.routes.js b/ui/src/app/audit/audit-log.routes.js
new file mode 100644
index 0000000..be26b9c
--- /dev/null
+++ b/ui/src/app/audit/audit-log.routes.js
@@ -0,0 +1,44 @@
+/*
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/* eslint-disable import/no-unresolved, import/default */
+
+import auditLogsTemplate from './audit-logs.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function AuditLogRoutes($stateProvider) {
+ $stateProvider
+ .state('home.auditLogs', {
+ url: '/auditLogs',
+ module: 'private',
+ auth: ['TENANT_ADMIN'],
+ views: {
+ "content@home": {
+ templateUrl: auditLogsTemplate,
+ controller: 'AuditLogsController',
+ controllerAs: 'vm'
+ }
+ },
+ data: {
+ searchEnabled: false,
+ pageTitle: 'audit-log.audit-logs'
+ },
+ ncyBreadcrumb: {
+ label: '{"icon": "track_changes", "label": "audit-log.audit-logs"}'
+ }
+ });
+}
ui/src/app/audit/audit-log.scss 91(+91 -0)
diff --git a/ui/src/app/audit/audit-log.scss b/ui/src/app/audit/audit-log.scss
new file mode 100644
index 0000000..1642cbe
--- /dev/null
+++ b/ui/src/app/audit/audit-log.scss
@@ -0,0 +1,91 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+.tb-audit-logs {
+ background-color: #fff;
+ .tb-audit-log-margin-18px {
+ margin-bottom: 18px;
+ }
+ .tb-audit-log-toolbar {
+ font-size: 20px;
+ }
+ md-input-container.tb-audit-log-search-input {
+ .md-errors-spacer {
+ min-height: 0px;
+ }
+ }
+}
+
+.tb-audit-log-container {
+ overflow-x: auto;
+}
+
+
+
+md-list.tb-audit-log-table {
+ padding: 0px;
+ min-width: 700px;
+ &.tb-audit-log-table-full {
+ min-width: 900px;
+ }
+
+ md-list-item {
+ padding: 0px;
+ }
+
+ .tb-row {
+ height: 48px;
+ padding: 0px;
+ overflow: hidden;
+ }
+
+ .tb-row:hover {
+ background-color: #EEEEEE;
+ }
+
+ .tb-header:hover {
+ background: none;
+ }
+
+ .tb-header {
+ .tb-cell {
+ color: rgba(0,0,0,.54);
+ font-size: 12px;
+ font-weight: 700;
+ white-space: nowrap;
+ background: none;
+ }
+ }
+
+ .tb-cell {
+ padding: 0 24px;
+ margin: auto 0;
+ color: rgba(0,0,0,.87);
+ font-size: 13px;
+ vertical-align: middle;
+ text-align: left;
+ overflow: hidden;
+ .md-button {
+ padding: 0;
+ margin: 0;
+ }
+ }
+
+ .tb-cell.tb-number {
+ text-align: right;
+ }
+
+}
diff --git a/ui/src/app/audit/audit-log-details-dialog.controller.js b/ui/src/app/audit/audit-log-details-dialog.controller.js
new file mode 100644
index 0000000..88cb10d
--- /dev/null
+++ b/ui/src/app/audit/audit-log-details-dialog.controller.js
@@ -0,0 +1,103 @@
+/*
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import $ from 'jquery';
+import 'brace/ext/language_tools';
+import 'brace/mode/java';
+import 'brace/theme/github';
+
+/* eslint-disable angular/angularelement */
+
+import './audit-log-details-dialog.scss';
+
+/*@ngInject*/
+export default function AuditLogDetailsDialogController($mdDialog, types, auditLog, showingCallback) {
+
+ var vm = this;
+
+ showingCallback.onShowing = function(scope, element) {
+ updateEditorSize(element, vm.actionData, 'tb-audit-log-action-data');
+ vm.actionDataEditor.resize();
+ if (vm.displayFailureDetails) {
+ updateEditorSize(element, vm.actionFailureDetails, 'tb-audit-log-failure-details');
+ vm.failureDetailsEditor.resize();
+ }
+ };
+
+ vm.types = types;
+ vm.auditLog = auditLog;
+ vm.displayFailureDetails = auditLog.actionStatus == types.auditLogActionStatus.FAILURE.value;
+ vm.actionData = auditLog.actionDataText;
+ vm.actionFailureDetails = auditLog.actionFailureDetails;
+
+ vm.actionDataContentOptions = {
+ useWrapMode: false,
+ mode: 'java',
+ showGutter: false,
+ showPrintMargin: false,
+ theme: 'github',
+ advanced: {
+ enableSnippets: false,
+ enableBasicAutocompletion: false,
+ enableLiveAutocompletion: false
+ },
+ onLoad: function (_ace) {
+ vm.actionDataEditor = _ace;
+ }
+ };
+
+ vm.failureDetailsContentOptions = {
+ useWrapMode: false,
+ mode: 'java',
+ showGutter: false,
+ showPrintMargin: false,
+ theme: 'github',
+ advanced: {
+ enableSnippets: false,
+ enableBasicAutocompletion: false,
+ enableLiveAutocompletion: false
+ },
+ onLoad: function (_ace) {
+ vm.failureDetailsEditor = _ace;
+ }
+ };
+
+ function updateEditorSize(element, content, editorId) {
+ var newHeight = 200;
+ var newWidth = 600;
+ if (content && content.length > 0) {
+ var lines = content.split('\n');
+ newHeight = 16 * lines.length + 16;
+ var maxLineLength = 0;
+ for (var i in lines) {
+ var line = lines[i].replace(/\t/g, ' ').replace(/\n/g, '');
+ var lineLength = line.length;
+ maxLineLength = Math.max(maxLineLength, lineLength);
+ }
+ newWidth = 8 * maxLineLength + 16;
+ }
+ $('#'+editorId, element).height(newHeight.toString() + "px").css('min-height', newHeight.toString() + "px")
+ .width(newWidth.toString() + "px");
+ }
+
+ vm.close = close;
+
+ function close () {
+ $mdDialog.hide();
+ }
+
+}
+
+/* eslint-enable angular/angularelement */
diff --git a/ui/src/app/audit/audit-log-details-dialog.scss b/ui/src/app/audit/audit-log-details-dialog.scss
new file mode 100644
index 0000000..83aaf40
--- /dev/null
+++ b/ui/src/app/audit/audit-log-details-dialog.scss
@@ -0,0 +1,23 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#tb-audit-log-action-data, #tb-audit-log-failure-details {
+ min-width: 400px;
+ min-height: 50px;
+ width: 100%;
+ height: 100%;
+ border: 1px solid #C0C0C0;
+}
\ No newline at end of file
diff --git a/ui/src/app/audit/audit-log-details-dialog.tpl.html b/ui/src/app/audit/audit-log-details-dialog.tpl.html
new file mode 100644
index 0000000..b1e1e63
--- /dev/null
+++ b/ui/src/app/audit/audit-log-details-dialog.tpl.html
@@ -0,0 +1,49 @@
+<!--
+
+ Copyright © 2016-2017 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<md-dialog aria-label="{{ 'audit-log.audit-log-details' | translate }}">
+ <md-toolbar>
+ <div class="md-toolbar-tools">
+ <h2 translate>audit-log.audit-log-details</h2>
+ <span flex></span>
+ <md-button class="md-icon-button" ng-click="vm.close()">
+ <ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
+ </md-button>
+ </div>
+ </md-toolbar>
+ <md-dialog-content>
+ <div class="md-dialog-content" layout="column">
+ <label translate class="tb-title no-padding">audit-log.action-data</label>
+ <div flex id="tb-audit-log-action-data" readonly
+ ui-ace="vm.actionDataContentOptions"
+ ng-model="vm.actionData">
+ </div>
+ <span style="height: 30px;"></span>
+ <label ng-show="vm.displayFailureDetails" translate class="tb-title no-padding">audit-log.failure-details</label>
+ <div ng-show="vm.displayFailureDetails" flex id="tb-audit-log-failure-details" readonly
+ ui-ace="vm.failureDetailsContentOptions"
+ ng-model="vm.actionFailureDetails">
+ </div>
+ </div>
+ </md-dialog-content>
+ <md-dialog-actions layout="row">
+ <span flex></span>
+ <md-button ng-disabled="$root.loading" ng-click="vm.close()" style="margin-right:20px;">{{ 'action.close' |
+ translate }}
+ </md-button>
+ </md-dialog-actions>
+</md-dialog>
diff --git a/ui/src/app/audit/audit-log-header.directive.js b/ui/src/app/audit/audit-log-header.directive.js
new file mode 100644
index 0000000..504e96c
--- /dev/null
+++ b/ui/src/app/audit/audit-log-header.directive.js
@@ -0,0 +1,41 @@
+/*
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/* eslint-disable import/no-unresolved, import/default */
+
+import auditLogHeaderTemplate from './audit-log-header.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function AuditLogHeaderDirective($compile, $templateCache, types) {
+
+ var linker = function (scope, element, attrs) {
+
+ var template = $templateCache.get(auditLogHeaderTemplate);
+ element.html(template);
+ scope.auditLogMode = attrs.auditLogMode;
+ scope.types = types;
+ $compile(element.contents())(scope);
+
+ };
+
+ return {
+ restrict: "A",
+ replace: false,
+ link: linker,
+ scope: false
+ };
+}
ui/src/app/audit/audit-log-header.tpl.html 24(+24 -0)
diff --git a/ui/src/app/audit/audit-log-header.tpl.html b/ui/src/app/audit/audit-log-header.tpl.html
new file mode 100644
index 0000000..6927ad7
--- /dev/null
+++ b/ui/src/app/audit/audit-log-header.tpl.html
@@ -0,0 +1,24 @@
+<!--
+
+ Copyright © 2016-2017 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<div translate class="tb-cell" flex="30">audit-log.timestamp</div>
+<div ng-if="auditLogMode != types.auditLogMode.entity" translate class="tb-cell" flex="10">audit-log.entity-type</div>
+<div ng-if="auditLogMode != types.auditLogMode.entity" translate class="tb-cell" flex="30">audit-log.entity-name</div>
+<div ng-if="auditLogMode != types.auditLogMode.user" translate class="tb-cell" flex="30">audit-log.user</div>
+<div translate class="tb-cell" flex="15">audit-log.type</div>
+<div translate class="tb-cell" flex="15">audit-log.status</div>
+<div translate class="tb-cell" flex="10">audit-log.details</div>
ui/src/app/audit/audit-log-row.directive.js 67(+67 -0)
diff --git a/ui/src/app/audit/audit-log-row.directive.js b/ui/src/app/audit/audit-log-row.directive.js
new file mode 100644
index 0000000..1cb6d5f
--- /dev/null
+++ b/ui/src/app/audit/audit-log-row.directive.js
@@ -0,0 +1,67 @@
+/*
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/* eslint-disable import/no-unresolved, import/default */
+
+import auditLogDetailsDialogTemplate from './audit-log-details-dialog.tpl.html';
+
+import auditLogRowTemplate from './audit-log-row.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function AuditLogRowDirective($compile, $templateCache, types, $mdDialog, $document) {
+
+ var linker = function (scope, element, attrs) {
+
+ var template = $templateCache.get(auditLogRowTemplate);
+ element.html(template);
+
+ scope.auditLog = attrs.auditLog;
+ scope.auditLogMode = attrs.auditLogMode;
+ scope.types = types;
+
+ scope.showAuditLogDetails = function($event) {
+ var onShowingCallback = {
+ onShowing: function(){}
+ }
+ $mdDialog.show({
+ controller: 'AuditLogDetailsDialogController',
+ controllerAs: 'vm',
+ templateUrl: auditLogDetailsDialogTemplate,
+ locals: {
+ auditLog: scope.auditLog,
+ showingCallback: onShowingCallback
+ },
+ parent: angular.element($document[0].body),
+ targetEvent: $event,
+ fullscreen: true,
+ skipHide: true,
+ onShowing: function(scope, element) {
+ onShowingCallback.onShowing(scope, element);
+ }
+ });
+ }
+
+ $compile(element.contents())(scope);
+ }
+
+ return {
+ restrict: "A",
+ replace: false,
+ link: linker,
+ scope: false
+ };
+}
ui/src/app/audit/audit-log-row.tpl.html 36(+36 -0)
diff --git a/ui/src/app/audit/audit-log-row.tpl.html b/ui/src/app/audit/audit-log-row.tpl.html
new file mode 100644
index 0000000..fd0af2d
--- /dev/null
+++ b/ui/src/app/audit/audit-log-row.tpl.html
@@ -0,0 +1,36 @@
+<!--
+
+ Copyright © 2016-2017 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<div class="tb-cell" flex="30">{{ auditLog.createdTime | date : 'yyyy-MM-dd HH:mm:ss' }}</div>
+<div ng-if="auditLogMode != types.auditLogMode.entity" class="tb-cell" flex="10">{{ auditLog.entityTypeText }}</div>
+<div ng-if="auditLogMode != types.auditLogMode.entity" class="tb-cell" flex="30">{{ auditLog.entityName }}</div>
+<div ng-if="auditLogMode != types.auditLogMode.user" class="tb-cell" flex="30">{{ auditLog.userName }}</div>
+<div class="tb-cell" flex="15">{{ auditLog.actionTypeText }}</div>
+<div class="tb-cell" flex="15">{{ auditLog.actionStatusText }}</div>
+<div class="tb-cell" flex="10">
+ <md-button class="md-icon-button md-primary"
+ ng-click="showAuditLogDetails($event)"
+ aria-label="{{ 'action.view' | translate }}">
+ <md-tooltip md-direction="top">
+ {{ 'audit-log.details' | translate }}
+ </md-tooltip>
+ <md-icon aria-label="{{ 'action.view' | translate }}"
+ class="material-icons">
+ more_horiz
+ </md-icon>
+ </md-button>
+</div>
ui/src/app/audit/audit-logs.controller.js 24(+24 -0)
diff --git a/ui/src/app/audit/audit-logs.controller.js b/ui/src/app/audit/audit-logs.controller.js
new file mode 100644
index 0000000..d2cb1e3
--- /dev/null
+++ b/ui/src/app/audit/audit-logs.controller.js
@@ -0,0 +1,24 @@
+/*
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*@ngInject*/
+export default function AuditLogsController(types) {
+
+ var vm = this;
+
+ vm.types = types;
+
+}
\ No newline at end of file
ui/src/app/audit/audit-logs.tpl.html 23(+23 -0)
diff --git a/ui/src/app/audit/audit-logs.tpl.html b/ui/src/app/audit/audit-logs.tpl.html
new file mode 100644
index 0000000..088c339
--- /dev/null
+++ b/ui/src/app/audit/audit-logs.tpl.html
@@ -0,0 +1,23 @@
+<!--
+
+ Copyright © 2016-2017 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+
+<tb-audit-log-table class="md-whiteframe-z1"
+ flex
+ audit-log-mode="{{vm.types.auditLogMode.tenant}}"
+ page-mode="true">
+</tb-audit-log-table>
ui/src/app/audit/audit-log-table.directive.js 262(+262 -0)
diff --git a/ui/src/app/audit/audit-log-table.directive.js b/ui/src/app/audit/audit-log-table.directive.js
new file mode 100644
index 0000000..ba15249
--- /dev/null
+++ b/ui/src/app/audit/audit-log-table.directive.js
@@ -0,0 +1,262 @@
+/*
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import './audit-log.scss';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import auditLogTableTemplate from './audit-log-table.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function AuditLogTableDirective($compile, $templateCache, $rootScope, $filter, $translate, types, auditLogService) {
+
+ var linker = function (scope, element) {
+
+ var template = $templateCache.get(auditLogTableTemplate);
+
+ element.html(template);
+
+ scope.types = types;
+
+ var pageSize = 20;
+ var startTime = 0;
+ var endTime = 0;
+
+ scope.timewindow = {
+ history: {
+ timewindowMs: 24 * 60 * 60 * 1000 // 1 day
+ }
+ }
+
+ scope.topIndex = 0;
+ scope.searchText = '';
+
+ scope.theAuditLogs = {
+ getItemAtIndex: function (index) {
+ if (index > scope.auditLogs.filtered.length) {
+ scope.theAuditLogs.fetchMoreItems_(index);
+ return null;
+ }
+ return scope.auditLogs.filtered[index];
+ },
+
+ getLength: function () {
+ if (scope.auditLogs.hasNext) {
+ return scope.auditLogs.filtered.length + scope.auditLogs.nextPageLink.limit;
+ } else {
+ return scope.auditLogs.filtered.length;
+ }
+ },
+
+ fetchMoreItems_: function () {
+ if (scope.auditLogs.hasNext && !scope.auditLogs.pending) {
+ var promise = getAuditLogsPromise(scope.auditLogs.nextPageLink);
+ if (promise) {
+ scope.auditLogs.pending = true;
+ promise.then(
+ function success(auditLogs) {
+ scope.auditLogs.data = scope.auditLogs.data.concat(prepareAuditLogsData(auditLogs.data));
+ scope.auditLogs.filtered = $filter('filter')(scope.auditLogs.data, {$: scope.searchText});
+ scope.auditLogs.nextPageLink = auditLogs.nextPageLink;
+ scope.auditLogs.hasNext = auditLogs.hasNext;
+ if (scope.auditLogs.hasNext) {
+ scope.auditLogs.nextPageLink.limit = pageSize;
+ }
+ scope.auditLogs.pending = false;
+ },
+ function fail() {
+ scope.auditLogs.hasNext = false;
+ scope.auditLogs.pending = false;
+ });
+ } else {
+ scope.auditLogs.hasNext = false;
+ }
+ }
+ }
+ };
+
+ function prepareAuditLogsData(data) {
+ data.forEach(
+ auditLog => {
+ auditLog.entityTypeText = $translate.instant(types.entityTypeTranslations[auditLog.entityId.entityType].type);
+ auditLog.actionTypeText = $translate.instant(types.auditLogActionType[auditLog.actionType].name);
+ auditLog.actionStatusText = $translate.instant(types.auditLogActionStatus[auditLog.actionStatus].name);
+ auditLog.actionDataText = auditLog.actionData ? angular.toJson(auditLog.actionData, true) : '';
+ }
+ );
+ return data;
+ }
+
+ scope.$watch("entityId", function(newVal, prevVal) {
+ if (newVal && !angular.equals(newVal, prevVal)) {
+ resetFilter();
+ scope.reload();
+ }
+ });
+
+ scope.$watch("userId", function(newVal, prevVal) {
+ if (newVal && !angular.equals(newVal, prevVal)) {
+ resetFilter();
+ scope.reload();
+ }
+ });
+
+ scope.$watch("customerId", function(newVal, prevVal) {
+ if (newVal && !angular.equals(newVal, prevVal)) {
+ resetFilter();
+ scope.reload();
+ }
+ });
+
+ function getAuditLogsPromise(pageLink) {
+ switch(scope.auditLogMode) {
+ case types.auditLogMode.tenant:
+ return auditLogService.getAuditLogs(pageLink);
+ case types.auditLogMode.entity:
+ if (scope.entityType && scope.entityId) {
+ return auditLogService.getAuditLogsByEntityId(scope.entityType, scope.entityId,
+ pageLink);
+ } else {
+ return null;
+ }
+ case types.auditLogMode.user:
+ if (scope.userId) {
+ return auditLogService.getAuditLogsByUserId(scope.userId, pageLink);
+ } else {
+ return null;
+ }
+ case types.auditLogMode.customer:
+ if (scope.customerId) {
+ return auditLogService.getAuditLogsByCustomerId(scope.customerId, pageLink);
+ } else {
+ return null;
+ }
+ }
+ }
+
+ function destroyWatchers() {
+ if (scope.timewindowWatchHandle) {
+ scope.timewindowWatchHandle();
+ scope.timewindowWatchHandle = null;
+ }
+ if (scope.searchTextWatchHandle) {
+ scope.searchTextWatchHandle();
+ scope.searchTextWatchHandle = null;
+ }
+ }
+
+ function initWatchers() {
+ scope.timewindowWatchHandle = scope.$watch("timewindow", function(newVal, prevVal) {
+ if (newVal && !angular.equals(newVal, prevVal)) {
+ scope.reload();
+ }
+ }, true);
+
+ scope.searchTextWatchHandle = scope.$watch("searchText", function(newVal, prevVal) {
+ if (!angular.equals(newVal, prevVal)) {
+ scope.searchTextUpdated();
+ }
+ }, true);
+ }
+
+ function resetFilter() {
+ destroyWatchers();
+ scope.timewindow = {
+ history: {
+ timewindowMs: 24 * 60 * 60 * 1000 // 1 day
+ }
+ };
+ scope.searchText = '';
+ initWatchers();
+ }
+
+ function updateTimeWindowRange () {
+ if (scope.timewindow.history.timewindowMs) {
+ var currentTime = (new Date).getTime();
+ startTime = currentTime - scope.timewindow.history.timewindowMs;
+ endTime = currentTime;
+ } else {
+ startTime = scope.timewindow.history.fixedTimewindow.startTimeMs;
+ endTime = scope.timewindow.history.fixedTimewindow.endTimeMs;
+ }
+ }
+
+ scope.reload = function() {
+ scope.topIndex = 0;
+ updateTimeWindowRange();
+ scope.auditLogs = {
+ data: [],
+ filtered: [],
+ nextPageLink: {
+ limit: pageSize,
+ startTime: startTime,
+ endTime: endTime
+ },
+ hasNext: true,
+ pending: false
+ };
+ scope.theAuditLogs.getItemAtIndex(pageSize);
+ }
+
+ scope.searchTextUpdated = function() {
+ scope.auditLogs.filtered = $filter('filter')(scope.auditLogs.data, {$: scope.searchText});
+ scope.theAuditLogs.getItemAtIndex(pageSize);
+ }
+
+ scope.noData = function() {
+ return scope.auditLogs.data.length == 0 && !scope.auditLogs.hasNext;
+ }
+
+ scope.hasData = function() {
+ return scope.auditLogs.data.length > 0;
+ }
+
+ scope.loading = function() {
+ return $rootScope.loading;
+ }
+
+ scope.hasScroll = function() {
+ var repeatContainer = scope.repeatContainer[0];
+ if (repeatContainer) {
+ var scrollElement = repeatContainer.children[0];
+ if (scrollElement) {
+ return scrollElement.scrollHeight > scrollElement.clientHeight;
+ }
+ }
+ return false;
+ }
+
+ scope.reload();
+
+ initWatchers();
+
+ $compile(element.contents())(scope);
+ }
+
+ return {
+ restrict: "E",
+ link: linker,
+ scope: {
+ entityType: '=?',
+ entityId: '=?',
+ userId: '=?',
+ customerId: '=?',
+ auditLogMode: '@',
+ pageMode: '@?'
+ }
+ };
+}
ui/src/app/audit/audit-log-table.tpl.html 68(+68 -0)
diff --git a/ui/src/app/audit/audit-log-table.tpl.html b/ui/src/app/audit/audit-log-table.tpl.html
new file mode 100644
index 0000000..88a1d93
--- /dev/null
+++ b/ui/src/app/audit/audit-log-table.tpl.html
@@ -0,0 +1,68 @@
+<!--
+
+ Copyright © 2016-2017 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<md-content flex class="md-padding tb-absolute-fill" layout="column">
+ <div flex layout="column" class="tb-audit-logs" ng-class="{'md-whiteframe-z1': pageMode}">
+ <div layout="column" layout-gt-sm="row" layout-align-gt-sm="start center" class="tb-audit-log-toolbar" ng-class="{'md-padding': pageMode, 'tb-audit-log-margin-18px': !pageMode}">
+ <tb-timewindow ng-model="timewindow" history-only as-button="true"></tb-timewindow>
+ <div flex layout="row" layout-align="start center">
+ <md-button class="md-icon-button" aria-label="{{ 'action.search' | translate }}">
+ <md-icon aria-label="{{ 'action.search' | translate }}" class="material-icons">search</md-icon>
+ <md-tooltip md-direction="top">
+ {{'audit-log.search' | translate}}
+ </md-tooltip>
+ </md-button>
+ <md-input-container flex class="tb-audit-log-search-input">
+ <label> </label>
+ <input ng-model="searchText" placeholder="{{'audit-log.search' | translate}}"/>
+ </md-input-container>
+ <md-button ng-disabled="$root.loading" class="md-icon-button" aria-label="Close" ng-click="searchText = ''">
+ <md-icon aria-label="Close" class="material-icons">close</md-icon>
+ <md-tooltip md-direction="top">
+ {{ 'audit-log.clear-search' | translate }}
+ </md-tooltip>
+ </md-button>
+ <md-button ng-disabled="$root.loading"
+ class="md-icon-button" ng-click="reload()">
+ <md-icon>refresh</md-icon>
+ <md-tooltip md-direction="top">
+ {{ 'action.refresh' | translate }}
+ </md-tooltip>
+ </md-button>
+ </div>
+ </div>
+ <div flex layout="column" class="tb-audit-log-container" ng-class="{'md-whiteframe-z1': !pageMode}">
+ <md-list flex layout="column" class="tb-audit-log-table" ng-class="{'tb-audit-log-table-full': pageMode}">
+ <md-list class="tb-row tb-header" layout="row" layout-align="start center" tb-audit-log-header audit-log-mode="{{auditLogMode}}">
+ </md-list>
+ <md-progress-linear style="max-height: 0px;" md-mode="indeterminate" ng-disabled="!$root.loading"
+ ng-show="$root.loading"></md-progress-linear>
+ <md-divider></md-divider>
+ <span translate layout-align="center center"
+ style="margin-top: 25px;"
+ class="tb-prompt" ng-show="noData()">audit-log.no-audit-logs-prompt</span>
+ <md-virtual-repeat-container ng-show="hasData()" flex md-top-index="topIndex" tb-scope-element="repeatContainer">
+ <md-list-item md-virtual-repeat="auditLog in theAuditLogs" md-on-demand flex ng-style="hasScroll() ? {'margin-right':'-15px'} : {}">
+ <md-list class="tb-row" flex layout="row" layout-align="start center" tb-audit-log-row audit-log-mode="{{auditLogMode}}" audit-log="{{auditLog}}">
+ </md-list>
+ <md-divider flex></md-divider>
+ </md-list-item>
+ </md-virtual-repeat-container>
+ </md-list>
+ </div>
+ </div>
+</md-content>
ui/src/app/audit/index.js 31(+31 -0)
diff --git a/ui/src/app/audit/index.js b/ui/src/app/audit/index.js
new file mode 100644
index 0000000..aebcd33
--- /dev/null
+++ b/ui/src/app/audit/index.js
@@ -0,0 +1,31 @@
+/*
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import AuditLogRoutes from './audit-log.routes';
+import AuditLogsController from './audit-logs.controller';
+import AuditLogDetailsDialogController from './audit-log-details-dialog.controller';
+import AuditLogHeaderDirective from './audit-log-header.directive';
+import AuditLogRowDirective from './audit-log-row.directive';
+import AuditLogTableDirective from './audit-log-table.directive';
+
+export default angular.module('thingsboard.auditLog', [])
+ .config(AuditLogRoutes)
+ .controller('AuditLogsController', AuditLogsController)
+ .controller('AuditLogDetailsDialogController', AuditLogDetailsDialogController)
+ .directive('tbAuditLogHeader', AuditLogHeaderDirective)
+ .directive('tbAuditLogRow', AuditLogRowDirective)
+ .directive('tbAuditLogTable', AuditLogTableDirective)
+ .name;
ui/src/app/common/types.constant.js 57(+57 -0)
diff --git a/ui/src/app/common/types.constant.js b/ui/src/app/common/types.constant.js
index 5b3c9e4..d82add1 100644
--- a/ui/src/app/common/types.constant.js
+++ b/ui/src/app/common/types.constant.js
@@ -156,6 +156,63 @@ export default angular.module('thingsboard.types', [])
color: "green"
}
},
+ auditLogActionType: {
+ "ADDED": {
+ name: "audit-log.type-added"
+ },
+ "DELETED": {
+ name: "audit-log.type-deleted"
+ },
+ "UPDATED": {
+ name: "audit-log.type-updated"
+ },
+ "ATTRIBUTES_UPDATED": {
+ name: "audit-log.type-attributes-updated"
+ },
+ "ATTRIBUTES_DELETED": {
+ name: "audit-log.type-attributes-deleted"
+ },
+ "RPC_CALL": {
+ name: "audit-log.type-rpc-call"
+ },
+ "CREDENTIALS_UPDATED": {
+ name: "audit-log.type-credentials-updated"
+ },
+ "ASSIGNED_TO_CUSTOMER": {
+ name: "audit-log.type-assigned-to-customer"
+ },
+ "UNASSIGNED_FROM_CUSTOMER": {
+ name: "audit-log.type-unassigned-from-customer"
+ },
+ "ACTIVATED": {
+ name: "audit-log.type-activated"
+ },
+ "SUSPENDED": {
+ name: "audit-log.type-suspended"
+ },
+ "CREDENTIALS_READ": {
+ name: "audit-log.type-credentials-read"
+ },
+ "ATTRIBUTES_READ": {
+ name: "audit-log.type-attributes-read"
+ }
+ },
+ auditLogActionStatus: {
+ "SUCCESS": {
+ value: "SUCCESS",
+ name: "audit-log.status-success"
+ },
+ "FAILURE": {
+ value: "FAILURE",
+ name: "audit-log.status-failure"
+ }
+ },
+ auditLogMode: {
+ tenant: "tenant",
+ entity: "entity",
+ user: "user",
+ customer: "customer"
+ },
aliasFilterType: {
singleEntity: {
value: 'singleEntity',
diff --git a/ui/src/app/components/grid.directive.js b/ui/src/app/components/grid.directive.js
index 727b999..290431c 100644
--- a/ui/src/app/components/grid.directive.js
+++ b/ui/src/app/components/grid.directive.js
@@ -125,7 +125,7 @@ function Grid() {
}
/*@ngInject*/
-function GridController($scope, $state, $mdDialog, $document, $q, $mdUtil, $timeout, $translate, $mdMedia, $templateCache, $window) {
+function GridController($scope, $state, $mdDialog, $document, $q, $mdUtil, $timeout, $translate, $mdMedia, $templateCache, $window, userService) {
var vm = this;
@@ -157,6 +157,7 @@ function GridController($scope, $state, $mdDialog, $document, $q, $mdUtil, $time
vm.saveItem = saveItem;
vm.toggleItemSelection = toggleItemSelection;
vm.triggerResize = triggerResize;
+ vm.isTenantAdmin = isTenantAdmin;
$scope.$watch(function () {
return $mdMedia('xs') || $mdMedia('sm');
@@ -634,6 +635,10 @@ function GridController($scope, $state, $mdDialog, $document, $q, $mdUtil, $time
w.triggerHandler('resize');
}
+ function isTenantAdmin() {
+ return userService.getAuthority() == 'TENANT_ADMIN';
+ }
+
function moveToTop() {
moveToIndex(0, true);
}
diff --git a/ui/src/app/customer/customers.tpl.html b/ui/src/app/customer/customers.tpl.html
index da0a2e9..0a0ca84 100644
--- a/ui/src/app/customer/customers.tpl.html
+++ b/ui/src/app/customer/customers.tpl.html
@@ -66,5 +66,10 @@
entity-type="{{vm.types.entityType.customer}}">
</tb-relation-table>
</md-tab>
+ <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.grid.isTenantAdmin()" md-on-select="vm.grid.triggerResize()" label="{{ 'audit-log.audit-logs' | translate }}">
+ <tb-audit-log-table flex customer-id="vm.grid.operatingItem().id.id"
+ audit-log-mode="{{vm.types.auditLogMode.customer}}">
+ </tb-audit-log-table>
+ </md-tab>
</md-tabs>
</tb-grid>
ui/src/app/dashboard/dashboards.tpl.html 29(+20 -9)
diff --git a/ui/src/app/dashboard/dashboards.tpl.html b/ui/src/app/dashboard/dashboards.tpl.html
index dde2f86..8149821 100644
--- a/ui/src/app/dashboard/dashboards.tpl.html
+++ b/ui/src/app/dashboard/dashboards.tpl.html
@@ -19,13 +19,24 @@
<details-buttons tb-help="'dashboards'" help-container-id="help-container">
<div id="help-container"></div>
</details-buttons>
- <tb-dashboard-details dashboard="vm.grid.operatingItem()"
- is-edit="vm.grid.detailsConfig.isDetailsEditMode"
- dashboard-scope="vm.dashboardsScope"
- the-form="vm.grid.detailsForm"
- on-assign-to-customer="vm.assignToCustomer(event, [ vm.grid.detailsConfig.currentItem.id.id ])"
- on-make-public="vm.makePublic(event, vm.grid.detailsConfig.currentItem)"
- on-unassign-from-customer="vm.unassignFromCustomer(event, vm.grid.detailsConfig.currentItem, isPublic)"
- on-export-dashboard="vm.exportDashboard(event, vm.grid.detailsConfig.currentItem)"
- on-delete-dashboard="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"></tb-dashboard-details>
+ <md-tabs ng-class="{'tb-headless': vm.grid.detailsConfig.isDetailsEditMode}"
+ id="tabs" md-border-bottom flex class="tb-absolute-fill">
+ <md-tab label="{{ 'dashboard.details' | translate }}">
+ <tb-dashboard-details dashboard="vm.grid.operatingItem()"
+ is-edit="vm.grid.detailsConfig.isDetailsEditMode"
+ dashboard-scope="vm.dashboardsScope"
+ the-form="vm.grid.detailsForm"
+ on-assign-to-customer="vm.assignToCustomer(event, [ vm.grid.detailsConfig.currentItem.id.id ])"
+ on-make-public="vm.makePublic(event, vm.grid.detailsConfig.currentItem)"
+ on-unassign-from-customer="vm.unassignFromCustomer(event, vm.grid.detailsConfig.currentItem, isPublic)"
+ on-export-dashboard="vm.exportDashboard(event, vm.grid.detailsConfig.currentItem)"
+ on-delete-dashboard="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"></tb-dashboard-details>
+ </md-tab>
+ <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.grid.isTenantAdmin()" md-on-select="vm.grid.triggerResize()" label="{{ 'audit-log.audit-logs' | translate }}">
+ <tb-audit-log-table flex entity-type="vm.types.entityType.dashboard"
+ entity-id="vm.grid.operatingItem().id.id"
+ audit-log-mode="{{vm.types.auditLogMode.entity}}">
+ </tb-audit-log-table>
+ </md-tab>
+ </md-tabs>
</tb-grid>
ui/src/app/device/device.directive.js 27(+15 -12)
diff --git a/ui/src/app/device/device.directive.js b/ui/src/app/device/device.directive.js
index eda4fb2..a40e4da 100644
--- a/ui/src/app/device/device.directive.js
+++ b/ui/src/app/device/device.directive.js
@@ -20,7 +20,7 @@ import deviceFieldsetTemplate from './device-fieldset.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
/*@ngInject*/
-export default function DeviceDirective($compile, $templateCache, toast, $translate, types, deviceService, customerService) {
+export default function DeviceDirective($compile, $templateCache, toast, $translate, types, clipboardService, deviceService, customerService) {
var linker = function (scope, element) {
var template = $templateCache.get(deviceFieldsetTemplate);
element.html(template);
@@ -30,17 +30,8 @@ export default function DeviceDirective($compile, $templateCache, toast, $transl
scope.isPublic = false;
scope.assignedCustomer = null;
- scope.deviceCredentials = null;
-
scope.$watch('device', function(newVal) {
if (newVal) {
- if (scope.device.id) {
- deviceService.getDeviceCredentials(scope.device.id.id).then(
- function success(credentials) {
- scope.deviceCredentials = credentials;
- }
- );
- }
if (scope.device.customerId && scope.device.customerId.id !== types.id.nullUid) {
scope.isAssignedToCustomer = true;
customerService.getShortCustomerInfo(scope.device.customerId.id).then(
@@ -61,8 +52,20 @@ export default function DeviceDirective($compile, $templateCache, toast, $transl
toast.showSuccess($translate.instant('device.idCopiedMessage'), 750, angular.element(element).parent().parent(), 'bottom left');
};
- scope.onAccessTokenCopied = function() {
- toast.showSuccess($translate.instant('device.accessTokenCopiedMessage'), 750, angular.element(element).parent().parent(), 'bottom left');
+ scope.copyAccessToken = function(e) {
+ const trigger = e.delegateTarget || e.currentTarget;
+ if (scope.device.id) {
+ deviceService.getDeviceCredentials(scope.device.id.id, true).then(
+ function success(credentials) {
+ var credentialsId = credentials.credentialsId;
+ clipboardService.copyToClipboard(trigger, credentialsId).then(
+ () => {
+ toast.showSuccess($translate.instant('device.accessTokenCopiedMessage'), 750, angular.element(element).parent().parent(), 'bottom left');
+ }
+ );
+ }
+ );
+ }
};
$compile(element.contents())(scope);
diff --git a/ui/src/app/device/device-fieldset.tpl.html b/ui/src/app/device/device-fieldset.tpl.html
index a4d15f3..758c77f 100644
--- a/ui/src/app/device/device-fieldset.tpl.html
+++ b/ui/src/app/device/device-fieldset.tpl.html
@@ -39,10 +39,8 @@
<md-icon md-svg-icon="mdi:clipboard-arrow-left"></md-icon>
<span translate>device.copyId</span>
</md-button>
- <md-button ngclipboard data-clipboard-action="copy"
- ngclipboard-success="onAccessTokenCopied(e)"
- data-clipboard-text="{{deviceCredentials.credentialsId}}" ng-show="!isEdit"
- class="md-raised">
+ <md-button ng-show="!isEdit"
+ class="md-raised" ng-click="copyAccessToken($event)">
<md-icon md-svg-icon="mdi:clipboard-arrow-left"></md-icon>
<span translate>device.copyAccessToken</span>
</md-button>
ui/src/app/device/devices.tpl.html 6(+6 -0)
diff --git a/ui/src/app/device/devices.tpl.html b/ui/src/app/device/devices.tpl.html
index 1ec0134..513465d 100644
--- a/ui/src/app/device/devices.tpl.html
+++ b/ui/src/app/device/devices.tpl.html
@@ -74,4 +74,10 @@
entity-type="{{vm.types.entityType.device}}">
</tb-extension-table>
</md-tab>
+ <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.grid.isTenantAdmin()" md-on-select="vm.grid.triggerResize()" label="{{ 'audit-log.audit-logs' | translate }}">
+ <tb-audit-log-table flex entity-type="vm.types.entityType.device"
+ entity-id="vm.grid.operatingItem().id.id"
+ audit-log-mode="{{vm.types.auditLogMode.entity}}">
+ </tb-audit-log-table>
+ </md-tab>
</tb-grid>
ui/src/app/event/event-table.tpl.html 11(+9 -2)
diff --git a/ui/src/app/event/event-table.tpl.html b/ui/src/app/event/event-table.tpl.html
index 760b55b..e056671 100644
--- a/ui/src/app/event/event-table.tpl.html
+++ b/ui/src/app/event/event-table.tpl.html
@@ -26,9 +26,16 @@
</md-select>
</md-input-container>
<tb-timewindow flex ng-model="timewindow" history-only as-button="true"></tb-timewindow>
+ <md-button ng-disabled="$root.loading"
+ class="md-icon-button" ng-click="reload()">
+ <md-icon>refresh</md-icon>
+ <md-tooltip md-direction="top">
+ {{ 'action.refresh' | translate }}
+ </md-tooltip>
+ </md-button>
</section>
<md-list flex layout="column" class="md-whiteframe-z1 tb-event-table">
- <md-list class="tb-row tb-header" layout="row" tb-event-header event-type="{{eventType}}">
+ <md-list class="tb-row tb-header" layout="row" layout-align="start center" tb-event-header event-type="{{eventType}}">
</md-list>
<md-progress-linear style="max-height: 0px;" md-mode="indeterminate" ng-disabled="!$root.loading"
ng-show="$root.loading"></md-progress-linear>
@@ -38,7 +45,7 @@
class="tb-prompt" ng-show="noData()">event.no-events-prompt</span>
<md-virtual-repeat-container ng-show="hasData()" flex md-top-index="topIndex" tb-scope-element="repeatContainer">
<md-list-item md-virtual-repeat="event in theEvents" md-on-demand flex ng-style="hasScroll() ? {'margin-right':'-15px'} : {}">
- <md-list class="tb-row" flex layout="row" tb-event-row event-type="{{eventType}}" event="{{event}}">
+ <md-list class="tb-row" flex layout="row" layout-align="start center" tb-event-row event-type="{{eventType}}" event="{{event}}">
</md-list>
<md-divider flex></md-divider>
</md-list-item>
ui/src/app/layout/index.js 2(+2 -0)
diff --git a/ui/src/app/layout/index.js b/ui/src/app/layout/index.js
index c23b008..9102928 100644
--- a/ui/src/app/layout/index.js
+++ b/ui/src/app/layout/index.js
@@ -35,6 +35,7 @@ import thingsboardUserMenu from './user-menu.directive';
import thingsboardEntity from '../entity';
import thingsboardEvent from '../event';
import thingsboardAlarm from '../alarm';
+import thingsboardAuditLog from '../audit';
import thingsboardExtension from '../extension';
import thingsboardTenant from '../tenant';
import thingsboardCustomer from '../customer';
@@ -67,6 +68,7 @@ export default angular.module('thingsboard.home', [
thingsboardEntity,
thingsboardEvent,
thingsboardAlarm,
+ thingsboardAuditLog,
thingsboardExtension,
thingsboardTenant,
thingsboardCustomer,
ui/src/app/locale/locale.constant.js 35(+34 -1)
diff --git a/ui/src/app/locale/locale.constant.js b/ui/src/app/locale/locale.constant.js
index b9977f6..96dc152 100644
--- a/ui/src/app/locale/locale.constant.js
+++ b/ui/src/app/locale/locale.constant.js
@@ -286,6 +286,38 @@ export default angular.module('thingsboard.locale', [])
"selected-attributes": "{ count, select, 1 {1 attribute} other {# attributes} } selected",
"selected-telemetry": "{ count, select, 1 {1 telemetry unit} other {# telemetry units} } selected"
},
+ "audit-log": {
+ "audit": "Audit",
+ "audit-logs": "Audit Logs",
+ "timestamp": "Timestamp",
+ "entity-type": "Entity Type",
+ "entity-name": "Entity Name",
+ "user": "User",
+ "type": "Type",
+ "status": "Status",
+ "details": "Details",
+ "type-added": "Added",
+ "type-deleted": "Deleted",
+ "type-updated": "Updated",
+ "type-attributes-updated": "Attributes updated",
+ "type-attributes-deleted": "Attributes deleted",
+ "type-rpc-call": "RPC call",
+ "type-credentials-updated": "Credentials updated",
+ "type-assigned-to-customer": "Assigned to Customer",
+ "type-unassigned-from-customer": "Unassigned from Customer",
+ "type-activated": "Activated",
+ "type-suspended": "Suspended",
+ "type-credentials-read": "Credentials read",
+ "type-attributes-read": "Attributes read",
+ "status-success": "Success",
+ "status-failure": "Failure",
+ "audit-log-details": "Audit log details",
+ "no-audit-logs-prompt": "No logs found",
+ "action-data": "Action data",
+ "failure-details": "Failure details",
+ "search": "Search audit logs",
+ "clear-search": "Clear search"
+ },
"confirm-on-exit": {
"message": "You have unsaved changes. Are you sure you want to leave this page?",
"html-message": "You have unsaved changes.<br/>Are you sure you want to leave this page?",
@@ -1183,7 +1215,8 @@ export default angular.module('thingsboard.locale', [])
"activation-link": "User activation link",
"activation-link-text": "In order to activate user use the following <a href='{{activationLink}}' target='_blank'>activation link</a> :",
"copy-activation-link": "Copy activation link",
- "activation-link-copied-message": "User activation link has been copied to clipboard"
+ "activation-link-copied-message": "User activation link has been copied to clipboard",
+ "details": "Details"
},
"value": {
"type": "Value type",
ui/src/app/plugin/plugins.tpl.html 7(+7 -0)
diff --git a/ui/src/app/plugin/plugins.tpl.html b/ui/src/app/plugin/plugins.tpl.html
index b3563e9..eafb9f7 100644
--- a/ui/src/app/plugin/plugins.tpl.html
+++ b/ui/src/app/plugin/plugins.tpl.html
@@ -66,5 +66,12 @@
entity-type="{{vm.types.entityType.plugin}}">
</tb-relation-table>
</md-tab>
+ <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isPluginEditable(vm.grid.operatingItem()) && vm.grid.isTenantAdmin()"
+ md-on-select="vm.grid.triggerResize()" label="{{ 'audit-log.audit-logs' | translate }}">
+ <tb-audit-log-table flex entity-type="vm.types.entityType.plugin"
+ entity-id="vm.grid.operatingItem().id.id"
+ audit-log-mode="{{vm.types.auditLogMode.entity}}">
+ </tb-audit-log-table>
+ </md-tab>
</md-tabs>
</tb-grid>
ui/src/app/rule/rules.tpl.html 7(+7 -0)
diff --git a/ui/src/app/rule/rules.tpl.html b/ui/src/app/rule/rules.tpl.html
index 064728b..f46d124 100644
--- a/ui/src/app/rule/rules.tpl.html
+++ b/ui/src/app/rule/rules.tpl.html
@@ -66,5 +66,12 @@
entity-type="{{vm.types.entityType.rule}}">
</tb-relation-table>
</md-tab>
+ <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isRuleEditable(vm.grid.operatingItem()) && vm.grid.isTenantAdmin()"
+ md-on-select="vm.grid.triggerResize()" label="{{ 'audit-log.audit-logs' | translate }}">
+ <tb-audit-log-table flex entity-type="vm.types.entityType.rule"
+ entity-id="vm.grid.operatingItem().id.id"
+ audit-log-mode="{{vm.types.auditLogMode.entity}}">
+ </tb-audit-log-table>
+ </md-tab>
</md-tabs>
</tb-grid>
ui/src/app/services/clipboard.service.js 128(+128 -0)
diff --git a/ui/src/app/services/clipboard.service.js b/ui/src/app/services/clipboard.service.js
new file mode 100644
index 0000000..7c6d960
--- /dev/null
+++ b/ui/src/app/services/clipboard.service.js
@@ -0,0 +1,128 @@
+/*
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export default angular.module('thingsboard.clipboard', [])
+ .factory('clipboardService', ClipboardService)
+ .name;
+
+/*@ngInject*/
+function ClipboardService($q) {
+
+ var fakeHandler, fakeHandlerCallback, fakeElem;
+
+ var service = {
+ copyToClipboard: copyToClipboard
+ };
+
+ return service;
+
+ /* eslint-disable */
+ function copyToClipboard(trigger, text) {
+ var deferred = $q.defer();
+ const isRTL = document.documentElement.getAttribute('dir') == 'rtl';
+ removeFake();
+ fakeHandlerCallback = () => removeFake();
+ fakeHandler = document.body.addEventListener('click', fakeHandlerCallback) || true;
+ fakeElem = document.createElement('textarea');
+ fakeElem.style.fontSize = '12pt';
+ fakeElem.style.border = '0';
+ fakeElem.style.padding = '0';
+ fakeElem.style.margin = '0';
+ fakeElem.style.position = 'absolute';
+ fakeElem.style[ isRTL ? 'right' : 'left' ] = '-9999px';
+ let yPosition = window.pageYOffset || document.documentElement.scrollTop;
+ fakeElem.style.top = `${yPosition}px`;
+ fakeElem.setAttribute('readonly', '');
+ fakeElem.value = text;
+ document.body.appendChild(fakeElem);
+ var selectedText = select(fakeElem);
+
+ let succeeded;
+ try {
+ succeeded = document.execCommand('copy');
+ }
+ catch (err) {
+ succeeded = false;
+ }
+ if (trigger) {
+ trigger.focus();
+ }
+ window.getSelection().removeAllRanges();
+ removeFake();
+ if (succeeded) {
+ deferred.resolve(selectedText);
+ } else {
+ deferred.reject();
+ }
+ return deferred.promise;
+ }
+
+ function removeFake() {
+ if (fakeHandler) {
+ document.body.removeEventListener('click', fakeHandlerCallback);
+ fakeHandler = null;
+ fakeHandlerCallback = null;
+ }
+ if (fakeElem) {
+ document.body.removeChild(fakeElem);
+ fakeElem = null;
+ }
+ }
+
+ function select(element) {
+ var selectedText;
+
+ if (element.nodeName === 'SELECT') {
+ element.focus();
+
+ selectedText = element.value;
+ }
+ else if (element.nodeName === 'INPUT' || element.nodeName === 'TEXTAREA') {
+ var isReadOnly = element.hasAttribute('readonly');
+
+ if (!isReadOnly) {
+ element.setAttribute('readonly', '');
+ }
+
+ element.select();
+ element.setSelectionRange(0, element.value.length);
+
+ if (!isReadOnly) {
+ element.removeAttribute('readonly');
+ }
+
+ selectedText = element.value;
+ }
+ else {
+ if (element.hasAttribute('contenteditable')) {
+ element.focus();
+ }
+
+ var selection = window.getSelection();
+ var range = document.createRange();
+
+ range.selectNodeContents(element);
+ selection.removeAllRanges();
+ selection.addRange(range);
+
+ selectedText = selection.toString();
+ }
+
+ return selectedText;
+ }
+
+ /* eslint-enable */
+
+}
\ No newline at end of file
ui/src/app/services/menu.service.js 16(+16 -0)
diff --git a/ui/src/app/services/menu.service.js b/ui/src/app/services/menu.service.js
index 24db768..a492585 100644
--- a/ui/src/app/services/menu.service.js
+++ b/ui/src/app/services/menu.service.js
@@ -211,6 +211,12 @@ function Menu(userService, $state, $rootScope) {
type: 'link',
state: 'home.dashboards',
icon: 'dashboards'
+ },
+ {
+ name: 'audit-log.audit-logs',
+ type: 'link',
+ state: 'home.auditLogs',
+ icon: 'track_changes'
}];
homeSections =
@@ -273,6 +279,16 @@ function Menu(userService, $state, $rootScope) {
state: 'home.dashboards'
}
]
+ },
+ {
+ name: 'audit-log.audit',
+ places: [
+ {
+ name: 'audit-log.audit-logs',
+ icon: 'track_changes',
+ state: 'home.auditLogs'
+ }
+ ]
}];
} else if (authority === 'CUSTOMER_USER') {
ui/src/app/user/user.controller.js 2(+2 -0)
diff --git a/ui/src/app/user/user.controller.js b/ui/src/app/user/user.controller.js
index a9f4ce3..6a9fdde 100644
--- a/ui/src/app/user/user.controller.js
+++ b/ui/src/app/user/user.controller.js
@@ -42,6 +42,8 @@ export default function UserController(userService, toast, $scope, $mdDialog, $d
var vm = this;
+ vm.types = types;
+
vm.userGridConfig = {
deleteItemTitleFunc: deleteUserTitle,
deleteItemContentFunc: deleteUserText,
ui/src/app/user/users.tpl.html 22(+16 -6)
diff --git a/ui/src/app/user/users.tpl.html b/ui/src/app/user/users.tpl.html
index 72f67d2..03a5da2 100644
--- a/ui/src/app/user/users.tpl.html
+++ b/ui/src/app/user/users.tpl.html
@@ -19,10 +19,20 @@
<details-buttons tb-help="'users'" help-container-id="help-container">
<div id="help-container"></div>
</details-buttons>
- <tb-user user="vm.grid.operatingItem()"
- is-edit="vm.grid.detailsConfig.isDetailsEditMode"
- the-form="vm.grid.detailsForm"
- on-display-activation-link="vm.displayActivationLink(event, vm.grid.detailsConfig.currentItem)"
- on-resend-activation="vm.resendActivation(vm.grid.detailsConfig.currentItem)"
- on-delete-user="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"></tb-user>
+ <md-tabs ng-class="{'tb-headless': vm.grid.detailsConfig.isDetailsEditMode}"
+ id="tabs" md-border-bottom flex class="tb-absolute-fill">
+ <md-tab label="{{ 'user.details' | translate }}">
+ <tb-user user="vm.grid.operatingItem()"
+ is-edit="vm.grid.detailsConfig.isDetailsEditMode"
+ the-form="vm.grid.detailsForm"
+ on-display-activation-link="vm.displayActivationLink(event, vm.grid.detailsConfig.currentItem)"
+ on-resend-activation="vm.resendActivation(vm.grid.detailsConfig.currentItem)"
+ on-delete-user="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"></tb-user>
+ </md-tab>
+ <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.grid.isTenantAdmin()" md-on-select="vm.grid.triggerResize()" label="{{ 'audit-log.audit-logs' | translate }}">
+ <tb-audit-log-table flex user-id="vm.grid.operatingItem().id.id"
+ audit-log-mode="{{vm.types.auditLogMode.user}}">
+ </tb-audit-log-table>
+ </md-tab>
+ </md-tabs>
</tb-grid>
ui/src/scss/main.scss 13(+13 -0)
diff --git a/ui/src/scss/main.scss b/ui/src/scss/main.scss
index 93418b2..a961ab5 100644
--- a/ui/src/scss/main.scss
+++ b/ui/src/scss/main.scss
@@ -203,6 +203,19 @@ md-sidenav {
* THINGSBOARD SPECIFIC
***********************/
+label {
+ &.tb-title {
+ pointer-events: none;
+ color: #666;
+ font-size: 13px;
+ font-weight: 400;
+ padding-bottom: 15px;
+ &.no-padding {
+ padding-bottom: 0px;
+ }
+ }
+}
+
.tb-noselect {
-webkit-touch-callout: none; /* iOS Safari */
-webkit-user-select: none; /* Safari */