Browse Source

Merge pull request #586 from thingsboard/feature/audit_log

Feature/audit log
pull/602/head
Igor Kulikov 9 years ago
committed by GitHub
parent
commit
39682a9e87
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 89
      application/src/main/data/upgrade/1.4.0/schema_update.cql
  2. 31
      application/src/main/data/upgrade/1.4.0/schema_update.sql
  3. 4
      application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
  4. 74
      application/src/main/java/org/thingsboard/server/actors/plugin/PluginProcessingContext.java
  5. 3
      application/src/main/java/org/thingsboard/server/actors/plugin/SharedPluginProcessingContext.java
  6. 2
      application/src/main/java/org/thingsboard/server/actors/rpc/BasicRpcSessionListener.java
  7. 43
      application/src/main/java/org/thingsboard/server/config/AuditLogLevelProperties.java
  8. 6
      application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java
  9. 67
      application/src/main/java/org/thingsboard/server/controller/AssetController.java
  10. 114
      application/src/main/java/org/thingsboard/server/controller/AuditLogController.java
  11. 26
      application/src/main/java/org/thingsboard/server/controller/BaseController.java
  12. 27
      application/src/main/java/org/thingsboard/server/controller/CustomerController.java
  13. 71
      application/src/main/java/org/thingsboard/server/controller/DashboardController.java
  14. 81
      application/src/main/java/org/thingsboard/server/controller/DeviceController.java
  15. 42
      application/src/main/java/org/thingsboard/server/controller/PluginController.java
  16. 44
      application/src/main/java/org/thingsboard/server/controller/RuleController.java
  17. 22
      application/src/main/java/org/thingsboard/server/controller/UserController.java
  18. 6
      application/src/main/java/org/thingsboard/server/controller/plugin/PluginApiController.java
  19. 5
      application/src/main/java/org/thingsboard/server/controller/plugin/PluginWebSocketHandler.java
  20. 34
      application/src/main/java/org/thingsboard/server/install/ThingsboardInstallConfiguration.java
  21. 2
      application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java
  22. 6
      application/src/main/java/org/thingsboard/server/service/install/CassandraDatabaseUpgradeService.java
  23. 9
      application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java
  24. 21
      application/src/main/resources/thingsboard.yml
  25. 30
      application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java
  26. 148
      application/src/test/java/org/thingsboard/server/controller/BaseAuditLogControllerTest.java
  27. 23
      application/src/test/java/org/thingsboard/server/controller/nosql/AuditLogControllerNoSqlTest.java
  28. 23
      application/src/test/java/org/thingsboard/server/controller/sql/AuditLogControllerSqlTest.java
  29. 20
      common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionStatus.java
  30. 41
      common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java
  31. 60
      common/data/src/main/java/org/thingsboard/server/common/data/audit/AuditLog.java
  32. 35
      common/data/src/main/java/org/thingsboard/server/common/data/id/AuditLogId.java
  33. 47
      dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogDao.java
  34. 46
      dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogLevelFilter.java
  35. 34
      dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogLevelMask.java
  36. 70
      dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogQueryCursor.java
  37. 53
      dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogService.java
  38. 316
      dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java
  39. 349
      dao/src/main/java/org/thingsboard/server/dao/audit/CassandraAuditLogDao.java
  40. 63
      dao/src/main/java/org/thingsboard/server/dao/audit/DummyAuditLogServiceImpl.java
  41. 10
      dao/src/main/java/org/thingsboard/server/dao/cassandra/AbstractCassandraCluster.java
  42. 39
      dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
  43. 139
      dao/src/main/java/org/thingsboard/server/dao/model/nosql/AuditLogEntity.java
  44. 135
      dao/src/main/java/org/thingsboard/server/dao/model/sql/AuditLogEntity.java
  45. 26
      dao/src/main/java/org/thingsboard/server/dao/model/type/ActionStatusCodec.java
  46. 26
      dao/src/main/java/org/thingsboard/server/dao/model/type/ActionTypeCodec.java
  47. 24
      dao/src/main/java/org/thingsboard/server/dao/sql/audit/AuditLogRepository.java
  48. 155
      dao/src/main/java/org/thingsboard/server/dao/sql/audit/JpaAuditLogDao.java
  49. 75
      dao/src/main/resources/cassandra/schema.cql
  50. 15
      dao/src/main/resources/sql/schema.sql
  51. 2
      dao/src/test/java/org/thingsboard/server/dao/JpaDaoTestSuite.java
  52. 16
      dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java
  53. 21
      dao/src/test/java/org/thingsboard/server/dao/sql/audit/JpaAuditLogDaoTest.java
  54. 9
      dao/src/test/resources/application-test.properties
  55. 1
      dao/src/test/resources/sql/drop-all-tables.sql
  56. 20
      extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginApiCallSecurityContext.java
  57. 11
      extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginContext.java
  58. 2
      extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/ToDeviceRpcRequest.java
  59. 2
      extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/messaging/DeviceMessagingRuleMsgHandler.java
  60. 4
      extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/RpcManager.java
  61. 10
      extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/handlers/RpcRestMsgHandler.java
  62. 2
      extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/handlers/RpcRuleMsgHandler.java
  63. 27
      extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRestMsgHandler.java
  64. 4
      ui/src/app/alarm/alarm-table.tpl.html
  65. 116
      ui/src/app/api/audit-log.service.js
  66. 27
      ui/src/app/api/device.service.js
  67. 9
      ui/src/app/api/user.service.js
  68. 4
      ui/src/app/app.js
  69. 6
      ui/src/app/asset/assets.tpl.html
  70. 103
      ui/src/app/audit/audit-log-details-dialog.controller.js
  71. 23
      ui/src/app/audit/audit-log-details-dialog.scss
  72. 49
      ui/src/app/audit/audit-log-details-dialog.tpl.html
  73. 41
      ui/src/app/audit/audit-log-header.directive.js
  74. 24
      ui/src/app/audit/audit-log-header.tpl.html
  75. 67
      ui/src/app/audit/audit-log-row.directive.js
  76. 36
      ui/src/app/audit/audit-log-row.tpl.html
  77. 262
      ui/src/app/audit/audit-log-table.directive.js
  78. 68
      ui/src/app/audit/audit-log-table.tpl.html
  79. 44
      ui/src/app/audit/audit-log.routes.js
  80. 91
      ui/src/app/audit/audit-log.scss
  81. 24
      ui/src/app/audit/audit-logs.controller.js
  82. 23
      ui/src/app/audit/audit-logs.tpl.html
  83. 31
      ui/src/app/audit/index.js
  84. 57
      ui/src/app/common/types.constant.js
  85. 7
      ui/src/app/components/grid.directive.js
  86. 5
      ui/src/app/customer/customers.tpl.html
  87. 29
      ui/src/app/dashboard/dashboards.tpl.html
  88. 6
      ui/src/app/device/device-fieldset.tpl.html
  89. 27
      ui/src/app/device/device.directive.js
  90. 6
      ui/src/app/device/devices.tpl.html
  91. 11
      ui/src/app/event/event-table.tpl.html
  92. 2
      ui/src/app/layout/index.js
  93. 35
      ui/src/app/locale/locale.constant.js
  94. 7
      ui/src/app/plugin/plugins.tpl.html
  95. 7
      ui/src/app/rule/rules.tpl.html
  96. 128
      ui/src/app/services/clipboard.service.js
  97. 16
      ui/src/app/services/menu.service.js
  98. 2
      ui/src/app/user/user.controller.js
  99. 22
      ui/src/app/user/users.tpl.html
  100. 13
      ui/src/scss/main.scss

89
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' };

31
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)
);

4
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.alarm.AlarmService;
import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.attributes.AttributesService; 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.customer.CustomerService;
import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.event.EventService; import org.thingsboard.server.dao.event.EventService;
@ -113,6 +114,9 @@ public class ActorSystemContext {
@Autowired @Autowired
@Getter private RelationService relationService; @Getter private RelationService relationService;
@Autowired
@Getter private AuditLogService auditLogService;
@Autowired @Autowired
@Getter @Setter private PluginWebSocketMsgEndpoint wsMsgEndpoint; @Getter @Setter private PluginWebSocketMsgEndpoint wsMsgEndpoint;

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

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

2
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())); DeviceId deviceId = new DeviceId(toUUID(msg.getDeviceId()));
ToDeviceRpcRequestBody requestBody = new ToDeviceRpcRequestBody(msg.getMethod(), msg.getParams()); 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); return new ToDeviceRpcRequestPluginMsg(serverAddress, pluginId, pluginTenantId, request);
} }

43
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;
}
}

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

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

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

26
application/src/main/java/org/thingsboard/server/controller/BaseController.java

@ -15,9 +15,12 @@
*/ */
package org.thingsboard.server.controller; package org.thingsboard.server.controller;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired; 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.Authentication;
import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.ExceptionHandler; 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.AlarmId;
import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.asset.Asset; 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.id.*;
import org.thingsboard.server.common.data.page.TextPageLink; import org.thingsboard.server.common.data.page.TextPageLink;
import org.thingsboard.server.common.data.page.TimePageLink; 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.common.data.widget.WidgetsBundle;
import org.thingsboard.server.dao.alarm.AlarmService; import org.thingsboard.server.dao.alarm.AlarmService;
import org.thingsboard.server.dao.asset.AssetService; 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.customer.CustomerService;
import org.thingsboard.server.dao.dashboard.DashboardService; import org.thingsboard.server.dao.dashboard.DashboardService;
import org.thingsboard.server.dao.device.DeviceCredentialsService; 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 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!"; public static final String YOU_DON_T_HAVE_PERMISSION_TO_PERFORM_THIS_OPERATION = "You don't have permission to perform this operation!";
@Autowired @Autowired
private ThingsboardErrorResponseHandler errorResponseHandler; private ThingsboardErrorResponseHandler errorResponseHandler;
@ -117,6 +124,9 @@ public abstract class BaseController {
@Autowired @Autowired
protected RelationService relationService; protected RelationService relationService;
@Autowired
protected AuditLogService auditLogService;
@ExceptionHandler(ThingsboardException.class) @ExceptionHandler(ThingsboardException.class)
public void handleThingsboardException(ThingsboardException ex, HttpServletResponse response) { public void handleThingsboardException(ThingsboardException ex, HttpServletResponse response) {
errorResponseHandler.handle(ex, response); errorResponseHandler.handle(ex, response);
@ -540,4 +550,20 @@ public abstract class BaseController {
serverPort); serverPort);
return baseUrl; 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);
}
} }

27
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.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import org.thingsboard.server.common.data.Customer; 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.CustomerId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.TextPageData; 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 { public Customer saveCustomer(@RequestBody Customer customer) throws ThingsboardException {
try { try {
customer.setTenantId(getCurrentUser().getTenantId()); 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) { } catch (Exception e) {
logEntityAction(emptyId(EntityType.CUSTOMER), customer,
null, customer.getId() == null ? ActionType.ADDED : ActionType.UPDATED, e);
throw handleException(e); throw handleException(e);
} }
} }
@ -99,9 +111,20 @@ public class CustomerController extends BaseController {
checkParameter(CUSTOMER_ID, strCustomerId); checkParameter(CUSTOMER_ID, strCustomerId);
try { try {
CustomerId customerId = new CustomerId(toUUID(strCustomerId)); CustomerId customerId = new CustomerId(toUUID(strCustomerId));
checkCustomerId(customerId); Customer customer = checkCustomerId(customerId);
customerService.deleteCustomer(customerId); customerService.deleteCustomer(customerId);
logEntityAction(customerId, customer,
customer.getId(),
ActionType.DELETED, null, strCustomerId);
} catch (Exception e) { } catch (Exception e) {
logEntityAction(emptyId(EntityType.CUSTOMER),
null,
null,
ActionType.DELETED, e, strCustomerId);
throw handleException(e); throw handleException(e);
} }
} }

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

81
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.Customer;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntitySubtype; 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.CustomerId;
import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.TenantId; 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.page.TextPageLink;
import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.security.DeviceCredentials; 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.exception.IncorrectParameterException;
import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.exception.ThingsboardErrorCode; import org.thingsboard.server.exception.ThingsboardErrorCode;
@ -75,14 +78,22 @@ public class DeviceController extends BaseController {
} }
} }
Device savedDevice = checkNotNull(deviceService.saveDevice(device)); Device savedDevice = checkNotNull(deviceService.saveDevice(device));
actorService actorService
.onDeviceNameOrTypeUpdate( .onDeviceNameOrTypeUpdate(
savedDevice.getTenantId(), savedDevice.getTenantId(),
savedDevice.getId(), savedDevice.getId(),
savedDevice.getName(), savedDevice.getName(),
savedDevice.getType()); savedDevice.getType());
logEntityAction(savedDevice.getId(), savedDevice,
savedDevice.getCustomerId(),
device.getId() == null ? ActionType.ADDED : ActionType.UPDATED, null);
return savedDevice; return savedDevice;
} catch (Exception e) { } catch (Exception e) {
logEntityAction(emptyId(EntityType.DEVICE), device,
null, device.getId() == null ? ActionType.ADDED : ActionType.UPDATED, e);
throw handleException(e); throw handleException(e);
} }
} }
@ -94,9 +105,18 @@ public class DeviceController extends BaseController {
checkParameter(DEVICE_ID, strDeviceId); checkParameter(DEVICE_ID, strDeviceId);
try { try {
DeviceId deviceId = new DeviceId(toUUID(strDeviceId)); DeviceId deviceId = new DeviceId(toUUID(strDeviceId));
checkDeviceId(deviceId); Device device = checkDeviceId(deviceId);
deviceService.deleteDevice(deviceId); deviceService.deleteDevice(deviceId);
logEntityAction(deviceId, device,
device.getCustomerId(),
ActionType.DELETED, null, strDeviceId);
} catch (Exception e) { } catch (Exception e) {
logEntityAction(emptyId(EntityType.DEVICE),
null,
null,
ActionType.DELETED, e, strDeviceId);
throw handleException(e); throw handleException(e);
} }
} }
@ -110,13 +130,22 @@ public class DeviceController extends BaseController {
checkParameter(DEVICE_ID, strDeviceId); checkParameter(DEVICE_ID, strDeviceId);
try { try {
CustomerId customerId = new CustomerId(toUUID(strCustomerId)); CustomerId customerId = new CustomerId(toUUID(strCustomerId));
checkCustomerId(customerId); Customer customer = checkCustomerId(customerId);
DeviceId deviceId = new DeviceId(toUUID(strDeviceId)); DeviceId deviceId = new DeviceId(toUUID(strDeviceId));
checkDeviceId(deviceId); 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) { } catch (Exception e) {
logEntityAction(emptyId(EntityType.DEVICE), null,
null,
ActionType.ASSIGNED_TO_CUSTOMER, e, strDeviceId, strCustomerId);
throw handleException(e); throw handleException(e);
} }
} }
@ -132,8 +161,19 @@ public class DeviceController extends BaseController {
if (device.getCustomerId() == null || device.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) { if (device.getCustomerId() == null || device.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) {
throw new IncorrectParameterException("Device isn't assigned to any customer!"); 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) { } catch (Exception e) {
logEntityAction(emptyId(EntityType.DEVICE), null,
null,
ActionType.UNASSIGNED_FROM_CUSTOMER, e, strDeviceId);
throw handleException(e); throw handleException(e);
} }
} }
@ -147,8 +187,17 @@ public class DeviceController extends BaseController {
DeviceId deviceId = new DeviceId(toUUID(strDeviceId)); DeviceId deviceId = new DeviceId(toUUID(strDeviceId));
Device device = checkDeviceId(deviceId); Device device = checkDeviceId(deviceId);
Customer publicCustomer = customerService.findOrCreatePublicCustomer(device.getTenantId()); 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) { } catch (Exception e) {
logEntityAction(emptyId(EntityType.DEVICE), null,
null,
ActionType.ASSIGNED_TO_CUSTOMER, e, strDeviceId);
throw handleException(e); throw handleException(e);
} }
} }
@ -160,9 +209,16 @@ public class DeviceController extends BaseController {
checkParameter(DEVICE_ID, strDeviceId); checkParameter(DEVICE_ID, strDeviceId);
try { try {
DeviceId deviceId = new DeviceId(toUUID(strDeviceId)); DeviceId deviceId = new DeviceId(toUUID(strDeviceId));
checkDeviceId(deviceId); Device device = checkDeviceId(deviceId);
return checkNotNull(deviceCredentialsService.findDeviceCredentialsByDeviceId(deviceId)); DeviceCredentials deviceCredentials = checkNotNull(deviceCredentialsService.findDeviceCredentialsByDeviceId(deviceId));
logEntityAction(deviceId, device,
device.getCustomerId(),
ActionType.CREDENTIALS_READ, null, strDeviceId);
return deviceCredentials;
} catch (Exception e) { } catch (Exception e) {
logEntityAction(emptyId(EntityType.DEVICE), null,
null,
ActionType.CREDENTIALS_READ, e, strDeviceId);
throw handleException(e); throw handleException(e);
} }
} }
@ -173,11 +229,17 @@ public class DeviceController extends BaseController {
public DeviceCredentials saveDeviceCredentials(@RequestBody DeviceCredentials deviceCredentials) throws ThingsboardException { public DeviceCredentials saveDeviceCredentials(@RequestBody DeviceCredentials deviceCredentials) throws ThingsboardException {
checkNotNull(deviceCredentials); checkNotNull(deviceCredentials);
try { try {
checkDeviceId(deviceCredentials.getDeviceId()); Device device = checkDeviceId(deviceCredentials.getDeviceId());
DeviceCredentials result = checkNotNull(deviceCredentialsService.updateDeviceCredentials(deviceCredentials)); DeviceCredentials result = checkNotNull(deviceCredentialsService.updateDeviceCredentials(deviceCredentials));
actorService.onCredentialsUpdate(getCurrentUser().getTenantId(), deviceCredentials.getDeviceId()); actorService.onCredentialsUpdate(getCurrentUser().getTenantId(), deviceCredentials.getDeviceId());
logEntityAction(device.getId(), device,
device.getCustomerId(),
ActionType.CREDENTIALS_UPDATED, null, deviceCredentials);
return result; return result;
} catch (Exception e) { } catch (Exception e) {
logEntityAction(emptyId(EntityType.DEVICE), null,
null,
ActionType.CREDENTIALS_UPDATED, e, deviceCredentials);
throw handleException(e); throw handleException(e);
} }
} }
@ -306,5 +368,4 @@ public class DeviceController extends BaseController {
throw handleException(e); throw handleException(e);
} }
} }
} }

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

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

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

6
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.actors.service.ActorService;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId; 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.common.data.plugin.PluginMetaData;
import org.thingsboard.server.controller.BaseController; import org.thingsboard.server.controller.BaseController;
import org.thingsboard.server.dao.model.ModelConstants; 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())){ if(tenantId != null && ModelConstants.NULL_UUID.equals(tenantId.getId())){
tenantId = null; 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)); actorService.process(new BasicPluginRestMsg(securityCtx, new RestRequest(requestEntity, request), result));
} else { } else {
result.setResult(new ResponseEntity<>(HttpStatus.FORBIDDEN)); result.setResult(new ResponseEntity<>(HttpStatus.FORBIDDEN));

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

34
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<>());
}
}

2
application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java

@ -81,6 +81,8 @@ public class ThingsboardInstallService {
case "1.3.1": case "1.3.1":
log.info("Upgrading ThingsBoard from version 1.3.1 to 1.4.0 ..."); log.info("Upgrading ThingsBoard from version 1.3.1 to 1.4.0 ...");
databaseUpgradeService.upgradeDatabase("1.3.1");
log.info("Updating system data..."); log.info("Updating system data...");
systemDataLoaderService.deleteSystemWidgetBundle("charts"); systemDataLoaderService.deleteSystemWidgetBundle("charts");

6
application/src/main/java/org/thingsboard/server/service/install/CassandraDatabaseUpgradeService.java

@ -159,6 +159,12 @@ public class CassandraDatabaseUpgradeService implements DatabaseUpgradeService {
break; break;
case "1.3.0": case "1.3.0":
break; 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: default:
throw new RuntimeException("Unable to upgrade Cassandra database, unsupported fromVersion: " + fromVersion); throw new RuntimeException("Unable to upgrade Cassandra database, unsupported fromVersion: " + fromVersion);
} }

9
application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java

@ -61,6 +61,15 @@ public class SqlDatabaseUpgradeService implements DatabaseUpgradeService {
} }
log.info("Schema updated."); log.info("Schema updated.");
break; 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: default:
throw new RuntimeException("Unable to upgrade SQL database, unsupported fromVersion: " + fromVersion); throw new RuntimeException("Unable to upgrade SQL database, unsupported fromVersion: " + fromVersion);
} }

21
application/src/main/resources/thingsboard.yml

@ -282,7 +282,6 @@ spring:
username: "${SPRING_DATASOURCE_USERNAME:sa}" username: "${SPRING_DATASOURCE_USERNAME:sa}"
password: "${SPRING_DATASOURCE_PASSWORD:}" password: "${SPRING_DATASOURCE_PASSWORD:}"
# PostgreSQL DAO Configuration # PostgreSQL DAO Configuration
#spring: #spring:
# data: # data:
@ -298,3 +297,23 @@ spring:
# url: "${SPRING_DATASOURCE_URL:jdbc:postgresql://localhost:5432/thingsboard}" # url: "${SPRING_DATASOURCE_URL:jdbc:postgresql://localhost:5432/thingsboard}"
# username: "${SPRING_DATASOURCE_USERNAME:postgres}" # username: "${SPRING_DATASOURCE_USERNAME:postgres}"
# password: "${SPRING_DATASOURCE_PASSWORD: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}"

30
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.TenantId;
import org.thingsboard.server.common.data.id.UUIDBased; import org.thingsboard.server.common.data.id.UUIDBased;
import org.thingsboard.server.common.data.page.TextPageLink; 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.common.data.security.Authority;
import org.thingsboard.server.config.ThingsboardSecurityConfiguration; import org.thingsboard.server.config.ThingsboardSecurityConfiguration;
import org.thingsboard.server.service.mail.TestMailService; import org.thingsboard.server.service.mail.TestMailService;
@ -336,6 +337,35 @@ public abstract class AbstractControllerTest {
return readResponse(doGet(urlTemplate, vars).andExpect(status().isOk()), responseType); 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 { protected <T> T doPost(String urlTemplate, Class<T> responseClass, String... params) throws Exception {
return readResponse(doPost(urlTemplate, params).andExpect(status().isOk()), responseClass); return readResponse(doPost(urlTemplate, params).andExpect(status().isOk()), responseClass);
} }

148
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());
}
}

23
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 {
}

23
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 {
}

20
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
}

41
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;
}
}

60
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();
}
}

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

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

46
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;
}
}
}

34
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;
}
}

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

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

316
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!");
}
}
};
}

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

63
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;
}
}

10
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.core.ProtocolOptions.Compression;
import com.datastax.driver.mapping.Mapper; import com.datastax.driver.mapping.Mapper;
import com.datastax.driver.mapping.MappingManager; import com.datastax.driver.mapping.MappingManager;
import lombok.AccessLevel;
import lombok.Data;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@ -36,7 +33,6 @@ import java.util.Collections;
import java.util.List; import java.util.List;
@Slf4j @Slf4j
@Data
public abstract class AbstractCassandraCluster { public abstract class AbstractCassandraCluster {
private static final String COMMA = ","; private static final String COMMA = ",";
@ -77,7 +73,7 @@ public abstract class AbstractCassandraCluster {
private Cluster cluster; private Cluster cluster;
private Cluster.Builder clusterBuilder; private Cluster.Builder clusterBuilder;
@Getter(AccessLevel.NONE) private Session session; private Session session;
private MappingManager mappingManager; private MappingManager mappingManager;
@ -115,6 +111,10 @@ public abstract class AbstractCassandraCluster {
} }
} }
public Cluster getCluster() {
return cluster;
}
public Session getSession() { public Session getSession() {
if (!isInstall()) { if (!isInstall()) {
return session; return session;

39
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 ADDITIONAL_INFO_PROPERTY = "additional_info";
public static final String ENTITY_TYPE_PROPERTY = "entity_type"; 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. * Cassandra user constants.
*/ */
@ -134,6 +141,31 @@ public class ModelConstants {
public static final String DEVICE_BY_TENANT_AND_NAME_VIEW_NAME = "device_by_tenant_and_name"; public static final String DEVICE_BY_TENANT_AND_NAME_VIEW_NAME = "device_by_tenant_and_name";
public static final String DEVICE_TYPES_BY_TENANT_VIEW_NAME = "device_types_by_tenant"; 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. * Cassandra asset constants.
*/ */
@ -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_PARTITIONS_CF = "ts_kv_partitions_cf";
public static final String TS_KV_LATEST_CF = "ts_kv_latest_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 PARTITION_COLUMN = "partition";
public static final String KEY_COLUMN = "key"; public static final String KEY_COLUMN = "key";
public static final String TS_COLUMN = "ts"; public static final String TS_COLUMN = "ts";

139
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;
}
}

135
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;
}
}

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

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

24
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> {
}

155
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[]{}));
};
}
}

75
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 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) 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); 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' };

15
dao/src/main/resources/sql/schema.sql

@ -47,6 +47,21 @@ CREATE TABLE IF NOT EXISTS asset (
type varchar(255) 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 ( CREATE TABLE IF NOT EXISTS attribute_kv (
entity_type varchar(255), entity_type varchar(255),
entity_id varchar(31), entity_id varchar(31),

2
dao/src/test/java/org/thingsboard/server/dao/JpaDaoTestSuite.java

@ -24,7 +24,7 @@ import java.util.Arrays;
@RunWith(ClasspathSuite.class) @RunWith(ClasspathSuite.class)
@ClassnameFilters({ @ClassnameFilters({
"org.thingsboard.server.dao.sql.*AAATest" "org.thingsboard.server.dao.sql.*THIS_MUST_BE_FIXED_Test"
}) })
public class JpaDaoTestSuite { public class JpaDaoTestSuite {

16
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 com.fasterxml.jackson.databind.node.TextNode;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.test.annotation.DirtiesContext; 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.junit4.SpringRunner;
import org.springframework.test.context.support.AnnotationConfigContextLoader; import org.springframework.test.context.support.AnnotationConfigContextLoader;
import org.thingsboard.server.common.data.BaseData; 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.Event;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId; 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.common.data.rule.RuleMetaData;
import org.thingsboard.server.dao.alarm.AlarmService; import org.thingsboard.server.dao.alarm.AlarmService;
import org.thingsboard.server.dao.asset.AssetService; 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.component.ComponentDescriptorService;
import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.dashboard.DashboardService; import org.thingsboard.server.dao.dashboard.DashboardService;
@ -58,6 +62,8 @@ import org.thingsboard.server.dao.widget.WidgetsBundleService;
import java.io.IOException; import java.io.IOException;
import java.util.Comparator; import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.ThreadLocalRandom;
@ -227,4 +233,14 @@ public abstract class AbstractServiceTest {
oNode.set("configuration", readFromResource(configuration)); oNode.set("configuration", readFromResource(configuration));
return oNode; 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);
}
} }

21
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 {
}

9
dao/src/test/resources/application-test.properties

@ -4,6 +4,10 @@ zk.zk_dir=/thingsboard
updates.enabled=false updates.enabled=false
audit_log.enabled=true
audit_log.by_tenant_partitioning=MONTHS
audit_log.default_query_period=30
cache.type=caffeine cache.type=caffeine
#cache.type=redis #cache.type=redis
@ -16,7 +20,10 @@ caffeine.specs.deviceCredentials.maxSize=100000
caffeine.specs.devices.timeToLiveInMinutes=1440 caffeine.specs.devices.timeToLiveInMinutes=1440
caffeine.specs.devices.maxSize=100000 caffeine.specs.devices.maxSize=100000
caching.specs.devices.timeToLiveInMinutes=1440
caching.specs.devices.maxSize=100000
redis.connection.host=localhost redis.connection.host=localhost
redis.connection.port=6379 redis.connection.port=6379
redis.connection.db=0 redis.connection.db=0
redis.connection.password= redis.connection.password=

1
dao/src/test/resources/sql/drop-all-tables.sql

@ -1,6 +1,7 @@
DROP TABLE IF EXISTS admin_settings; DROP TABLE IF EXISTS admin_settings;
DROP TABLE IF EXISTS alarm; DROP TABLE IF EXISTS alarm;
DROP TABLE IF EXISTS asset; DROP TABLE IF EXISTS asset;
DROP TABLE IF EXISTS audit_log;
DROP TABLE IF EXISTS attribute_kv; DROP TABLE IF EXISTS attribute_kv;
DROP TABLE IF EXISTS component_descriptor; DROP TABLE IF EXISTS component_descriptor;
DROP TABLE IF EXISTS customer; DROP TABLE IF EXISTS customer;

20
extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginApiCallSecurityContext.java

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

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

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

2
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); pendingMsgs.put(uid, requestMd);
log.trace("[{}] Forwarding {} to [{}]", uid, params, targetDeviceId); log.trace("[{}] Forwarding {} to [{}]", uid, params, targetDeviceId);
ToDeviceRpcRequestBody requestBody = new ToDeviceRpcRequestBody(ON_MSG_METHOD_NAME, GSON.toJson(params.get("body"))); 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 { } else {
replyWithError(ctx, requestMd, RpcError.FORBIDDEN); replyWithError(ctx, requestMd, RpcError.FORBIDDEN);
} }

4
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); LocalRequestMetaData md = localRpcRequests.remove(requestId);
if (md != null) { if (md != null) {
log.trace("[{}] Processing local rpc response from device [{}]", requestId, md.getRequest().getDeviceId()); 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 { } else {
log.trace("[{}] Unknown or stale rpc response received [{}]", requestId, response); log.trace("[{}] Unknown or stale rpc response received [{}]", requestId, response);
} }
@ -62,7 +62,7 @@ public class RpcManager {
LocalRequestMetaData md = localRpcRequests.remove(requestId); LocalRequestMetaData md = localRpcRequests.remove(requestId);
if (md != null) { if (md != null) {
log.trace("[{}] Processing rpc timeout for local device [{}]", requestId, md.getRequest().getDeviceId()); 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);
} }
} }
} }

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

2
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 @Override
public void onSuccess(PluginContext ctx, Void value) { public void onSuccess(PluginContext ctx, Void value) {
ctx.sendRpcRequest(new ToDeviceRpcRequest(UUID.randomUUID(), 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); log.trace("[{}] Sent RPC Call Action msg", tmpId);
} }

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

4
ui/src/app/alarm/alarm-table.tpl.html

@ -29,7 +29,7 @@
</section> </section>
<div flex layout="column" class="tb-alarm-container md-whiteframe-z1"> <div flex layout="column" class="tb-alarm-container md-whiteframe-z1">
<md-list flex layout="column" class="tb-alarm-table"> <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-list>
<md-progress-linear style="max-height: 0px;" md-mode="indeterminate" ng-disabled="!$root.loading" <md-progress-linear style="max-height: 0px;" md-mode="indeterminate" ng-disabled="!$root.loading"
ng-show="$root.loading"></md-progress-linear> ng-show="$root.loading"></md-progress-linear>
@ -39,7 +39,7 @@
class="tb-prompt" ng-show="noData()">alarm.no-alarms-prompt</span> 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-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-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-list>
<md-divider flex></md-divider> <md-divider flex></md-divider>
</md-list-item> </md-list-item>

116
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;
}
}

27
ui/src/app/api/device.service.js

@ -20,7 +20,7 @@ export default angular.module('thingsboard.api.device', [thingsboardTypes])
.name; .name;
/*@ngInject*/ /*@ngInject*/
function DeviceService($http, $q, attributeService, customerService, types) { function DeviceService($http, $q, $window, userService, attributeService, customerService, types) {
var service = { var service = {
assignDeviceToCustomer: assignDeviceToCustomer, assignDeviceToCustomer: assignDeviceToCustomer,
@ -181,14 +181,27 @@ function DeviceService($http, $q, attributeService, customerService, types) {
return deferred.promise; return deferred.promise;
} }
function getDeviceCredentials(deviceId) { function getDeviceCredentials(deviceId, sync) {
var deferred = $q.defer(); var deferred = $q.defer();
var url = '/api/device/' + deviceId + '/credentials'; var url = '/api/device/' + deviceId + '/credentials';
$http.get(url, null).then(function success(response) { if (sync) {
deferred.resolve(response.data); var request = new $window.XMLHttpRequest();
}, function fail() { request.open('GET', url, false);
deferred.reject(); 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; return deferred.promise;
} }

9
ui/src/app/api/user.service.js

@ -54,6 +54,7 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi
refreshJwtToken: refreshJwtToken, refreshJwtToken: refreshJwtToken,
refreshTokenPending: refreshTokenPending, refreshTokenPending: refreshTokenPending,
updateAuthorizationHeader: updateAuthorizationHeader, updateAuthorizationHeader: updateAuthorizationHeader,
setAuthorizationRequestHeader: setAuthorizationRequestHeader,
gotoDefaultPlace: gotoDefaultPlace, gotoDefaultPlace: gotoDefaultPlace,
forceDefaultPlace: forceDefaultPlace, forceDefaultPlace: forceDefaultPlace,
updateLastPublicDashboardId: updateLastPublicDashboardId, updateLastPublicDashboardId: updateLastPublicDashboardId,
@ -367,6 +368,14 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi
return jwtToken; 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) { function getTenantAdmins(tenantId, pageLink) {
var deferred = $q.defer(); var deferred = $q.defer();
var url = '/api/tenant/' + tenantId + '/users?limit=' + pageLink.limit; var url = '/api/tenant/' + tenantId + '/users?limit=' + pageLink.limit;

4
ui/src/app/app.js

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

6
ui/src/app/asset/assets.tpl.html

@ -66,4 +66,10 @@
entity-type="{{vm.types.entityType.asset}}"> entity-type="{{vm.types.entityType.asset}}">
</tb-relation-table> </tb-relation-table>
</md-tab> </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> </tb-grid>

103
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 */

23
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;
}

49
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>

41
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
};
}

24
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>

67
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
};
}

36
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>

262
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: '@?'
}
};
}

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

44
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"}'
}
});
}

91
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;
}
}

24
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;
}

23
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>

31
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;

57
ui/src/app/common/types.constant.js

@ -156,6 +156,63 @@ export default angular.module('thingsboard.types', [])
color: "green" 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: { aliasFilterType: {
singleEntity: { singleEntity: {
value: 'singleEntity', value: 'singleEntity',

7
ui/src/app/components/grid.directive.js

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

5
ui/src/app/customer/customers.tpl.html

@ -66,5 +66,10 @@
entity-type="{{vm.types.entityType.customer}}"> entity-type="{{vm.types.entityType.customer}}">
</tb-relation-table> </tb-relation-table>
</md-tab> </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> </md-tabs>
</tb-grid> </tb-grid>

29
ui/src/app/dashboard/dashboards.tpl.html

@ -19,13 +19,24 @@
<details-buttons tb-help="'dashboards'" help-container-id="help-container"> <details-buttons tb-help="'dashboards'" help-container-id="help-container">
<div id="help-container"></div> <div id="help-container"></div>
</details-buttons> </details-buttons>
<tb-dashboard-details dashboard="vm.grid.operatingItem()" <md-tabs ng-class="{'tb-headless': vm.grid.detailsConfig.isDetailsEditMode}"
is-edit="vm.grid.detailsConfig.isDetailsEditMode" id="tabs" md-border-bottom flex class="tb-absolute-fill">
dashboard-scope="vm.dashboardsScope" <md-tab label="{{ 'dashboard.details' | translate }}">
the-form="vm.grid.detailsForm" <tb-dashboard-details dashboard="vm.grid.operatingItem()"
on-assign-to-customer="vm.assignToCustomer(event, [ vm.grid.detailsConfig.currentItem.id.id ])" is-edit="vm.grid.detailsConfig.isDetailsEditMode"
on-make-public="vm.makePublic(event, vm.grid.detailsConfig.currentItem)" dashboard-scope="vm.dashboardsScope"
on-unassign-from-customer="vm.unassignFromCustomer(event, vm.grid.detailsConfig.currentItem, isPublic)" the-form="vm.grid.detailsForm"
on-export-dashboard="vm.exportDashboard(event, vm.grid.detailsConfig.currentItem)" on-assign-to-customer="vm.assignToCustomer(event, [ vm.grid.detailsConfig.currentItem.id.id ])"
on-delete-dashboard="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"></tb-dashboard-details> 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> </tb-grid>

6
ui/src/app/device/device-fieldset.tpl.html

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

27
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 */ /* eslint-enable import/no-unresolved, import/default */
/*@ngInject*/ /*@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 linker = function (scope, element) {
var template = $templateCache.get(deviceFieldsetTemplate); var template = $templateCache.get(deviceFieldsetTemplate);
element.html(template); element.html(template);
@ -30,17 +30,8 @@ export default function DeviceDirective($compile, $templateCache, toast, $transl
scope.isPublic = false; scope.isPublic = false;
scope.assignedCustomer = null; scope.assignedCustomer = null;
scope.deviceCredentials = null;
scope.$watch('device', function(newVal) { scope.$watch('device', function(newVal) {
if (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) { if (scope.device.customerId && scope.device.customerId.id !== types.id.nullUid) {
scope.isAssignedToCustomer = true; scope.isAssignedToCustomer = true;
customerService.getShortCustomerInfo(scope.device.customerId.id).then( 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'); toast.showSuccess($translate.instant('device.idCopiedMessage'), 750, angular.element(element).parent().parent(), 'bottom left');
}; };
scope.onAccessTokenCopied = function() { scope.copyAccessToken = function(e) {
toast.showSuccess($translate.instant('device.accessTokenCopiedMessage'), 750, angular.element(element).parent().parent(), 'bottom left'); 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); $compile(element.contents())(scope);

6
ui/src/app/device/devices.tpl.html

@ -74,4 +74,10 @@
entity-type="{{vm.types.entityType.device}}"> entity-type="{{vm.types.entityType.device}}">
</tb-extension-table> </tb-extension-table>
</md-tab> </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> </tb-grid>

11
ui/src/app/event/event-table.tpl.html

@ -26,9 +26,16 @@
</md-select> </md-select>
</md-input-container> </md-input-container>
<tb-timewindow flex ng-model="timewindow" history-only as-button="true"></tb-timewindow> <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> </section>
<md-list flex layout="column" class="md-whiteframe-z1 tb-event-table"> <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-list>
<md-progress-linear style="max-height: 0px;" md-mode="indeterminate" ng-disabled="!$root.loading" <md-progress-linear style="max-height: 0px;" md-mode="indeterminate" ng-disabled="!$root.loading"
ng-show="$root.loading"></md-progress-linear> ng-show="$root.loading"></md-progress-linear>
@ -38,7 +45,7 @@
class="tb-prompt" ng-show="noData()">event.no-events-prompt</span> 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-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-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-list>
<md-divider flex></md-divider> <md-divider flex></md-divider>
</md-list-item> </md-list-item>

2
ui/src/app/layout/index.js

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

35
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-attributes": "{ count, select, 1 {1 attribute} other {# attributes} } selected",
"selected-telemetry": "{ count, select, 1 {1 telemetry unit} other {# telemetry units} } 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": { "confirm-on-exit": {
"message": "You have unsaved changes. Are you sure you want to leave this page?", "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?", "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": "User activation link",
"activation-link-text": "In order to activate user use the following <a href='{{activationLink}}' target='_blank'>activation link</a> :", "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", "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": { "value": {
"type": "Value type", "type": "Value type",

7
ui/src/app/plugin/plugins.tpl.html

@ -66,5 +66,12 @@
entity-type="{{vm.types.entityType.plugin}}"> entity-type="{{vm.types.entityType.plugin}}">
</tb-relation-table> </tb-relation-table>
</md-tab> </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> </md-tabs>
</tb-grid> </tb-grid>

7
ui/src/app/rule/rules.tpl.html

@ -66,5 +66,12 @@
entity-type="{{vm.types.entityType.rule}}"> entity-type="{{vm.types.entityType.rule}}">
</tb-relation-table> </tb-relation-table>
</md-tab> </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> </md-tabs>
</tb-grid> </tb-grid>

128
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 */
}

16
ui/src/app/services/menu.service.js

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

2
ui/src/app/user/user.controller.js

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

22
ui/src/app/user/users.tpl.html

@ -19,10 +19,20 @@
<details-buttons tb-help="'users'" help-container-id="help-container"> <details-buttons tb-help="'users'" help-container-id="help-container">
<div id="help-container"></div> <div id="help-container"></div>
</details-buttons> </details-buttons>
<tb-user user="vm.grid.operatingItem()" <md-tabs ng-class="{'tb-headless': vm.grid.detailsConfig.isDetailsEditMode}"
is-edit="vm.grid.detailsConfig.isDetailsEditMode" id="tabs" md-border-bottom flex class="tb-absolute-fill">
the-form="vm.grid.detailsForm" <md-tab label="{{ 'user.details' | translate }}">
on-display-activation-link="vm.displayActivationLink(event, vm.grid.detailsConfig.currentItem)" <tb-user user="vm.grid.operatingItem()"
on-resend-activation="vm.resendActivation(vm.grid.detailsConfig.currentItem)" is-edit="vm.grid.detailsConfig.isDetailsEditMode"
on-delete-user="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"></tb-user> 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> </tb-grid>

13
ui/src/scss/main.scss

@ -203,6 +203,19 @@ md-sidenav {
* THINGSBOARD SPECIFIC * 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 { .tb-noselect {
-webkit-touch-callout: none; /* iOS Safari */ -webkit-touch-callout: none; /* iOS Safari */
-webkit-user-select: none; /* Safari */ -webkit-user-select: none; /* Safari */

Loading…
Cancel
Save