diff --git a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java index b4c308db86..e50006a344 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java @@ -56,6 +56,7 @@ import org.thingsboard.server.dao.cassandra.CassandraCluster; import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.dashboard.DashboardService; import org.thingsboard.server.dao.device.ClaimDevicesService; +import org.thingsboard.server.dao.device.DeviceCredentialsService; import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.edge.EdgeEventService; import org.thingsboard.server.dao.edge.EdgeService; @@ -174,6 +175,10 @@ public class ActorSystemContext { @Getter private DeviceService deviceService; + @Autowired + @Getter + private DeviceCredentialsService deviceCredentialsService; + @Autowired @Getter private TbTenantProfileCache tenantProfileCache; diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java index 92fdfc6e48..0a371e7003 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java @@ -64,6 +64,7 @@ import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.cassandra.CassandraCluster; import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.dashboard.DashboardService; +import org.thingsboard.server.dao.device.DeviceCredentialsService; import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.edge.EdgeEventService; import org.thingsboard.server.dao.edge.EdgeService; @@ -478,6 +479,11 @@ class DefaultTbContext implements TbContext { return mainCtx.getDeviceService(); } + @Override + public DeviceCredentialsService getDeviceCredentialsService() { + return mainCtx.getDeviceCredentialsService(); + } + @Override public TbClusterService getClusterService() { return mainCtx.getClusterService(); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsService.java index 2fc2d7dcfb..42d6d10a1a 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsService.java @@ -18,8 +18,7 @@ package org.thingsboard.server.dao.device; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.security.DeviceCredentials; - -import java.util.List; +import com.fasterxml.jackson.databind.JsonNode; public interface DeviceCredentialsService { @@ -33,6 +32,8 @@ public interface DeviceCredentialsService { void formatCredentials(DeviceCredentials deviceCredentials); + JsonNode toCredentialsInfo(DeviceCredentials deviceCredentials); + void deleteDeviceCredentials(TenantId tenantId, DeviceCredentials deviceCredentials); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java index b6ea2b8c4f..c4cbabfb3d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java @@ -15,6 +15,8 @@ */ package org.thingsboard.server.dao.device; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.extern.slf4j.Slf4j; import org.eclipse.leshan.core.SecurityMode; import org.eclipse.leshan.core.util.SecurityUtil; @@ -136,6 +138,17 @@ public class DeviceCredentialsServiceImpl extends AbstractCachedEntityServicefetchToMetadata is set to true, otherwise, adds properties " + + "to the message data. If originator type is not DEVICE or rule node failed to get device credentials " + + "- send Message via Failure chain, otherwise Success chain is used.", + uiResources = {"static/rulenode/rulenode-core-config.js"}, + configDirective = "tbEnrichmentNodeFetchDeviceCredentialsConfig", + icon = "functions" +) +public class TbFetchDeviceCredentialsNode implements TbNode { + + private static final String CREDENTIALS = "credentials"; + private static final String CREDENTIALS_TYPE = "credentialsType"; + + TbFetchDeviceCredentialsNodeConfiguration config; + boolean fetchToMetadata; + + @Override + public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { + this.config = TbNodeUtils.convert(configuration, TbFetchDeviceCredentialsNodeConfiguration.class); + this.fetchToMetadata = config.isFetchToMetadata(); + } + + @Override + public void onMsg(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException, TbNodeException { + EntityId originator = msg.getOriginator(); + if (!EntityType.DEVICE.equals(originator.getEntityType())) { + ctx.tellFailure(msg, new RuntimeException("Unsupported originator type: " + originator.getEntityType() + "!")); + return; + } + DeviceId deviceId = new DeviceId(msg.getOriginator().getId()); + DeviceCredentials deviceCredentials = ctx.getDeviceCredentialsService().findDeviceCredentialsByDeviceId(ctx.getTenantId(), deviceId); + if (deviceCredentials == null) { + ctx.tellFailure(msg, new RuntimeException("Failed to get Device Credentials for device: " + deviceId + "!")); + return; + } + + TbMsg transformedMsg; + String credentialsType = deviceCredentials.getCredentialsType().name(); + JsonNode credentialsInfo = ctx.getDeviceCredentialsService().toCredentialsInfo(deviceCredentials); + if (fetchToMetadata) { + TbMsgMetaData metaData = msg.getMetaData(); + metaData.putValue(CREDENTIALS_TYPE, credentialsType); + metaData.putValue(CREDENTIALS, JacksonUtil.toString(credentialsInfo)); + transformedMsg = TbMsg.transformMsg(msg, msg.getType(), originator, metaData, msg.getData()); + } else { + ObjectNode data = (ObjectNode) JacksonUtil.toJsonNode(msg.getData()); + data.put(CREDENTIALS_TYPE, credentialsType); + data.set(CREDENTIALS, credentialsInfo); + transformedMsg = TbMsg.transformMsg(msg, msg.getType(), originator, msg.getMetaData(), JacksonUtil.toString(data)); + } + ctx.tellSuccess(transformedMsg); + } +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbFetchDeviceCredentialsNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbFetchDeviceCredentialsNodeConfiguration.java new file mode 100644 index 0000000000..9cda4de739 --- /dev/null +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbFetchDeviceCredentialsNodeConfiguration.java @@ -0,0 +1,34 @@ +/** + * Copyright © 2016-2022 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.rule.engine.metadata; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Data; +import org.thingsboard.rule.engine.api.NodeConfiguration; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class TbFetchDeviceCredentialsNodeConfiguration implements NodeConfiguration { + + private boolean fetchToMetadata; + + @Override + public TbFetchDeviceCredentialsNodeConfiguration defaultConfiguration() { + TbFetchDeviceCredentialsNodeConfiguration configuration = new TbFetchDeviceCredentialsNodeConfiguration(); + configuration.setFetchToMetadata(true); + return configuration; + } +} diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbFetchDeviceCredentialsNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbFetchDeviceCredentialsNodeTest.java new file mode 100644 index 0000000000..0fc7799bca --- /dev/null +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbFetchDeviceCredentialsNodeTest.java @@ -0,0 +1,156 @@ +/** + * Copyright © 2016-2022 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.rule.engine.metadata; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.rule.engine.api.TbNodeConfiguration; +import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.security.DeviceCredentials; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.TbMsgMetaData; +import org.thingsboard.server.common.msg.queue.TbMsgCallback; +import org.thingsboard.server.dao.device.DeviceCredentialsService; + +import java.util.Map; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.willAnswer; +import static org.mockito.BDDMockito.willReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.thingsboard.server.common.data.security.DeviceCredentialsType.ACCESS_TOKEN; + +public class TbFetchDeviceCredentialsNodeTest { + final ObjectMapper mapper = new ObjectMapper(); + + DeviceId deviceId; + TbFetchDeviceCredentialsNode node; + TbFetchDeviceCredentialsNodeConfiguration config; + TbNodeConfiguration nodeConfiguration; + TbContext ctx; + TbMsgCallback callback; + DeviceCredentialsService deviceCredentialsService; + + @BeforeEach + void setUp() throws TbNodeException { + deviceId = new DeviceId(UUID.randomUUID()); + callback = mock(TbMsgCallback.class); + ctx = mock(TbContext.class); + config = new TbFetchDeviceCredentialsNodeConfiguration().defaultConfiguration(); + config.setFetchToMetadata(true); + nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config)); + node = spy(new TbFetchDeviceCredentialsNode()); + node.init(ctx, nodeConfiguration); + deviceCredentialsService = mock(DeviceCredentialsService.class); + + willReturn(deviceCredentialsService).given(ctx).getDeviceCredentialsService(); + willAnswer(invocation -> { + DeviceCredentials deviceCredentials = new DeviceCredentials(); + deviceCredentials.setCredentialsType(ACCESS_TOKEN); + return deviceCredentials; + }).given(deviceCredentialsService).findDeviceCredentialsByDeviceId(any(), any()); + willAnswer(invocation -> { + return JacksonUtil.newObjectNode(); + }).given(deviceCredentialsService).toCredentialsInfo(any()); + } + + @AfterEach + void tearDown() { + node.destroy(); + } + + @Test + void givenDefaultConfig_whenInit_thenOK() { + assertThat(node.config).isEqualTo(config); + assertThat(node.fetchToMetadata).isEqualTo(true); + } + + @Test + void givenDefaultConfig_whenVerify_thenOK() { + TbFetchDeviceCredentialsNodeConfiguration defaultConfig = new TbFetchDeviceCredentialsNodeConfiguration().defaultConfiguration(); + assertThat(defaultConfig.isFetchToMetadata()).isEqualTo(true); + } + + @Test + void givenMsg_whenOnMsg_thenVerifyOutput() throws Exception { + node.onMsg(ctx, getTbMsg(deviceId)); + + ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + verify(ctx, times(1)).tellSuccess(newMsgCaptor.capture()); + verify(ctx, never()).tellFailure(any(), any()); + verify(deviceCredentialsService, times(1)).findDeviceCredentialsByDeviceId(any(), any()); + + TbMsg newMsg = newMsgCaptor.getValue(); + assertThat(newMsg).isNotNull(); + + assertThat(newMsg.getMetaData().getData().containsKey("credentials")).isEqualTo(true); + assertThat(newMsg.getMetaData().getData().containsKey("credentialsType")).isEqualTo(true); + } + + @Test + void givenUnsupportedOriginatorType_whenOnMsg_thenTellFailure() throws Exception { + node.onMsg(ctx, getTbMsg(new CustomerId(UUID.randomUUID()))); + + ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + ArgumentCaptor exceptionCaptor = ArgumentCaptor.forClass(Exception.class); + verify(ctx, never()).tellSuccess(any()); + verify(ctx, times(1)).tellFailure(newMsgCaptor.capture(), exceptionCaptor.capture()); + + assertThat(exceptionCaptor.getValue()).isInstanceOf(RuntimeException.class); + } + + @Test + void givenGetDeviceCredentials_whenOnMsg_thenTellFailure() throws Exception { + willAnswer(invocation -> { + return null; + }).given(deviceCredentialsService).findDeviceCredentialsByDeviceId(any(), any()); + + node.onMsg(ctx, getTbMsg(deviceId)); + + ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + ArgumentCaptor exceptionCaptor = ArgumentCaptor.forClass(Exception.class); + verify(ctx, never()).tellSuccess(any()); + verify(ctx, times(1)).tellFailure(newMsgCaptor.capture(), exceptionCaptor.capture()); + + assertThat(exceptionCaptor.getValue()).isInstanceOf(RuntimeException.class); + } + + private TbMsg getTbMsg(EntityId entityId) { + final Map mdMap = Map.of( + "country", "US", + "city", "NY" + ); + + final TbMsgMetaData metaData = new TbMsgMetaData(mdMap); + final String data = "{\"TestAttribute_1\": \"humidity\", \"TestAttribute_2\": \"voltage\"}"; + + return TbMsg.newMsg("POST_ATTRIBUTES_REQUEST", entityId, metaData, data, callback); + } +}