diff --git a/application/src/main/data/upgrade/3.1.1/schema_update_before.sql b/application/src/main/data/upgrade/3.1.1/schema_update_before.sql index c1591e7831..8f1d2f69a2 100644 --- a/application/src/main/data/upgrade/3.1.1/schema_update_before.sql +++ b/application/src/main/data/upgrade/3.1.1/schema_update_before.sql @@ -20,13 +20,16 @@ CREATE TABLE IF NOT EXISTS device_profile ( name varchar(255), type varchar(255), transport_type varchar(255), + provision_type varchar(255), profile_data jsonb, description varchar, search_text varchar(255), is_default boolean, tenant_id uuid, default_rule_chain_id uuid, + provision_device_key varchar, CONSTRAINT device_profile_name_unq_key UNIQUE (tenant_id, name), + CONSTRAINT device_provision_key_unq_key UNIQUE (provision_device_key), CONSTRAINT fk_default_rule_chain_device_profile FOREIGN KEY (default_rule_chain_id) REFERENCES rule_chain(id) ); diff --git a/application/src/main/java/org/thingsboard/server/controller/BaseController.java b/application/src/main/java/org/thingsboard/server/controller/BaseController.java index 8434f67d1d..7b68b18bd6 100644 --- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java +++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java @@ -706,6 +706,12 @@ public abstract class BaseController { case ASSIGNED_TO_TENANT: msgType = DataConstants.ENTITY_ASSIGNED_TO_TENANT; break; + case PROVISION_SUCCESS: + msgType = DataConstants.PROVISION_SUCCESS; + break; + case PROVISION_FAILURE: + msgType = DataConstants.PROVISION_FAILURE; + break; } if (!StringUtils.isEmpty(msgType)) { try { diff --git a/application/src/main/java/org/thingsboard/server/service/device/DeviceProvisionServiceImpl.java b/application/src/main/java/org/thingsboard/server/service/device/DeviceProvisionServiceImpl.java new file mode 100644 index 0000000000..2e2f1cbb80 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/device/DeviceProvisionServiceImpl.java @@ -0,0 +1,278 @@ +/** + * Copyright © 2016-2020 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.service.device; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; +import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.device.profile.AllowCreateNewDevicesDeviceProfileProvisionConfiguration; +import org.thingsboard.server.common.data.device.profile.CheckPreProvisionedDevicesDeviceProfileProvisionConfiguration; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; +import org.thingsboard.server.common.data.kv.StringDataEntry; +import org.thingsboard.server.common.data.security.DeviceCredentials; +import org.thingsboard.server.common.data.security.DeviceCredentialsType; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.TbMsgMetaData; +import org.thingsboard.server.common.msg.queue.ServiceType; +import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; +import org.thingsboard.server.dao.attributes.AttributesService; +import org.thingsboard.server.dao.audit.AuditLogService; +import org.thingsboard.server.dao.device.DeviceCredentialsService; +import org.thingsboard.server.dao.device.DeviceDao; +import org.thingsboard.server.dao.device.DeviceProfileDao; +import org.thingsboard.server.dao.device.DeviceProvisionService; +import org.thingsboard.server.dao.device.DeviceService; +import org.thingsboard.server.dao.device.provision.ProvisionRequest; +import org.thingsboard.server.dao.device.provision.ProvisionResponse; +import org.thingsboard.server.dao.device.provision.ProvisionResponseStatus; +import org.thingsboard.server.dao.util.mapping.JacksonUtil; +import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; +import org.thingsboard.server.queue.TbQueueCallback; +import org.thingsboard.server.queue.TbQueueProducer; +import org.thingsboard.server.queue.common.TbProtoQueueMsg; +import org.thingsboard.server.queue.discovery.PartitionService; +import org.thingsboard.server.queue.provider.TbQueueProducerProvider; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.state.DeviceStateService; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.locks.ReentrantLock; + + +@Service +@Slf4j +@TbCoreComponent +public class DeviceProvisionServiceImpl implements DeviceProvisionService { + + protected TbQueueProducer> ruleEngineMsgProducer; + + private static final String DEVICE_PROVISION_STATE = "provisionState"; + private static final String PROVISIONED_STATE = "provisioned"; + + private final ReentrantLock deviceCreationLock = new ReentrantLock(); + + @Autowired + DeviceDao deviceDao; + + @Autowired + DeviceProfileDao deviceProfileDao; + + @Autowired + DeviceService deviceService; + + @Autowired + DeviceCredentialsService deviceCredentialsService; + + @Autowired + AttributesService attributesService; + + @Autowired + DeviceStateService deviceStateService; + + @Autowired + AuditLogService auditLogService; + + @Autowired + PartitionService partitionService; + + public DeviceProvisionServiceImpl(TbQueueProducerProvider producerProvider) { + ruleEngineMsgProducer = producerProvider.getRuleEngineMsgProducer(); + } + + @Override + public ListenableFuture provisionDevice(ProvisionRequest provisionRequest) { + String provisionRequestKey = provisionRequest.getCredentials().getProvisionDeviceKey(); + String provisionRequestSecret = provisionRequest.getCredentials().getProvisionDeviceSecret(); + + if (StringUtils.isEmpty(provisionRequestKey) || StringUtils.isEmpty(provisionRequestSecret)) { + return Futures.immediateFuture(new ProvisionResponse(null, ProvisionResponseStatus.NOT_FOUND)); + } + + DeviceProfile targetProfile = deviceProfileDao.findByProvisionDeviceKey(provisionRequestKey); + + if (targetProfile == null) { + return Futures.immediateFuture(new ProvisionResponse(null, ProvisionResponseStatus.NOT_FOUND)); + } + + Device targetDevice = deviceDao.findDeviceByTenantIdAndName(targetProfile.getTenantId().getId(), provisionRequest.getDeviceName()).orElse(null); + + switch(targetProfile.getProvisionType()) { + case ALLOW_CREATE_NEW_DEVICES: + if (((AllowCreateNewDevicesDeviceProfileProvisionConfiguration) targetProfile.getProfileData().getProvisionConfiguration()).getProvisionDeviceSecret().equals(provisionRequestSecret)){ + if (targetDevice != null) { + log.warn("[{}] The device is present and could not be provisioned once more!", targetDevice.getName()); + notify(targetDevice, provisionRequest, DataConstants.PROVISION_FAILURE, false); + return Futures.immediateFuture(new ProvisionResponse(null, ProvisionResponseStatus.FAILURE)); + } else { + return createDevice(provisionRequest, targetProfile); + } + } + break; + case CHECK_PRE_PROVISIONED_DEVICES: + if (((CheckPreProvisionedDevicesDeviceProfileProvisionConfiguration) targetProfile.getProfileData().getProvisionConfiguration()).getProvisionDeviceSecret().equals(provisionRequestSecret)) { + if (targetDevice != null && targetDevice.getDeviceProfileId().equals(targetProfile.getId())) { + return processProvision(targetDevice, provisionRequest); + } else { + log.warn("[{}] Failed to find pre provisioned device!", provisionRequest.getDeviceName()); + return Futures.immediateFuture(new ProvisionResponse(null, ProvisionResponseStatus.FAILURE)); + } + } + break; + } + return Futures.immediateFuture(new ProvisionResponse(null, ProvisionResponseStatus.NOT_FOUND)); + } + + private ListenableFuture processProvision(Device device, ProvisionRequest provisionRequest) { + ListenableFuture> provisionStateFuture = attributesService.find(device.getTenantId(), device.getId(), + DataConstants.SERVER_SCOPE, DEVICE_PROVISION_STATE); + ListenableFuture provisionedFuture = Futures.transformAsync(provisionStateFuture, optionalAtr -> { + if (optionalAtr.isPresent()) { + String state = optionalAtr.get().getValueAsString(); + if (state.equals(PROVISIONED_STATE)) { + return Futures.immediateFuture(true); + } else { + log.error("[{}][{}] Unknown provision state: {}!", device.getName(), DEVICE_PROVISION_STATE, state); + return Futures.immediateCancelledFuture(); + } + } + return Futures.transform(saveProvisionStateAttribute(device), input -> false, MoreExecutors.directExecutor()); + }, MoreExecutors.directExecutor()); + if (provisionedFuture.isCancelled()) { + throw new RuntimeException("Unknown provision state!"); + } + return Futures.transform(provisionedFuture, provisioned -> { + if (provisioned) { + notify(device, provisionRequest, DataConstants.PROVISION_FAILURE, false); + return new ProvisionResponse(null, ProvisionResponseStatus.FAILURE); + } + notify(device, provisionRequest, DataConstants.PROVISION_SUCCESS, true); + return new ProvisionResponse(deviceCredentialsService.findDeviceCredentialsByDeviceId(device.getTenantId(), device.getId()), ProvisionResponseStatus.SUCCESS); + }, MoreExecutors.directExecutor()); + } + + private ListenableFuture createDevice(ProvisionRequest provisionRequest, DeviceProfile profile) { + deviceCreationLock.lock(); + try { + return processCreateDevice(provisionRequest, profile); + } finally { + deviceCreationLock.unlock(); + } + } + + private void notify(Device device, ProvisionRequest provisionRequest, String type, boolean success) { + pushProvisionEventToRuleEngine(provisionRequest, device, type); + logAction(device.getTenantId(), device.getCustomerId(), device, success, provisionRequest); + } + + private ListenableFuture processCreateDevice(ProvisionRequest provisionRequest, DeviceProfile profile) { + Device device = deviceService.findDeviceByTenantIdAndName(profile.getTenantId(), provisionRequest.getDeviceName()); + if (device == null) { + Device savedDevice = saveDevice(provisionRequest, profile); + + deviceStateService.onDeviceAdded(savedDevice); + pushDeviceCreatedEventToRuleEngine(savedDevice); + notify(savedDevice, provisionRequest, DataConstants.PROVISION_SUCCESS, true); + + return Futures.transform(saveProvisionStateAttribute(savedDevice), input -> + new ProvisionResponse( + getDeviceCredentials(savedDevice, provisionRequest.getX509CertPubKey()), + ProvisionResponseStatus.SUCCESS), MoreExecutors.directExecutor()); + } + log.warn("[{}] The device is already provisioned!", device.getName()); + notify(device, provisionRequest, DataConstants.PROVISION_FAILURE, false); + return Futures.immediateFuture(new ProvisionResponse(null, ProvisionResponseStatus.FAILURE)); + } + + private ListenableFuture> saveProvisionStateAttribute(Device device) { + return attributesService.save(device.getTenantId(), device.getId(), DataConstants.SERVER_SCOPE, + Collections.singletonList(new BaseAttributeKvEntry(new StringDataEntry(DEVICE_PROVISION_STATE, PROVISIONED_STATE), + System.currentTimeMillis()))); + } + + private Device saveDevice(ProvisionRequest provisionRequest, DeviceProfile profile) { + Device device = new Device(); + device.setName(provisionRequest.getDeviceName()); + device.setType(profile.getName()); + device.setTenantId(profile.getTenantId()); + return deviceService.saveDevice(device); + } + + private DeviceCredentials getDeviceCredentials(Device device, String x509CertPubKey) { + DeviceCredentials credentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(device.getTenantId(), device.getId()); + if (!StringUtils.isEmpty(x509CertPubKey)) { + credentials.setCredentialsType(DeviceCredentialsType.X509_CERTIFICATE); + credentials.setCredentialsValue(x509CertPubKey); + return deviceCredentialsService.updateDeviceCredentials(device.getTenantId(), credentials); + } + return credentials; + } + + private void pushProvisionEventToRuleEngine(ProvisionRequest request, Device device, String type) { + try { + ObjectNode entityNode = JacksonUtil.OBJECT_MAPPER.valueToTree(request); + TbMsg msg = TbMsg.newMsg(type, device.getId(), createTbMsgMetaData(device), JacksonUtil.OBJECT_MAPPER.writeValueAsString(entityNode)); + sendToRuleEngine(device.getTenantId(), msg, null); + } catch (JsonProcessingException | IllegalArgumentException e) { + log.warn("[{}] Failed to push device action to rule engine: {}", device.getId(), type, e); + } + } + + private void pushDeviceCreatedEventToRuleEngine(Device device) { + try { + ObjectNode entityNode = JacksonUtil.OBJECT_MAPPER.valueToTree(device); + TbMsg msg = TbMsg.newMsg(DataConstants.ENTITY_CREATED, device.getId(), createTbMsgMetaData(device), JacksonUtil.OBJECT_MAPPER.writeValueAsString(entityNode)); + sendToRuleEngine(device.getTenantId(), msg, null); + } catch (JsonProcessingException | IllegalArgumentException e) { + log.warn("[{}] Failed to push device action to rule engine: {}", device.getId(), DataConstants.ENTITY_CREATED, e); + } + } + + protected void sendToRuleEngine(TenantId tenantId, TbMsg tbMsg, TbQueueCallback callback) { + TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_RULE_ENGINE, tenantId, tbMsg.getOriginator()); + TransportProtos.ToRuleEngineMsg msg = TransportProtos.ToRuleEngineMsg.newBuilder().setTbMsg(TbMsg.toByteString(tbMsg)) + .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) + .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()).build(); + ruleEngineMsgProducer.send(tpi, new TbProtoQueueMsg<>(tbMsg.getId(), msg), callback); + } + + private TbMsgMetaData createTbMsgMetaData(Device device) { + TbMsgMetaData metaData = new TbMsgMetaData(); + metaData.putValue("tenantId", device.getTenantId().toString()); + return metaData; + } + + private void logAction(TenantId tenantId, CustomerId customerId, Device device, boolean success, ProvisionRequest provisionRequest) { + ActionType actionType = success ? ActionType.PROVISION_SUCCESS : ActionType.PROVISION_FAILURE; + auditLogService.logEntityAction(tenantId, customerId, new UserId(UserId.NULL_UUID), device.getName(), device.getId(), device, actionType, null, provisionRequest); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java index 7d41190ac0..7b4701754b 100644 --- a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java +++ b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java @@ -23,7 +23,6 @@ import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; import com.google.protobuf.ByteString; import lombok.extern.slf4j.Slf4j; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import org.thingsboard.server.common.data.DataConstants; @@ -31,6 +30,7 @@ import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.device.credentials.BasicMqttCredentials; +import org.thingsboard.server.common.data.device.profile.ProvisionDeviceProfileCredentials; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceProfileId; @@ -45,17 +45,23 @@ import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.transport.util.DataDecodingEncodingService; import org.thingsboard.server.dao.device.DeviceCredentialsService; import org.thingsboard.server.dao.device.DeviceProfileService; +import org.thingsboard.server.dao.device.DeviceProvisionService; import org.thingsboard.server.dao.device.DeviceService; +import org.thingsboard.server.dao.device.provision.ProvisionRequest; +import org.thingsboard.server.dao.device.provision.ProvisionResponse; +import org.thingsboard.server.dao.device.provision.ProvisionResponseStatus; import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.tenant.TenantProfileService; import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.util.mapping.JacksonUtil; import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.gen.transport.TransportProtos.CredentialsType; import org.thingsboard.server.gen.transport.TransportProtos.DeviceInfoProto; import org.thingsboard.server.gen.transport.TransportProtos.GetOrCreateDeviceFromGatewayRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.GetOrCreateDeviceFromGatewayResponseMsg; import org.thingsboard.server.gen.transport.TransportProtos.GetTenantRoutingInfoRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.GetTenantRoutingInfoResponseMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiResponseMsg; import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceCredentialsResponseMsg; @@ -94,6 +100,7 @@ public class DefaultTransportApiService implements TransportApiService { private final DbCallbackExecutorService dbCallbackExecutorService; private final TbClusterService tbClusterService; private final DataDecodingEncodingService dataDecodingEncodingService; + private final DeviceProvisionService deviceProvisionService; private final ConcurrentMap deviceCreationLocks = new ConcurrentHashMap<>(); @@ -102,7 +109,8 @@ public class DefaultTransportApiService implements TransportApiService { TenantProfileService tenantProfileService, DeviceService deviceService, RelationService relationService, DeviceCredentialsService deviceCredentialsService, DeviceStateService deviceStateService, DbCallbackExecutorService dbCallbackExecutorService, - TbClusterService tbClusterService, DataDecodingEncodingService dataDecodingEncodingService) { + TbClusterService tbClusterService, DataDecodingEncodingService dataDecodingEncodingService, + DeviceProvisionService deviceProvisionService) { this.deviceProfileService = deviceProfileService; this.tenantService = tenantService; this.tenantProfileService = tenantProfileService; @@ -113,6 +121,7 @@ public class DefaultTransportApiService implements TransportApiService { this.dbCallbackExecutorService = dbCallbackExecutorService; this.tbClusterService = tbClusterService; this.dataDecodingEncodingService = dataDecodingEncodingService; + this.deviceProvisionService = deviceProvisionService; } @Override @@ -139,6 +148,9 @@ public class DefaultTransportApiService implements TransportApiService { } else if (transportApiRequestMsg.hasGetDeviceProfileRequestMsg()) { return Futures.transform(handle(transportApiRequestMsg.getGetDeviceProfileRequestMsg()), value -> new TbProtoQueueMsg<>(tbProtoQueueMsg.getKey(), value, tbProtoQueueMsg.getHeaders()), MoreExecutors.directExecutor()); + } else if (transportApiRequestMsg.hasProvisionDeviceRequestMsg()) { + return Futures.transform(handle(transportApiRequestMsg.getProvisionDeviceRequestMsg()), + value -> new TbProtoQueueMsg<>(tbProtoQueueMsg.getKey(), value, tbProtoQueueMsg.getHeaders()), MoreExecutors.directExecutor()); } return Futures.transform(getEmptyTransportApiResponseFuture(), value -> new TbProtoQueueMsg<>(tbProtoQueueMsg.getKey(), value, tbProtoQueueMsg.getHeaders()), MoreExecutors.directExecutor()); @@ -263,6 +275,47 @@ public class DefaultTransportApiService implements TransportApiService { }, dbCallbackExecutorService); } + + private ListenableFuture handle(ProvisionDeviceRequestMsg requestMsg) { + ListenableFuture provisionResponseFuture = null; + provisionResponseFuture = deviceProvisionService.provisionDevice( + new ProvisionRequest( + requestMsg.getDeviceName(), + requestMsg.getX509CertPubKey(), + new ProvisionDeviceProfileCredentials( + requestMsg.getProvisionDeviceCredentialsMsg().getProvisionDeviceKey(), + requestMsg.getProvisionDeviceCredentialsMsg().getProvisionDeviceSecret()))); + return Futures.transform(provisionResponseFuture, provisionResponse -> { + if (provisionResponse.getResponseStatus() == ProvisionResponseStatus.NOT_FOUND) { + return getTransportApiResponseMsg(TransportProtos.DeviceCredentialsProto.getDefaultInstance(), TransportProtos.ProvisionResponseStatus.NOT_FOUND); + } else if (provisionResponse.getResponseStatus() == ProvisionResponseStatus.FAILURE) { + return getTransportApiResponseMsg(TransportProtos.DeviceCredentialsProto.getDefaultInstance(), TransportProtos.ProvisionResponseStatus.FAILURE); + } else { + return getTransportApiResponseMsg(getDeviceCredentials(provisionResponse.getDeviceCredentials()), TransportProtos.ProvisionResponseStatus.SUCCESS); + } + }, dbCallbackExecutorService); + } + + private TransportApiResponseMsg getTransportApiResponseMsg(TransportProtos.DeviceCredentialsProto deviceCredentials, TransportProtos.ProvisionResponseStatus status) { + return TransportApiResponseMsg.newBuilder() + .setProvisionDeviceResponseMsg(TransportProtos.ProvisionDeviceResponseMsg.newBuilder() + .setDeviceCredentials(deviceCredentials) + .setProvisionResponseStatus(status) + .build()) + .build(); + } + + private TransportProtos.DeviceCredentialsProto getDeviceCredentials(DeviceCredentials deviceCredentials) { + return TransportProtos.DeviceCredentialsProto.newBuilder() + .setDeviceIdMSB(deviceCredentials.getDeviceId().getId().getMostSignificantBits()) + .setDeviceIdLSB(deviceCredentials.getDeviceId().getId().getLeastSignificantBits()) + .setCredentialsType(deviceCredentials.getCredentialsType() == DeviceCredentialsType.ACCESS_TOKEN ? + CredentialsType.ACCESS_TOKEN : CredentialsType.X509_CERTIFICATE) + .setCredentialsId(deviceCredentials.getCredentialsId()) + .setCredentialsValue(deviceCredentials.getCredentialsValue() != null ? deviceCredentials.getCredentialsValue() : "") + .build(); + } + private ListenableFuture handle(GetTenantRoutingInfoRequestMsg requestMsg) { TenantId tenantId = new TenantId(new UUID(requestMsg.getTenantIdMSB(), requestMsg.getTenantIdLSB())); // TODO: Tenant Profile from cache diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java index 8d3391bb65..cd4a87d4de 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java @@ -68,6 +68,7 @@ import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileConfiguration; import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileTransportConfiguration; import org.thingsboard.server.common.data.device.profile.DeviceProfileData; +import org.thingsboard.server.common.data.device.profile.ProvisionDeviceProfileCredentials; import org.thingsboard.server.common.data.id.HasId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseDeviceProfileControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseDeviceProfileControllerTest.java index b2334d7c46..3376c16573 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseDeviceProfileControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseDeviceProfileControllerTest.java @@ -28,6 +28,7 @@ import org.thingsboard.server.common.data.DeviceProfileType; import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.device.profile.ProvisionDeviceProfileCredentials; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.security.Authority; @@ -153,6 +154,17 @@ public abstract class BaseDeviceProfileControllerTest extends AbstractController .andExpect(statusReason(containsString("Device profile with such name already exists"))); } + @Test + public void testSaveDeviceProfileWithSameProvisionDeviceKey() throws Exception { + DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile"); + deviceProfile.setProvisionDeviceKey("testProvisionDeviceKey"); + doPost("/api/deviceProfile", deviceProfile).andExpect(status().isOk()); + DeviceProfile deviceProfile2 = this.createDeviceProfile("Device Profile 2"); + deviceProfile2.setProvisionDeviceKey("testProvisionDeviceKey"); + doPost("/api/deviceProfile", deviceProfile2).andExpect(status().isBadRequest()) + .andExpect(statusReason(containsString("Device profile with such provision device key already exists"))); + } + @Ignore @Test public void testChangeDeviceProfileTypeWithExistingDevices() throws Exception { diff --git a/application/src/test/java/org/thingsboard/server/mqtt/AbstractMqttIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/AbstractMqttIntegrationTest.java index a25b6334e5..a11243533f 100644 --- a/application/src/test/java/org/thingsboard/server/mqtt/AbstractMqttIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/mqtt/AbstractMqttIntegrationTest.java @@ -26,13 +26,18 @@ import org.junit.Assert; import org.springframework.util.StringUtils; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.DeviceProfileProvisionType; import org.thingsboard.server.common.data.DeviceProfileType; import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.TransportPayloadType; import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.device.profile.AllowCreateNewDevicesDeviceProfileProvisionConfiguration; +import org.thingsboard.server.common.data.device.profile.CheckPreProvisionedDevicesDeviceProfileProvisionConfiguration; import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileConfiguration; import org.thingsboard.server.common.data.device.profile.DeviceProfileData; +import org.thingsboard.server.common.data.device.profile.DeviceProfileProvisionConfiguration; +import org.thingsboard.server.common.data.device.profile.DisabledDeviceProfileProvisionConfiguration; import org.thingsboard.server.common.data.device.profile.MqttDeviceProfileTransportConfiguration; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.DeviceCredentials; @@ -63,7 +68,18 @@ public abstract class AbstractMqttIntegrationTest extends AbstractControllerTest protected Device savedGateway; protected String gatewayAccessToken; - protected void processBeforeTest(String deviceName, String gatewayName, TransportPayloadType payloadType, String telemetryTopic, String attributesTopic) throws Exception { + protected void processBeforeTest (String deviceName, String gatewayName, TransportPayloadType payloadType, String telemetryTopic, String attributesTopic) throws Exception { + this.processBeforeTest(deviceName, gatewayName, payloadType, telemetryTopic, attributesTopic, DeviceProfileProvisionType.DISABLED, null, null); + } + + protected void processBeforeTest(String deviceName, + String gatewayName, + TransportPayloadType payloadType, + String telemetryTopic, + String attributesTopic, + DeviceProfileProvisionType provisionType, + String provisionKey, String provisionSecret + ) throws Exception { loginSysAdmin(); Tenant tenant = new Tenant(); @@ -92,7 +108,7 @@ public abstract class AbstractMqttIntegrationTest extends AbstractControllerTest gateway.setAdditionalInfo(additionalInfo); if (payloadType != null) { - DeviceProfile mqttDeviceProfile = createMqttDeviceProfile(payloadType, telemetryTopic, attributesTopic); + DeviceProfile mqttDeviceProfile = createMqttDeviceProfile(payloadType, telemetryTopic, attributesTopic, provisionType, provisionKey, provisionSecret); DeviceProfile savedDeviceProfile = doPost("/api/deviceProfile", mqttDeviceProfile, DeviceProfile.class); device.setType(savedDeviceProfile.getName()); device.setDeviceProfileId(savedDeviceProfile.getId()); @@ -182,11 +198,17 @@ public abstract class AbstractMqttIntegrationTest extends AbstractControllerTest return keyValueProtoBuilder.build(); } - protected DeviceProfile createMqttDeviceProfile(TransportPayloadType transportPayloadType, String telemetryTopic, String attributesTopic) { + protected DeviceProfile createMqttDeviceProfile(TransportPayloadType transportPayloadType, + String telemetryTopic, String attributesTopic, + DeviceProfileProvisionType provisionType, + String provisionKey, String provisionSecret + ) { DeviceProfile deviceProfile = new DeviceProfile(); deviceProfile.setName(transportPayloadType.name()); deviceProfile.setType(DeviceProfileType.DEFAULT); deviceProfile.setTransportType(DeviceTransportType.MQTT); + deviceProfile.setProvisionType(provisionType); + deviceProfile.setProvisionDeviceKey(provisionKey); deviceProfile.setDescription(transportPayloadType.name() + " Test"); DeviceProfileData deviceProfileData = new DeviceProfileData(); DefaultDeviceProfileConfiguration configuration = new DefaultDeviceProfileConfiguration(); @@ -199,6 +221,19 @@ public abstract class AbstractMqttIntegrationTest extends AbstractControllerTest transportConfiguration.setDeviceAttributesTopic(attributesTopic); } deviceProfileData.setTransportConfiguration(transportConfiguration); + DeviceProfileProvisionConfiguration provisionConfiguration; + switch (provisionType) { + case ALLOW_CREATE_NEW_DEVICES: + provisionConfiguration = new AllowCreateNewDevicesDeviceProfileProvisionConfiguration(provisionSecret); + break; + case CHECK_PRE_PROVISIONED_DEVICES: + provisionConfiguration = new CheckPreProvisionedDevicesDeviceProfileProvisionConfiguration(provisionSecret); + break; + case DISABLED: + default: + provisionConfiguration = new DisabledDeviceProfileProvisionConfiguration(provisionSecret); + } + deviceProfileData.setProvisionConfiguration(provisionConfiguration); deviceProfileData.setConfiguration(configuration); deviceProfile.setProfileData(deviceProfileData); deviceProfile.setDefault(false); diff --git a/application/src/test/java/org/thingsboard/server/mqtt/MqttSqlTestSuite.java b/application/src/test/java/org/thingsboard/server/mqtt/MqttSqlTestSuite.java index 095a5c3e7a..f71e8f37a2 100644 --- a/application/src/test/java/org/thingsboard/server/mqtt/MqttSqlTestSuite.java +++ b/application/src/test/java/org/thingsboard/server/mqtt/MqttSqlTestSuite.java @@ -31,7 +31,8 @@ import java.util.Arrays; "org.thingsboard.server.mqtt.telemetry.attributes.sql.*Test", "org.thingsboard.server.mqtt.attributes.updates.sql.*Test", "org.thingsboard.server.mqtt.attributes.request.sql.*Test", - "org.thingsboard.server.mqtt.claim.sql.*Test" + "org.thingsboard.server.mqtt.claim.sql.*Test", + "org.thingsboard.server.mqtt.provision.sql.*Test" }) public class MqttSqlTestSuite { diff --git a/application/src/test/java/org/thingsboard/server/mqtt/provision/AbstractMqttProvisionJsonDeviceTest.java b/application/src/test/java/org/thingsboard/server/mqtt/provision/AbstractMqttProvisionJsonDeviceTest.java new file mode 100644 index 0000000000..26d7e2afce --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/provision/AbstractMqttProvisionJsonDeviceTest.java @@ -0,0 +1,188 @@ +/** + * Copyright © 2016-2020 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.mqtt.provision; + +import com.google.gson.JsonObject; +import io.netty.handler.codec.mqtt.MqttQoS; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken; +import org.eclipse.paho.client.mqttv3.MqttAsyncClient; +import org.eclipse.paho.client.mqttv3.MqttCallback; +import org.eclipse.paho.client.mqttv3.MqttMessage; +import org.junit.After; +import org.junit.Assert; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceProfileProvisionType; +import org.thingsboard.server.common.data.TransportPayloadType; +import org.thingsboard.server.common.data.device.profile.MqttTopics; +import org.thingsboard.server.common.data.security.DeviceCredentials; +import org.thingsboard.server.common.transport.util.JsonUtils; +import org.thingsboard.server.dao.device.DeviceCredentialsService; +import org.thingsboard.server.dao.device.DeviceService; +import org.thingsboard.server.dao.device.provision.ProvisionResponseStatus; +import org.thingsboard.server.mqtt.AbstractMqttIntegrationTest; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +@Slf4j +public abstract class AbstractMqttProvisionJsonDeviceTest extends AbstractMqttIntegrationTest { + + @Autowired + DeviceCredentialsService deviceCredentialsService; + + @Autowired + DeviceService deviceService; + + @After + public void afterTest() throws Exception { + super.processAfterTest(); + } + + @Test + public void testProvisioningDisabledDevice() throws Exception { + processTestProvisioningDisabledDevice(); + } + + @Test + public void testProvisioningCheckPreProvisionedDevice() throws Exception { + processTestProvisioningCheckPreProvisionedDevice(); + } + + @Test + public void testProvisioningCreateNewDevice() throws Exception { + processTestProvisioningCreateNewDevice(); + } + + @Test + public void testProvisioningWithBadKeyDevice() throws Exception { + processTestProvisioningWithBadKeyDevice(); + } + + + protected void processTestProvisioningDisabledDevice() throws Exception { + super.processBeforeTest("Test Provision device", "Test Provision gateway", TransportPayloadType.JSON, null, null, DeviceProfileProvisionType.DISABLED, null, null); + byte[] result = createMqttClientAndPublish().getPayloadBytes(); + JsonObject response = JsonUtils.parse(new String(result)).getAsJsonObject(); + Assert.assertEquals("Provision data was not found!", response.get("errorMsg").getAsString()); + Assert.assertEquals(ProvisionResponseStatus.NOT_FOUND.name(), response.get("provisionDeviceStatus").getAsString()); + } + + + protected void processTestProvisioningCreateNewDevice() throws Exception { + super.processBeforeTest("Test Provision device3", "Test Provision gateway", TransportPayloadType.JSON, null, null, DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES, "testProvisionKey", "testProvisionSecret"); + byte[] result = createMqttClientAndPublish().getPayloadBytes(); + JsonObject response = JsonUtils.parse(new String(result)).getAsJsonObject(); + + Device createdDevice = deviceService.findDeviceByTenantIdAndName(savedTenant.getTenantId(), "Test Provision device"); + + Assert.assertNotNull(createdDevice); + Assert.assertEquals(createdDevice.getId().toString(), response.get("deviceId").getAsString()); + + DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(savedTenant.getTenantId(), createdDevice.getId()); + + Assert.assertEquals(deviceCredentials.getCredentialsType().name(), response.get("credentialsType").getAsString()); + Assert.assertEquals(deviceCredentials.getCredentialsId(), response.get("credentialsId").getAsString()); + Assert.assertEquals(ProvisionResponseStatus.SUCCESS.name(), response.get("provisionDeviceStatus").getAsString()); + } + + protected void processTestProvisioningCheckPreProvisionedDevice() throws Exception { + super.processBeforeTest("Test Provision device", "Test Provision gateway", TransportPayloadType.JSON, null, null, DeviceProfileProvisionType.CHECK_PRE_PROVISIONED_DEVICES, "testProvisionKey", "testProvisionSecret"); + byte[] result = createMqttClientAndPublish().getPayloadBytes(); + JsonObject response = JsonUtils.parse(new String(result)).getAsJsonObject(); + Assert.assertEquals(savedDevice.getId().toString(), response.get("deviceId").getAsString()); + + DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(savedTenant.getTenantId(), savedDevice.getId()); + + Assert.assertEquals(deviceCredentials.getCredentialsType().name(), response.get("credentialsType").getAsString()); + Assert.assertEquals(deviceCredentials.getCredentialsId(), response.get("credentialsId").getAsString()); + Assert.assertEquals(ProvisionResponseStatus.SUCCESS.name(), response.get("provisionDeviceStatus").getAsString()); + } + + protected void processTestProvisioningWithBadKeyDevice() throws Exception { + super.processBeforeTest("Test Provision device", "Test Provision gateway", TransportPayloadType.JSON, null, null, DeviceProfileProvisionType.CHECK_PRE_PROVISIONED_DEVICES, "testProvisionKeyOrig", "testProvisionSecret"); + byte[] result = createMqttClientAndPublish().getPayloadBytes(); + JsonObject response = JsonUtils.parse(new String(result)).getAsJsonObject(); + Assert.assertEquals("Provision data was not found!", response.get("errorMsg").getAsString()); + Assert.assertEquals(ProvisionResponseStatus.NOT_FOUND.name(), response.get("provisionDeviceStatus").getAsString()); + } + + protected TestMqttCallback createMqttClientAndPublish() throws Exception{ + String provisionRequestMsg = createTestProvisionMessage(); + MqttAsyncClient client = getMqttAsyncClient("provision"); + TestMqttCallback onProvisionCallback = getTestMqttCallback(); + client.setCallback(onProvisionCallback); + client.subscribe(MqttTopics.DEVICE_PROVISION_RESPONSE_TOPIC, MqttQoS.AT_MOST_ONCE.value()); + Thread.sleep(2000); + client.publish(MqttTopics.DEVICE_PROVISION_REQUEST_TOPIC, new MqttMessage(provisionRequestMsg.getBytes())); + onProvisionCallback.getLatch().await(3, TimeUnit.SECONDS); + return onProvisionCallback; + } + + + protected TestMqttCallback getTestMqttCallback() { + CountDownLatch latch = new CountDownLatch(1); + return new TestMqttCallback(latch); + } + + + protected static class TestMqttCallback implements MqttCallback { + + private final CountDownLatch latch; + private Integer qoS; + private byte[] payloadBytes; + + TestMqttCallback(CountDownLatch latch) { + this.latch = latch; + } + + public int getQoS() { + return qoS; + } + + public byte[] getPayloadBytes() { + return payloadBytes; + } + + public CountDownLatch getLatch() { + return latch; + } + + @Override + public void connectionLost(Throwable throwable) { + } + + @Override + public void messageArrived(String requestTopic, MqttMessage mqttMessage) throws Exception { + qoS = mqttMessage.getQos(); + payloadBytes = mqttMessage.getPayload(); + latch.countDown(); + } + + @Override + public void deliveryComplete(IMqttDeliveryToken iMqttDeliveryToken) { + + } + } + + + protected String createTestProvisionMessage() { + return "{\"deviceName\":\"Test Provision device\",\"provisionDeviceKey\":\"testProvisionKey\", \"provisionDeviceSecret\":\"testProvisionSecret\"}"; + } + +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/provision/AbstractMqttProvisionProtoDeviceTest.java b/application/src/test/java/org/thingsboard/server/mqtt/provision/AbstractMqttProvisionProtoDeviceTest.java new file mode 100644 index 0000000000..7720ddad4a --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/provision/AbstractMqttProvisionProtoDeviceTest.java @@ -0,0 +1,189 @@ +/** + * Copyright © 2016-2020 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.mqtt.provision; + +import com.google.gson.JsonObject; +import com.sun.mail.iap.ByteArray; +import io.netty.handler.codec.mqtt.MqttQoS; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken; +import org.eclipse.paho.client.mqttv3.MqttAsyncClient; +import org.eclipse.paho.client.mqttv3.MqttCallback; +import org.eclipse.paho.client.mqttv3.MqttMessage; +import org.junit.After; +import org.junit.Assert; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceProfileProvisionType; +import org.thingsboard.server.common.data.TransportPayloadType; +import org.thingsboard.server.common.data.device.profile.MqttTopics; +import org.thingsboard.server.common.data.security.DeviceCredentials; +import org.thingsboard.server.common.transport.util.JsonUtils; +import org.thingsboard.server.dao.device.DeviceCredentialsService; +import org.thingsboard.server.dao.device.DeviceService; +import org.thingsboard.server.dao.device.provision.ProvisionResponseStatus; +import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceCredentialsMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceRequestMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceResponseMsg; +import org.thingsboard.server.mqtt.AbstractMqttIntegrationTest; + +import java.util.UUID; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +@Slf4j +public abstract class AbstractMqttProvisionProtoDeviceTest extends AbstractMqttIntegrationTest { + + @Autowired + DeviceCredentialsService deviceCredentialsService; + + @Autowired + DeviceService deviceService; + + @After + public void afterTest() throws Exception { + super.processAfterTest(); + } + + @Test + public void testProvisioningDisabledDevice() throws Exception { + processTestProvisioningDisabledDevice(); + } + + @Test + public void testProvisioningCheckPreProvisionedDevice() throws Exception { + processTestProvisioningCheckPreProvisionedDevice(); + } + + @Test + public void testProvisioningCreateNewDevice() throws Exception { + processTestProvisioningCreateNewDevice(); + } + + @Test + public void testProvisioningWithBadKeyDevice() throws Exception { + processTestProvisioningWithBadKeyDevice(); + } + + + protected void processTestProvisioningDisabledDevice() throws Exception { + super.processBeforeTest("Test Provision device", "Test Provision gateway", TransportPayloadType.PROTOBUF, null, null, DeviceProfileProvisionType.DISABLED, null, null); + ProvisionDeviceResponseMsg result = ProvisionDeviceResponseMsg.parseFrom(createMqttClientAndPublish().getPayloadBytes()); + Assert.assertNotNull(result); + Assert.assertEquals(ProvisionResponseStatus.NOT_FOUND.name(), result.getProvisionResponseStatus().toString()); + } + + + protected void processTestProvisioningCreateNewDevice() throws Exception { + super.processBeforeTest("Test Provision device3", "Test Provision gateway", TransportPayloadType.JSON, null, null, DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES, "testProvisionKey", "testProvisionSecret"); + ProvisionDeviceResponseMsg response = ProvisionDeviceResponseMsg.parseFrom(createMqttClientAndPublish().getPayloadBytes()); + + Device createdDevice = deviceService.findDeviceByTenantIdAndName(savedTenant.getTenantId(), "Test Provision device"); + + Assert.assertNotNull(createdDevice); + Assert.assertEquals(createdDevice.getId().getId(), new UUID(response.getDeviceCredentials().getDeviceIdMSB(), response.getDeviceCredentials().getDeviceIdLSB())); + + DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(savedTenant.getTenantId(), createdDevice.getId()); + + Assert.assertEquals(deviceCredentials.getCredentialsType().name(), response.getDeviceCredentials().getCredentialsType().toString()); + Assert.assertEquals(deviceCredentials.getCredentialsId(), response.getDeviceCredentials().getCredentialsId()); + Assert.assertEquals(ProvisionResponseStatus.SUCCESS.name(), response.getProvisionResponseStatus().toString()); + } + + protected void processTestProvisioningCheckPreProvisionedDevice() throws Exception { + super.processBeforeTest("Test Provision device", "Test Provision gateway", TransportPayloadType.JSON, null, null, DeviceProfileProvisionType.CHECK_PRE_PROVISIONED_DEVICES, "testProvisionKey", "testProvisionSecret"); + ProvisionDeviceResponseMsg response = ProvisionDeviceResponseMsg.parseFrom(createMqttClientAndPublish().getPayloadBytes()); + Assert.assertEquals(savedDevice.getId().getId(), new UUID(response.getDeviceCredentials().getDeviceIdMSB(), response.getDeviceCredentials().getDeviceIdLSB())); + + DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(savedTenant.getTenantId(), savedDevice.getId()); + + Assert.assertEquals(deviceCredentials.getCredentialsType().name(), response.getDeviceCredentials().getCredentialsType().toString()); + Assert.assertEquals(deviceCredentials.getCredentialsId(), response.getDeviceCredentials().getCredentialsId()); + Assert.assertEquals(ProvisionResponseStatus.SUCCESS.name(), response.getProvisionResponseStatus().toString()); + } + + protected void processTestProvisioningWithBadKeyDevice() throws Exception { + super.processBeforeTest("Test Provision device", "Test Provision gateway", TransportPayloadType.JSON, null, null, DeviceProfileProvisionType.CHECK_PRE_PROVISIONED_DEVICES, "testProvisionKeyOrig", "testProvisionSecret"); + ProvisionDeviceResponseMsg response = ProvisionDeviceResponseMsg.parseFrom(createMqttClientAndPublish().getPayloadBytes()); + Assert.assertEquals(ProvisionResponseStatus.NOT_FOUND.name(), response.getProvisionResponseStatus().toString()); + } + + protected TestMqttCallback createMqttClientAndPublish() throws Exception{ + byte[] provisionRequestMsg = createTestProvisionMessage(); + MqttAsyncClient client = getMqttAsyncClient("provision"); + TestMqttCallback onProvisionCallback = getTestMqttCallback(); + client.setCallback(onProvisionCallback); + client.subscribe(MqttTopics.DEVICE_PROVISION_RESPONSE_TOPIC, MqttQoS.AT_MOST_ONCE.value()); + Thread.sleep(2000); + client.publish(MqttTopics.DEVICE_PROVISION_REQUEST_TOPIC, new MqttMessage(provisionRequestMsg)); + onProvisionCallback.getLatch().await(3, TimeUnit.SECONDS); + return onProvisionCallback; + } + + + protected TestMqttCallback getTestMqttCallback() { + CountDownLatch latch = new CountDownLatch(1); + return new TestMqttCallback(latch); + } + + + protected static class TestMqttCallback implements MqttCallback { + + private final CountDownLatch latch; + private Integer qoS; + private byte[] payloadBytes; + + TestMqttCallback(CountDownLatch latch) { + this.latch = latch; + } + + public int getQoS() { + return qoS; + } + + public byte[] getPayloadBytes() { + return payloadBytes; + } + + public CountDownLatch getLatch() { + return latch; + } + + @Override + public void connectionLost(Throwable throwable) { + } + + @Override + public void messageArrived(String requestTopic, MqttMessage mqttMessage) throws Exception { + qoS = mqttMessage.getQos(); + payloadBytes = mqttMessage.getPayload(); + latch.countDown(); + } + + @Override + public void deliveryComplete(IMqttDeliveryToken iMqttDeliveryToken) { + + } + } + + + protected byte[] createTestProvisionMessage() { + return ProvisionDeviceRequestMsg.newBuilder().setX509CertPubKey("").setDeviceName("Test Provision device").setProvisionDeviceCredentialsMsg(ProvisionDeviceCredentialsMsg.newBuilder().setProvisionDeviceKey("testProvisionKey").setProvisionDeviceSecret("testProvisionSecret")).build().toByteArray(); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/provision/sql/MqttProvisionDeviceJsonSqlTest.java b/application/src/test/java/org/thingsboard/server/mqtt/provision/sql/MqttProvisionDeviceJsonSqlTest.java new file mode 100644 index 0000000000..c9e6495fd3 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/provision/sql/MqttProvisionDeviceJsonSqlTest.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2020 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.mqtt.provision.sql; + +import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.mqtt.provision.AbstractMqttProvisionJsonDeviceTest; + +@DaoSqlTest +public class MqttProvisionDeviceJsonSqlTest extends AbstractMqttProvisionJsonDeviceTest { +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/provision/sql/MqttProvisionDeviceProtoSqlTest.java b/application/src/test/java/org/thingsboard/server/mqtt/provision/sql/MqttProvisionDeviceProtoSqlTest.java new file mode 100644 index 0000000000..7a8170d01e --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/mqtt/provision/sql/MqttProvisionDeviceProtoSqlTest.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2020 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.mqtt.provision.sql; + +import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.mqtt.provision.AbstractMqttProvisionProtoDeviceTest; + +@DaoSqlTest +public class MqttProvisionDeviceProtoSqlTest extends AbstractMqttProvisionProtoDeviceTest { +} diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceProvisionService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceProvisionService.java new file mode 100644 index 0000000000..28456fb4fe --- /dev/null +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceProvisionService.java @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2020 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.device; + +import com.google.common.util.concurrent.ListenableFuture; +import org.thingsboard.server.dao.device.provision.ProvisionRequest; +import org.thingsboard.server.dao.device.provision.ProvisionResponse; + +public interface DeviceProvisionService { + ListenableFuture provisionDevice(ProvisionRequest provisionRequest); +} diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/device/provision/ProvisionRequest.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/provision/ProvisionRequest.java new file mode 100644 index 0000000000..1c743fd855 --- /dev/null +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/provision/ProvisionRequest.java @@ -0,0 +1,28 @@ +/** + * Copyright © 2016-2020 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.device.provision; + +import lombok.AllArgsConstructor; +import lombok.Data; +import org.thingsboard.server.common.data.device.profile.ProvisionDeviceProfileCredentials; + +@Data +@AllArgsConstructor +public class ProvisionRequest { + private String deviceName; + private String x509CertPubKey; + private ProvisionDeviceProfileCredentials credentials; +} diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/device/provision/ProvisionResponse.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/provision/ProvisionResponse.java new file mode 100644 index 0000000000..d38b53b134 --- /dev/null +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/provision/ProvisionResponse.java @@ -0,0 +1,25 @@ +/** + * Copyright © 2016-2020 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.device.provision; + +import lombok.Data; +import org.thingsboard.server.common.data.security.DeviceCredentials; + +@Data +public class ProvisionResponse { + private final DeviceCredentials deviceCredentials; + private final ProvisionResponseStatus responseStatus; +} diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/device/provision/ProvisionResponseStatus.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/provision/ProvisionResponseStatus.java new file mode 100644 index 0000000000..46764f1b67 --- /dev/null +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/provision/ProvisionResponseStatus.java @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2020 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.device.provision; + +public enum ProvisionResponseStatus { + UNKNOWN, + SUCCESS, + NOT_FOUND, + FAILURE + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java b/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java index 8dc492093c..d2c4aaf60b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java @@ -63,6 +63,8 @@ public class DataConstants { public static final String ALARM_CLEAR = "ALARM_CLEAR"; public static final String ENTITY_ASSIGNED_FROM_TENANT = "ENTITY_ASSIGNED_FROM_TENANT"; public static final String ENTITY_ASSIGNED_TO_TENANT = "ENTITY_ASSIGNED_TO_TENANT"; + public static final String PROVISION_SUCCESS = "PROVISION_SUCCESS"; + public static final String PROVISION_FAILURE = "PROVISION_FAILURE"; public static final String RPC_CALL_FROM_SERVER_TO_DEVICE = "RPC_CALL_FROM_SERVER_TO_DEVICE"; @@ -70,4 +72,12 @@ public class DataConstants { public static final String SECRET_KEY_FIELD_NAME = "secretKey"; public static final String DURATION_MS_FIELD_NAME = "durationMs"; + public static final String PROVISION = "provision"; + public static final String PROVISION_KEY = "provisionDeviceKey"; + public static final String PROVISION_SECRET = "provisionDeviceSecret"; + + public static final String DEVICE_NAME = "deviceName"; + public static final String DEVICE_TYPE = "deviceType"; + public static final String CERT_PUB_KEY = "x509CertPubKey"; + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfile.java b/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfile.java index 097d64b198..10990bc436 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfile.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfile.java @@ -41,10 +41,12 @@ public class DeviceProfile extends SearchTextBased implements H private boolean isDefault; private DeviceProfileType type; private DeviceTransportType transportType; + private DeviceProfileProvisionType provisionType; private RuleChainId defaultRuleChainId; private transient DeviceProfileData profileData; @JsonIgnore private byte[] profileDataBytes; + private String provisionDeviceKey; public DeviceProfile() { super(); @@ -62,6 +64,7 @@ public class DeviceProfile extends SearchTextBased implements H this.isDefault = deviceProfile.isDefault(); this.defaultRuleChainId = deviceProfile.getDefaultRuleChainId(); this.setProfileData(deviceProfile.getProfileData()); + this.provisionDeviceKey = deviceProfile.getProvisionDeviceKey(); } @Override diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfileProvisionType.java b/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfileProvisionType.java new file mode 100644 index 0000000000..f33c705d52 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfileProvisionType.java @@ -0,0 +1,22 @@ +/** + * Copyright © 2016-2020 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; + +public enum DeviceProfileProvisionType { + DISABLED, + ALLOW_CREATE_NEW_DEVICES, + CHECK_PRE_PROVISIONED_DEVICES +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java b/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java index 14a38c810c..e30e20090d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java @@ -42,7 +42,9 @@ public enum ActionType { LOGOUT(false), LOCKOUT(false), ASSIGNED_FROM_TENANT(false), - ASSIGNED_TO_TENANT(false); + ASSIGNED_TO_TENANT(false), + PROVISION_SUCCESS(false), + PROVISION_FAILURE(false); private final boolean isRead; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AllowCreateNewDevicesDeviceProfileProvisionConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AllowCreateNewDevicesDeviceProfileProvisionConfiguration.java new file mode 100644 index 0000000000..8414caa400 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AllowCreateNewDevicesDeviceProfileProvisionConfiguration.java @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2020 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.device.profile; + +import lombok.Data; +import org.thingsboard.server.common.data.DeviceProfileProvisionType; + +@Data +public class AllowCreateNewDevicesDeviceProfileProvisionConfiguration implements DeviceProfileProvisionConfiguration { + + private final String provisionDeviceSecret; + + @Override + public DeviceProfileProvisionType getType() { + return DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/CheckPreProvisionedDevicesDeviceProfileProvisionConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/CheckPreProvisionedDevicesDeviceProfileProvisionConfiguration.java new file mode 100644 index 0000000000..40af29ea51 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/CheckPreProvisionedDevicesDeviceProfileProvisionConfiguration.java @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2020 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.device.profile; + +import lombok.Data; +import org.thingsboard.server.common.data.DeviceProfileProvisionType; + +@Data +public class CheckPreProvisionedDevicesDeviceProfileProvisionConfiguration implements DeviceProfileProvisionConfiguration { + + private final String provisionDeviceSecret; + + @Override + public DeviceProfileProvisionType getType() { + return DeviceProfileProvisionType.CHECK_PRE_PROVISIONED_DEVICES; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileData.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileData.java index 275e6269d6..7cb3304934 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileData.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileData.java @@ -24,6 +24,7 @@ public class DeviceProfileData { private DeviceProfileConfiguration configuration; private DeviceProfileTransportConfiguration transportConfiguration; + private DeviceProfileProvisionConfiguration provisionConfiguration; private List alarms; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileProvisionConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileProvisionConfiguration.java new file mode 100644 index 0000000000..140c7334ec --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileProvisionConfiguration.java @@ -0,0 +1,39 @@ +/** + * Copyright © 2016-2020 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.device.profile; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import org.thingsboard.server.common.data.DeviceProfileProvisionType; + + +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.PROPERTY, + property = "type") +@JsonSubTypes({ + @JsonSubTypes.Type(value = DisabledDeviceProfileProvisionConfiguration.class, name = "DISABLED"), + @JsonSubTypes.Type(value = AllowCreateNewDevicesDeviceProfileProvisionConfiguration.class, name = "ALLOW_CREATE_NEW_DEVICES"), + @JsonSubTypes.Type(value = CheckPreProvisionedDevicesDeviceProfileProvisionConfiguration.class, name = "CHECK_PRE_PROVISIONED_DEVICES")}) +public interface DeviceProfileProvisionConfiguration { + + @JsonIgnore + DeviceProfileProvisionType getType(); + +} \ No newline at end of file diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DisabledDeviceProfileProvisionConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DisabledDeviceProfileProvisionConfiguration.java new file mode 100644 index 0000000000..01876c810d --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DisabledDeviceProfileProvisionConfiguration.java @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2020 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.device.profile; + +import lombok.Data; +import org.thingsboard.server.common.data.DeviceProfileProvisionType; + +@Data +public class DisabledDeviceProfileProvisionConfiguration implements DeviceProfileProvisionConfiguration { + + private final String provisionDeviceSecret; + + @Override + public DeviceProfileProvisionType getType() { + return DeviceProfileProvisionType.DISABLED; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/MqttTopics.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/MqttTopics.java index ba8ac7f3be..23c173ae97 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/MqttTopics.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/MqttTopics.java @@ -20,6 +20,8 @@ package org.thingsboard.server.common.data.device.profile; */ public class MqttTopics { + private static final String REQUEST = "/request"; + private static final String RESPONSE = "/response"; private static final String RPC = "/rpc"; private static final String CONNECT = "/connect"; private static final String DISCONNECT = "/disconnect"; @@ -27,15 +29,18 @@ public class MqttTopics { private static final String ATTRIBUTES = "/attributes"; private static final String CLAIM = "/claim"; private static final String SUB_TOPIC = "+"; - private static final String ATTRIBUTES_RESPONSE = "/attributes/response"; - private static final String ATTRIBUTES_REQUEST = "/attributes/request"; + private static final String PROVISION = "/provision"; - private static final String DEVICE_RPC_RESPONSE = "/rpc/response/"; - private static final String DEVICE_RPC_REQUEST = "/rpc/request/"; + private static final String ATTRIBUTES_RESPONSE = ATTRIBUTES + RESPONSE; + private static final String ATTRIBUTES_REQUEST = ATTRIBUTES + REQUEST; + + private static final String DEVICE_RPC_RESPONSE = RPC + RESPONSE + "/"; + private static final String DEVICE_RPC_REQUEST = RPC + REQUEST + "/"; private static final String DEVICE_ATTRIBUTES_RESPONSE = ATTRIBUTES_RESPONSE + "/"; private static final String DEVICE_ATTRIBUTES_REQUEST = ATTRIBUTES_REQUEST + "/"; + private static final String PROVISION_RESPONSE = PROVISION + RESPONSE; // V1_JSON topics public static final String BASE_DEVICE_API_TOPIC = "v1/devices/me"; @@ -50,6 +55,8 @@ public class MqttTopics { public static final String DEVICE_TELEMETRY_TOPIC = BASE_DEVICE_API_TOPIC + TELEMETRY; public static final String DEVICE_CLAIM_TOPIC = BASE_DEVICE_API_TOPIC + CLAIM; public static final String DEVICE_ATTRIBUTES_TOPIC = BASE_DEVICE_API_TOPIC + ATTRIBUTES; + public static final String DEVICE_PROVISION_REQUEST_TOPIC = BASE_DEVICE_API_TOPIC + PROVISION; + public static final String DEVICE_PROVISION_RESPONSE_TOPIC = BASE_DEVICE_API_TOPIC + PROVISION_RESPONSE; // V1_JSON gateway topics @@ -62,6 +69,8 @@ public class MqttTopics { public static final String GATEWAY_RPC_TOPIC = BASE_GATEWAY_API_TOPIC + RPC; public static final String GATEWAY_ATTRIBUTES_REQUEST_TOPIC = BASE_GATEWAY_API_TOPIC + ATTRIBUTES_REQUEST; public static final String GATEWAY_ATTRIBUTES_RESPONSE_TOPIC = BASE_GATEWAY_API_TOPIC + ATTRIBUTES_RESPONSE; + public static final String GATEWAY_PROVISION_REQUEST_TOPIC = BASE_GATEWAY_API_TOPIC + PROVISION; + public static final String GATEWAY_PROVISION_RESPONSE_TOPIC = BASE_GATEWAY_API_TOPIC + PROVISION_RESPONSE; private MqttTopics() { } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/ProvisionDeviceProfileCredentials.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/ProvisionDeviceProfileCredentials.java new file mode 100644 index 0000000000..659da71488 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/ProvisionDeviceProfileCredentials.java @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2020 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.device.profile; + +import lombok.Data; + +@Data +public class ProvisionDeviceProfileCredentials { + private final String provisionDeviceKey; + private final String provisionDeviceSecret; +} diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/session/FeatureType.java b/common/message/src/main/java/org/thingsboard/server/common/msg/session/FeatureType.java index 9f421c0e73..58289cc94b 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/session/FeatureType.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/session/FeatureType.java @@ -16,5 +16,5 @@ package org.thingsboard.server.common.msg.session; public enum FeatureType { - ATTRIBUTES, TELEMETRY, RPC, CLAIM + ATTRIBUTES, TELEMETRY, RPC, CLAIM, PROVISION } diff --git a/common/queue/src/main/proto/queue.proto b/common/queue/src/main/proto/queue.proto index 331c17b9d7..6a96f8a6d6 100644 --- a/common/queue/src/main/proto/queue.proto +++ b/common/queue/src/main/proto/queue.proto @@ -73,6 +73,11 @@ enum KeyValueType { JSON_V = 4; } +enum CredentialsType { + ACCESS_TOKEN = 0; + X509_CERTIFICATE = 1; +} + message KeyValueProto { string key = 1; KeyValueType type = 2; @@ -241,6 +246,36 @@ message ClaimDeviceMsg { int64 durationMs = 4; } +message DeviceCredentialsProto { + int64 deviceIdMSB = 1; + int64 deviceIdLSB = 2; + CredentialsType credentialsType = 3; + string credentialsId = 4; + string credentialsValue = 5; +} + +message ProvisionDeviceRequestMsg { + string deviceName = 1; + string x509CertPubKey = 2; + ProvisionDeviceCredentialsMsg provisionDeviceCredentialsMsg = 3; +} + +message ProvisionDeviceCredentialsMsg { + string provisionDeviceKey = 1; + string provisionDeviceSecret = 2; +} + +message ProvisionDeviceResponseMsg { + DeviceCredentialsProto deviceCredentials = 1; + ProvisionResponseStatus provisionResponseStatus = 2; +} + +enum ProvisionResponseStatus { + UNKNOWN = 0; + SUCCESS = 1; + NOT_FOUND = 2; + FAILURE = 3; +} //Used to report session state to tb-Service and persist this state in the cache on the tb-Service level. message SubscriptionInfoProto { int64 lastActivityTime = 1; @@ -266,6 +301,7 @@ message TransportToDeviceActorMsg { ToDeviceRpcResponseMsg toDeviceRPCCallResponse = 6; SubscriptionInfoProto subscriptionInfo = 7; ClaimDeviceMsg claimDevice = 8; + ProvisionDeviceRequestMsg provisionDevice = 9; } message TransportToRuleEngineMsg { @@ -441,6 +477,7 @@ message TransportApiRequestMsg { GetTenantRoutingInfoRequestMsg getTenantRoutingInfoRequestMsg = 4; GetDeviceProfileRequestMsg getDeviceProfileRequestMsg = 5; ValidateBasicMqttCredRequestMsg validateBasicMqttCredRequestMsg = 6; + ProvisionDeviceRequestMsg provisionDeviceRequestMsg = 7; } /* Response from ThingsBoard Core Service to Transport Service */ @@ -449,6 +486,7 @@ message TransportApiResponseMsg { GetOrCreateDeviceFromGatewayResponseMsg getOrCreateDeviceResponseMsg = 2; GetTenantRoutingInfoResponseMsg getTenantRoutingInfoResponseMsg = 4; GetDeviceProfileResponseMsg getDeviceProfileResponseMsg = 5; + ProvisionDeviceResponseMsg provisionDeviceResponseMsg = 6; } /* Messages that are handled by ThingsBoard Core Service */ @@ -491,4 +529,5 @@ message ToTransportMsg { ToServerRpcResponseMsg toServerResponse = 7; DeviceProfileUpdateMsg deviceProfileUpdateMsg = 8; DeviceProfileDeleteMsg deviceProfileDeleteMsg = 9; + ProvisionDeviceResponseMsg provisionResponse = 10; } diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java index b846bac38c..63af515c00 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java @@ -24,6 +24,7 @@ import org.eclipse.californium.core.network.ExchangeObserver; import org.eclipse.californium.core.server.resources.CoapExchange; import org.eclipse.californium.core.server.resources.Resource; import org.springframework.util.ReflectionUtils; +import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.security.DeviceTokenCredentials; import org.thingsboard.server.common.msg.session.FeatureType; @@ -33,9 +34,11 @@ import org.thingsboard.server.common.transport.TransportContext; import org.thingsboard.server.common.transport.TransportService; import org.thingsboard.server.common.transport.TransportServiceCallback; import org.thingsboard.server.common.transport.adaptor.AdaptorException; +import org.thingsboard.server.common.transport.adaptor.JsonConverter; import org.thingsboard.server.common.transport.auth.SessionInfoCreator; import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse; import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceResponseMsg; import java.lang.reflect.Field; import java.util.List; @@ -130,10 +133,25 @@ public class CoapTransportResource extends CoapResource { case CLAIM: processRequest(exchange, SessionMsgType.CLAIM_REQUEST); break; + case PROVISION: + processProvision(exchange); + break; } } } + private void processProvision(CoapExchange exchange) { + log.trace("Processing {}", exchange.advanced().getRequest()); + exchange.accept(); + try { + transportService.process(transportContext.getAdaptor().convertToProvisionRequestMsg(UUID.randomUUID(), exchange.advanced().getRequest()), + new DeviceProvisionCallback(exchange)); + } catch (AdaptorException e) { + log.trace("Failed to decode message: ", e); + exchange.respond(ResponseCode.BAD_REQUEST); + } + } + private void processRequest(CoapExchange exchange, SessionMsgType type) { log.trace("Processing {}", exchange.advanced().getRequest()); exchange.accept(); @@ -274,6 +292,8 @@ public class CoapTransportResource extends CoapResource { try { if (uriPath.size() >= FEATURE_TYPE_POSITION) { return Optional.of(FeatureType.valueOf(uriPath.get(FEATURE_TYPE_POSITION - 1).toUpperCase())); + } else if (uriPath.size() == 3 && uriPath.contains(DataConstants.PROVISION)) { + return Optional.of(FeatureType.valueOf(DataConstants.PROVISION.toUpperCase())); } } catch (RuntimeException e) { log.warn("Failed to decode feature type: {}", uriPath); @@ -325,6 +345,25 @@ public class CoapTransportResource extends CoapResource { } } + private static class DeviceProvisionCallback implements TransportServiceCallback { + private final CoapExchange exchange; + + DeviceProvisionCallback(CoapExchange exchange) { + this.exchange = exchange; + } + + @Override + public void onSuccess(TransportProtos.ProvisionDeviceResponseMsg msg) { + exchange.respond(JsonConverter.toJson(msg).toString()); + } + + @Override + public void onError(Throwable e) { + log.warn("Failed to process request", e); + exchange.respond(ResponseCode.INTERNAL_SERVER_ERROR); + } + } + private static class CoapOkCallback implements TransportServiceCallback { private final CoapExchange exchange; diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/CoapTransportAdaptor.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/CoapTransportAdaptor.java index 82c0b80547..fd010a7141 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/CoapTransportAdaptor.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/CoapTransportAdaptor.java @@ -19,6 +19,7 @@ import org.eclipse.californium.core.coap.Request; import org.eclipse.californium.core.coap.Response; import org.thingsboard.server.common.transport.adaptor.AdaptorException; import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceRequestMsg; import org.thingsboard.server.transport.coap.CoapTransportResource; import java.util.UUID; @@ -45,4 +46,6 @@ public interface CoapTransportAdaptor { Response convertToPublish(CoapTransportResource.CoapSessionListener coapSessionListener, TransportProtos.ToServerRpcResponseMsg msg) throws AdaptorException; + ProvisionDeviceRequestMsg convertToProvisionRequestMsg(UUID sessionId, Request inbound) throws AdaptorException; + } diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/JsonCoapAdaptor.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/JsonCoapAdaptor.java index 5c9c471570..28292f6c20 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/JsonCoapAdaptor.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/JsonCoapAdaptor.java @@ -123,6 +123,16 @@ public class JsonCoapAdaptor implements CoapTransportAdaptor { return response; } + @Override + public TransportProtos.ProvisionDeviceRequestMsg convertToProvisionRequestMsg(UUID sessionId, Request inbound) throws AdaptorException { + String payload = validatePayload(sessionId, inbound, false); + try { + return JsonConverter.convertToProvisionRequestMsg(payload); + } catch (IllegalStateException | JsonSyntaxException ex) { + throw new AdaptorException(ex); + } + } + @Override public Response convertToPublish(CoapTransportResource.CoapSessionListener session, TransportProtos.GetAttributeResponseMsg msg) throws AdaptorException { if (msg.getClientAttributeListCount() == 0 && msg.getSharedAttributeListCount() == 0) { diff --git a/common/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java b/common/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java index 404024b202..cdc2791839 100644 --- a/common/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java +++ b/common/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java @@ -44,6 +44,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.AttributeUpdateNotif import org.thingsboard.server.gen.transport.TransportProtos.DeviceInfoProto; import org.thingsboard.server.gen.transport.TransportProtos.GetAttributeRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.GetAttributeResponseMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceResponseMsg; import org.thingsboard.server.gen.transport.TransportProtos.SessionCloseNotificationProto; import org.thingsboard.server.gen.transport.TransportProtos.SessionInfoProto; import org.thingsboard.server.gen.transport.TransportProtos.SubscribeToAttributeUpdatesMsg; @@ -203,6 +204,14 @@ public class DeviceApiController { return responseWriter; } + @RequestMapping(value = "/provision", method = RequestMethod.POST) + public DeferredResult provisionDevice(@RequestBody String json, HttpServletRequest httpRequest) { + DeferredResult responseWriter = new DeferredResult<>(); + transportContext.getTransportService().process(JsonConverter.convertToProvisionRequestMsg(json), + new DeviceProvisionCallback(responseWriter)); + return responseWriter; + } + private static class DeviceAuthCallback implements TransportServiceCallback { private final TransportContext transportContext; private final DeferredResult responseWriter; @@ -230,6 +239,25 @@ public class DeviceApiController { } } + private static class DeviceProvisionCallback implements TransportServiceCallback { + private final DeferredResult responseWriter; + + DeviceProvisionCallback(DeferredResult responseWriter) { + this.responseWriter = responseWriter; + } + + @Override + public void onSuccess(ProvisionDeviceResponseMsg msg) { + responseWriter.setResult(new ResponseEntity<>(JsonConverter.toJson(msg).toString(), HttpStatus.OK)); + } + + @Override + public void onError(Throwable e) { + log.warn("Failed to process request", e); + responseWriter.setResult(new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR)); + } + } + private static class SessionCloseOnErrorCallback implements TransportServiceCallback { private final TransportService transportService; private final SessionInfoProto sessionInfo; diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java index 6a09bdda40..cabae348a2 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java @@ -16,6 +16,7 @@ package org.thingsboard.server.transport.mqtt; import com.fasterxml.jackson.databind.JsonNode; +import com.google.gson.JsonSyntaxException; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.handler.codec.mqtt.MqttConnAckMessage; @@ -38,8 +39,10 @@ import io.netty.util.ReferenceCountUtil; import io.netty.util.concurrent.Future; import io.netty.util.concurrent.GenericFutureListener; import lombok.extern.slf4j.Slf4j; +import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceTransportType; +import org.thingsboard.server.common.data.TransportPayloadType; import org.thingsboard.server.common.data.device.profile.MqttTopics; import org.thingsboard.server.common.msg.EncryptionUtil; import org.thingsboard.server.common.transport.SessionMsgListener; @@ -51,6 +54,7 @@ import org.thingsboard.server.common.transport.auth.TransportDeviceInfo; import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse; import org.thingsboard.server.common.transport.service.DefaultTransportService; import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceResponseMsg; import org.thingsboard.server.gen.transport.TransportProtos.SessionEvent; import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceX509CertRequestMsg; import org.thingsboard.server.transport.mqtt.adaptors.MqttTransportAdaptor; @@ -65,6 +69,7 @@ import java.io.IOException; import java.net.InetSocketAddress; import java.util.ArrayList; import java.util.List; +import java.util.Optional; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; @@ -73,6 +78,7 @@ import java.util.Date; import static io.netty.handler.codec.mqtt.MqttConnectReturnCode.CONNECTION_ACCEPTED; import static io.netty.handler.codec.mqtt.MqttConnectReturnCode.CONNECTION_REFUSED_NOT_AUTHORIZED; import static io.netty.handler.codec.mqtt.MqttMessageType.CONNACK; +import static io.netty.handler.codec.mqtt.MqttMessageType.CONNECT; import static io.netty.handler.codec.mqtt.MqttMessageType.PINGRESP; import static io.netty.handler.codec.mqtt.MqttMessageType.PUBACK; import static io.netty.handler.codec.mqtt.MqttMessageType.SUBACK; @@ -130,10 +136,58 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement return; } deviceSessionCtx.setChannel(ctx); + if (CONNECT.equals(msg.fixedHeader().messageType())) { + processConnect(ctx, (MqttConnectMessage) msg); + } else if (deviceSessionCtx.isProvisionOnly()) { + processProvisionSessionMsg(ctx, msg); + } else { + processRegularSessionMsg(ctx, msg); + } + } + + private void processProvisionSessionMsg(ChannelHandlerContext ctx, MqttMessage msg) { switch (msg.fixedHeader().messageType()) { - case CONNECT: - processConnect(ctx, (MqttConnectMessage) msg); + case PUBLISH: + MqttPublishMessage mqttMsg = (MqttPublishMessage) msg; + String topicName = mqttMsg.variableHeader().topicName(); + int msgId = mqttMsg.variableHeader().packetId(); + try { + if (topicName.equals(MqttTopics.DEVICE_PROVISION_REQUEST_TOPIC)) { + try { + TransportProtos.ProvisionDeviceRequestMsg provisionRequestMsg = deviceSessionCtx.getContext().getJsonMqttAdaptor().convertToProvisionRequestMsg(deviceSessionCtx, mqttMsg); + transportService.process(provisionRequestMsg, new DeviceProvisionCallback(ctx, msgId, provisionRequestMsg)); + log.trace("[{}][{}] Processing provision publish msg [{}][{}]!", sessionId, deviceSessionCtx.getDeviceId(), topicName, msgId); + } catch (Exception e) { + if (e.getCause().toString().contains("JsonSyntaxException")) { + TransportProtos.ProvisionDeviceRequestMsg provisionRequestMsg = deviceSessionCtx.getContext().getProtoMqttAdaptor().convertToProvisionRequestMsg(deviceSessionCtx, mqttMsg); + transportService.process(provisionRequestMsg, new DeviceProvisionCallback(ctx, msgId, provisionRequestMsg)); + deviceSessionCtx.setProvisionPayloadType(TransportPayloadType.PROTOBUF); + log.trace("[{}][{}] Processing provision publish msg [{}][{}]!", sessionId, deviceSessionCtx.getDeviceId(), topicName, msgId); + } else { + throw e; + } + } + } else { + throw new RuntimeException("Unsupported topic for provisioning requests!"); + } + } catch (RuntimeException | AdaptorException e) { + log.warn("[{}] Failed to process publish msg [{}][{}]", sessionId, topicName, msgId, e); + ctx.close(); + } break; + case PINGREQ: + ctx.writeAndFlush(new MqttMessage(new MqttFixedHeader(PINGRESP, false, AT_MOST_ONCE, false, 0))); + break; + case DISCONNECT: + if (checkConnected(ctx, msg)) { + processDisconnect(ctx); + } + break; + } + } + + private void processRegularSessionMsg(ChannelHandlerContext ctx, MqttMessage msg) { + switch (msg.fixedHeader().messageType()) { case PUBLISH: processPublish(ctx, (MqttPublishMessage) msg); break; @@ -257,6 +311,41 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement }; } + private class DeviceProvisionCallback implements TransportServiceCallback { + private final ChannelHandlerContext ctx; + private final int msgId; + private final TransportProtos.ProvisionDeviceRequestMsg msg; + + DeviceProvisionCallback(ChannelHandlerContext ctx, int msgId, TransportProtos.ProvisionDeviceRequestMsg msg) { + this.ctx = ctx; + this.msgId = msgId; + this.msg = msg; + } + + @Override + public void onSuccess(TransportProtos.ProvisionDeviceResponseMsg provisionResponseMsg) { + log.trace("[{}] Published msg: {}", sessionId, msg); + if (msgId > 0) { + ctx.writeAndFlush(createMqttPubAckMsg(msgId)); + } + try { + if (deviceSessionCtx.getProvisionPayloadType().equals(TransportPayloadType.JSON)) { + deviceSessionCtx.getContext().getJsonMqttAdaptor().convertToPublish(deviceSessionCtx, provisionResponseMsg).ifPresent(deviceSessionCtx.getChannel()::writeAndFlush); + } else { + deviceSessionCtx.getContext().getProtoMqttAdaptor().convertToPublish(deviceSessionCtx, provisionResponseMsg).ifPresent(deviceSessionCtx.getChannel()::writeAndFlush); + } + } catch (Exception e) { + log.trace("[{}] Failed to convert device attributes response to MQTT msg", sessionId, e); + } + } + + @Override + public void onError(Throwable e) { + log.trace("[{}] Failed to publish msg: {}", sessionId, msg, e); + processDisconnect(ctx); + } + } + private void processSubscribe(ChannelHandlerContext ctx, MqttSubscribeMessage mqttMsg) { if (!checkConnected(ctx, mqttMsg)) { return; @@ -286,6 +375,8 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement case MqttTopics.GATEWAY_ATTRIBUTES_TOPIC: case MqttTopics.GATEWAY_RPC_TOPIC: case MqttTopics.GATEWAY_ATTRIBUTES_RESPONSE_TOPIC: + case MqttTopics.GATEWAY_PROVISION_RESPONSE_TOPIC: + case MqttTopics.DEVICE_PROVISION_RESPONSE_TOPIC: registerSubQoS(topic, grantedQoSList, reqQoS); break; default: @@ -351,11 +442,18 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement private void processConnect(ChannelHandlerContext ctx, MqttConnectMessage msg) { log.info("[{}] Processing connect msg for client: {}!", sessionId, msg.payload().clientIdentifier()); - X509Certificate cert; - if (sslHandler != null && (cert = getX509Certificate()) != null) { - processX509CertConnect(ctx, cert); + String userName = msg.payload().userName(); + if (DataConstants.PROVISION.equals(userName)) { + deviceSessionCtx.setProvisionOnly(true); + ctx.writeAndFlush(createMqttConnAckMsg(CONNECTION_ACCEPTED)); } else { - processAuthTokenConnect(ctx, msg); + X509Certificate cert; + + if (sslHandler != null && (cert = getX509Certificate()) != null) { + processX509CertConnect(ctx, cert); + } else { + processAuthTokenConnect(ctx, msg); + } } } diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java index 811e29cdae..54f8075c24 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java @@ -87,6 +87,16 @@ public class JsonMqttAdaptor implements MqttTransportAdaptor { } } + @Override + public TransportProtos.ProvisionDeviceRequestMsg convertToProvisionRequestMsg(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException { + String payload = validatePayload(ctx.getSessionId(), inbound.payload(), false); + try { + return JsonConverter.convertToProvisionRequestMsg(payload); + } catch (IllegalStateException | JsonSyntaxException ex) { + throw new AdaptorException(ex); + } + } + @Override public TransportProtos.GetAttributeRequestMsg convertToGetAttributes(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException { return processGetAttributeRequestMsg(inbound, MqttTopics.DEVICE_ATTRIBUTES_REQUEST_TOPIC_PREFIX); @@ -138,6 +148,18 @@ public class JsonMqttAdaptor implements MqttTransportAdaptor { return Optional.of(createMqttPublishMsg(ctx, MqttTopics.DEVICE_RPC_RESPONSE_TOPIC + rpcResponse.getRequestId(), JsonConverter.toJson(rpcResponse))); } + @Override + public Optional convertToPublish(MqttDeviceAwareSessionContext ctx, TransportProtos.ProvisionDeviceResponseMsg provisionResponse) { + return Optional.of(createMqttPublishMsg(ctx, MqttTopics.DEVICE_PROVISION_RESPONSE_TOPIC, JsonConverter.toJson(provisionResponse))); + } + + @Override + public Optional convertToGatewayPublish(MqttDeviceAwareSessionContext ctx, String deviceName, TransportProtos.ProvisionDeviceResponseMsg responseMsg) { + return Optional.of(createMqttPublishMsg(ctx, + MqttTopics.GATEWAY_PROVISION_RESPONSE_TOPIC, + JsonConverter.toGatewayJson(deviceName, responseMsg))); + } + public static JsonElement validateJsonPayload(UUID sessionId, ByteBuf payloadData) throws AdaptorException { String payload = validatePayload(sessionId, payloadData, false); try { diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/MqttTransportAdaptor.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/MqttTransportAdaptor.java index d4d36320f3..c0f1dd2369 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/MqttTransportAdaptor.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/MqttTransportAdaptor.java @@ -24,6 +24,8 @@ import org.thingsboard.server.gen.transport.TransportProtos.GetAttributeRequestM import org.thingsboard.server.gen.transport.TransportProtos.GetAttributeResponseMsg; import org.thingsboard.server.gen.transport.TransportProtos.PostAttributeMsg; import org.thingsboard.server.gen.transport.TransportProtos.PostTelemetryMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceRequestMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceResponseMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToDeviceRpcRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToDeviceRpcResponseMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToServerRpcRequestMsg; @@ -63,4 +65,10 @@ public interface MqttTransportAdaptor { Optional convertToPublish(MqttDeviceAwareSessionContext ctx, ToServerRpcResponseMsg rpcResponse) throws AdaptorException; + ProvisionDeviceRequestMsg convertToProvisionRequestMsg(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException; + + Optional convertToPublish(MqttDeviceAwareSessionContext ctx, ProvisionDeviceResponseMsg provisionResponse) throws AdaptorException; + + Optional convertToGatewayPublish(MqttDeviceAwareSessionContext ctx, String deviceName, ProvisionDeviceResponseMsg provisionResponse) throws AdaptorException; + } diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/ProtoMqttAdaptor.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/ProtoMqttAdaptor.java index f66daf2289..d5e9599812 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/ProtoMqttAdaptor.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/ProtoMqttAdaptor.java @@ -32,6 +32,7 @@ import org.thingsboard.server.common.transport.adaptor.AdaptorException; import org.thingsboard.server.common.transport.adaptor.ProtoConverter; import org.thingsboard.server.gen.transport.TransportApiProtos; import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceResponseMsg; import org.thingsboard.server.transport.mqtt.session.MqttDeviceAwareSessionContext; import java.util.Optional; @@ -108,6 +109,17 @@ public class ProtoMqttAdaptor implements MqttTransportAdaptor { } } + @Override + public TransportProtos.ProvisionDeviceRequestMsg convertToProvisionRequestMsg(MqttDeviceAwareSessionContext ctx, MqttPublishMessage mqttMsg) throws AdaptorException { + byte[] bytes = toBytes(mqttMsg.payload()); + String topicName = mqttMsg.variableHeader().topicName(); + try { + return ProtoConverter.convertToProvisionRequestMsg(bytes); + } catch (InvalidProtocolBufferException ex) { + throw new AdaptorException(ex); + } + } + @Override public Optional convertToPublish(MqttDeviceAwareSessionContext ctx, TransportProtos.GetAttributeResponseMsg responseMsg) throws AdaptorException { if (!StringUtils.isEmpty(responseMsg.getError())) { @@ -139,6 +151,11 @@ public class ProtoMqttAdaptor implements MqttTransportAdaptor { return Optional.of(createMqttPublishMsg(ctx, MqttTopics.DEVICE_ATTRIBUTES_TOPIC, notificationMsg.toByteArray())); } + @Override + public Optional convertToPublish(MqttDeviceAwareSessionContext ctx, TransportProtos.ProvisionDeviceResponseMsg provisionResponse) { + return Optional.of(createMqttPublishMsg(ctx, MqttTopics.DEVICE_PROVISION_RESPONSE_TOPIC, provisionResponse.toByteArray())); + } + @Override public Optional convertToGatewayPublish(MqttDeviceAwareSessionContext ctx, String deviceName, TransportProtos.GetAttributeResponseMsg responseMsg) throws AdaptorException { if (!StringUtils.isEmpty(responseMsg.getError())) { @@ -170,6 +187,15 @@ public class ProtoMqttAdaptor implements MqttTransportAdaptor { return Optional.of(createMqttPublishMsg(ctx, MqttTopics.GATEWAY_RPC_TOPIC, payloadBytes)); } + @Override + public Optional convertToGatewayPublish(MqttDeviceAwareSessionContext ctx, String deviceName, TransportProtos.ProvisionDeviceResponseMsg responseMsg) throws AdaptorException { + TransportApiProtos.GatewayProvisionResponseMsg.Builder builder = TransportApiProtos.GatewayProvisionResponseMsg.newBuilder(); + builder.setDeviceName(deviceName); + builder.setProvisionDeviceResponseMsg(responseMsg); + byte[] payloadBytes = builder.build().toByteArray(); + return Optional.of(createMqttPublishMsg(ctx, MqttTopics.GATEWAY_PROVISION_RESPONSE_TOPIC, payloadBytes)); + } + public static byte[] toBytes(ByteBuf inbound) { byte[] bytes = new byte[inbound.readableBytes()]; int readerIndex = inbound.readerIndex(); diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/DeviceSessionCtx.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/DeviceSessionCtx.java index ba701ba56c..20ddfb5a24 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/DeviceSessionCtx.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/DeviceSessionCtx.java @@ -17,6 +17,7 @@ package org.thingsboard.server.transport.mqtt.session; import io.netty.channel.ChannelHandlerContext; import lombok.Getter; +import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceTransportType; @@ -46,10 +47,18 @@ public class DeviceSessionCtx extends MqttDeviceAwareSessionContext { private final AtomicInteger msgIdSeq = new AtomicInteger(0); + @Getter + @Setter + private boolean provisionOnly = false; + private volatile MqttTopicFilter telemetryTopicFilter = MqttTopicFilterFactory.getDefaultTelemetryFilter(); private volatile MqttTopicFilter attributesTopicFilter = MqttTopicFilterFactory.getDefaultAttributesFilter(); private volatile TransportPayloadType payloadType = TransportPayloadType.JSON; + @Getter + @Setter + private TransportPayloadType provisionPayloadType = payloadType; + public DeviceSessionCtx(UUID sessionId, ConcurrentMap mqttQoSMap, MqttTransportContext context) { super(sessionId, mqttQoSMap); this.context = context; diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java index 3fc8ed96d1..995b9eca91 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java @@ -27,6 +27,8 @@ import org.thingsboard.server.gen.transport.TransportProtos.GetTenantRoutingInfo import org.thingsboard.server.gen.transport.TransportProtos.GetTenantRoutingInfoResponseMsg; import org.thingsboard.server.gen.transport.TransportProtos.PostAttributeMsg; import org.thingsboard.server.gen.transport.TransportProtos.PostTelemetryMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceRequestMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceResponseMsg; import org.thingsboard.server.gen.transport.TransportProtos.SessionEventMsg; import org.thingsboard.server.gen.transport.TransportProtos.SessionInfoProto; import org.thingsboard.server.gen.transport.TransportProtos.SubscribeToAttributeUpdatesMsg; @@ -57,6 +59,9 @@ public interface TransportService { void process(GetOrCreateDeviceFromGatewayRequestMsg msg, TransportServiceCallback callback); + void process(ProvisionDeviceRequestMsg msg, + TransportServiceCallback callback); + void getDeviceProfile(DeviceProfileId deviceProfileId, TransportServiceCallback callback); void onProfileUpdate(DeviceProfile deviceProfile); @@ -90,5 +95,4 @@ public interface TransportService { void reportActivity(SessionInfoProto sessionInfo); void deregisterSession(SessionInfoProto sessionInfo); - } diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/JsonConverter.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/JsonConverter.java index 428dfb0912..cd070e6927 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/JsonConverter.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/JsonConverter.java @@ -42,6 +42,8 @@ import org.thingsboard.server.gen.transport.TransportProtos.KeyValueProto; import org.thingsboard.server.gen.transport.TransportProtos.KeyValueType; import org.thingsboard.server.gen.transport.TransportProtos.PostAttributeMsg; import org.thingsboard.server.gen.transport.TransportProtos.PostTelemetryMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceResponseMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ProvisionResponseStatus; import org.thingsboard.server.gen.transport.TransportProtos.TsKvListProto; import org.thingsboard.server.gen.transport.TransportProtos.TsKvProto; @@ -53,6 +55,7 @@ import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.TreeMap; +import java.util.UUID; import java.util.function.Consumer; import java.util.stream.Collectors; @@ -397,6 +400,36 @@ public class JsonConverter { } } + public static JsonObject toJson(ProvisionDeviceResponseMsg payload) { + return toJson(payload, false, 0); + } + + public static JsonObject toJson(ProvisionDeviceResponseMsg payload, int requestId) { + return toJson(payload, true, requestId); + } + + private static JsonObject toJson(ProvisionDeviceResponseMsg payload, boolean toGateway, int requestId) { + JsonObject result = new JsonObject(); + if (payload.getProvisionResponseStatus() == TransportProtos.ProvisionResponseStatus.NOT_FOUND) { + result.addProperty("errorMsg", "Provision data was not found!"); + result.addProperty("provisionDeviceStatus", ProvisionResponseStatus.NOT_FOUND.name()); + } else if (payload.getProvisionResponseStatus() == TransportProtos.ProvisionResponseStatus.FAILURE) { + result.addProperty("errorMsg", "Failed to provision device!"); + result.addProperty("provisionDeviceStatus", ProvisionResponseStatus.FAILURE.name()); + } else { + if (toGateway) { + result.addProperty("id", requestId); + } + result.addProperty("deviceId", new UUID(payload.getDeviceCredentials().getDeviceIdMSB(), payload.getDeviceCredentials().getDeviceIdLSB()).toString()); + result.addProperty("credentialsType", payload.getDeviceCredentials().getCredentialsType().name()); + result.addProperty("credentialsId", payload.getDeviceCredentials().getCredentialsId()); + result.addProperty("credentialsValue", + StringUtils.isEmpty(payload.getDeviceCredentials().getCredentialsValue()) ? null : payload.getDeviceCredentials().getCredentialsValue()); + result.addProperty("provisionDeviceStatus", ProvisionResponseStatus.SUCCESS.name()); + } + return result; + } + public static JsonElement toErrorJson(String errorMsg) { JsonObject error = new JsonObject(); error.addProperty("error", errorMsg); @@ -410,6 +443,13 @@ public class JsonConverter { return result; } + public static JsonElement toGatewayJson(String deviceName, TransportProtos.ProvisionDeviceResponseMsg responseRequest) { + JsonObject result = new JsonObject(); + result.addProperty(DEVICE_PROPERTY, deviceName); + result.add("data", JsonConverter.toJson(responseRequest)); + return result; + } + public static Set convertToAttributes(JsonElement element) { Set result = new HashSet<>(); long ts = System.currentTimeMillis(); @@ -498,4 +538,43 @@ public class JsonConverter { maxStringValueLength = length; } + public static TransportProtos.ProvisionDeviceRequestMsg convertToProvisionRequestMsg(String json) { + JsonElement jsonElement = new JsonParser().parse(json); + if (jsonElement.isJsonObject()) { + return buildProvisionRequestMsg(jsonElement.getAsJsonObject()); + } else { + throw new JsonSyntaxException(CAN_T_PARSE_VALUE + jsonElement); + } + } + + public static TransportProtos.ProvisionDeviceRequestMsg convertToProvisionRequestMsg(JsonObject jo) { + return buildProvisionRequestMsg(jo); + } + + private static TransportProtos.ProvisionDeviceRequestMsg buildProvisionRequestMsg(JsonObject jo) { + return TransportProtos.ProvisionDeviceRequestMsg.newBuilder() + .setDeviceName(getStrValue(jo, DataConstants.DEVICE_NAME, true)) + .setX509CertPubKey(getStrValue(jo, DataConstants.CERT_PUB_KEY, false)) + .setProvisionDeviceCredentialsMsg(buildProvisionDeviceCredentialsMsg( + getStrValue(jo, DataConstants.PROVISION_KEY, true), + getStrValue(jo, DataConstants.PROVISION_SECRET, true))) + .build(); + } + + private static TransportProtos.ProvisionDeviceCredentialsMsg buildProvisionDeviceCredentialsMsg(String provisionKey, String provisionSecret) { + return TransportProtos.ProvisionDeviceCredentialsMsg.newBuilder() + .setProvisionDeviceKey(provisionKey) + .setProvisionDeviceSecret(provisionSecret) + .build(); + } + private static String getStrValue(JsonObject jo, String field, boolean requiredField) { + if (jo.has(field)) { + return jo.get(field).getAsString(); + } else { + if (requiredField) { + throw new RuntimeException("Failed to find the field " + field + " in JSON body " + jo + "!"); + } + return ""; + } + } } diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/ProtoConverter.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/ProtoConverter.java index b7d3d2d36c..26e7a9cf9e 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/ProtoConverter.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/ProtoConverter.java @@ -15,7 +15,10 @@ */ package org.thingsboard.server.common.transport.adaptor; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; import com.google.gson.JsonParser; +import com.google.gson.JsonSyntaxException; import com.google.protobuf.InvalidProtocolBufferException; import lombok.extern.slf4j.Slf4j; import org.springframework.util.CollectionUtils; @@ -130,6 +133,9 @@ public class ProtoConverter { } } + public static TransportProtos.ProvisionDeviceRequestMsg convertToProvisionRequestMsg(byte[] bytes) throws InvalidProtocolBufferException { + return TransportProtos.ProvisionDeviceRequestMsg.parseFrom(bytes); + } private static List validateKeyValueProtos(List kvList) { kvList.forEach(keyValueProto -> { diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java index 46c9bc2d01..58b06e696e 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java @@ -52,6 +52,8 @@ import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsRes import org.thingsboard.server.common.transport.util.DataDecodingEncodingService; import org.thingsboard.server.common.transport.util.JsonUtils; import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceRequestMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceResponseMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; @@ -332,6 +334,16 @@ public class DefaultTransportService implements TransportService { return tdi; } + @Override + public void process(ProvisionDeviceRequestMsg requestMsg, TransportServiceCallback callback) { + log.trace("Processing msg: {}", requestMsg); + TbProtoQueueMsg protoMsg = new TbProtoQueueMsg<>(UUID.randomUUID(), TransportApiRequestMsg.newBuilder().setProvisionDeviceRequestMsg(requestMsg).build()); + ListenableFuture response = Futures.transform(transportApiRequestTemplate.send(protoMsg), tmp -> + tmp.getValue().getProvisionDeviceResponseMsg() + , MoreExecutors.directExecutor()); + AsyncCallbackTemplate.withCallback(response, callback::onSuccess, callback::onError, transportCallbackExecutor); + } + @Override public void process(TransportProtos.SessionInfoProto sessionInfo, TransportProtos.SubscriptionInfoProto msg, TransportServiceCallback callback) { if (log.isTraceEnabled()) { diff --git a/common/transport/transport-api/src/main/proto/transport.proto b/common/transport/transport-api/src/main/proto/transport.proto index b536c22198..5eff9313fa 100644 --- a/common/transport/transport-api/src/main/proto/transport.proto +++ b/common/transport/transport-api/src/main/proto/transport.proto @@ -72,6 +72,11 @@ message GatewayAttributesMsg { repeated AttributesMsg msg = 1; } +message GatewayProvisionResponseMsg { + string deviceName = 1; + transport.ProvisionDeviceResponseMsg provisionDeviceResponseMsg = 2; +} + message GatewayRpcResponseMsg { string deviceName = 1; int32 id = 2; diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java index 467e957f74..6b3f1650a2 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java @@ -47,6 +47,7 @@ import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.dao.audit.sink.AuditLogSink; import org.thingsboard.server.dao.entity.EntityService; import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.device.provision.ProvisionRequest; import org.thingsboard.server.dao.service.DataValidator; import java.io.PrintWriter; @@ -257,6 +258,13 @@ public class AuditLogServiceImpl implements AuditLogService { actionData.put("os", os); actionData.put("device", device); break; + case PROVISION_SUCCESS: + case PROVISION_FAILURE: + ProvisionRequest request = extractParameter(ProvisionRequest.class, additionalInfo); + if (request != null) { + actionData.set("provisionRequest", objectMapper.valueToTree(request)); + } + break; } return actionData; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceDao.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceDao.java index bbc1735c9f..54ca245ee8 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceDao.java @@ -214,5 +214,4 @@ public interface DeviceDao extends Dao { * @return the list of device objects */ PageData findDevicesByTenantIdAndProfileId(UUID tenantId, UUID profileId, PageLink pageLink); - } diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileDao.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileDao.java index 267aff358e..9d11a34b2e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileDao.java @@ -38,5 +38,7 @@ public interface DeviceProfileDao extends Dao { DeviceProfileInfo findDefaultDeviceProfileInfo(TenantId tenantId); + DeviceProfile findByProvisionDeviceKey(String provisionDeviceKey); + DeviceProfile findByName(TenantId tenantId, String profileName); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java index b40ad102eb..d83408aead 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java @@ -26,6 +26,7 @@ import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceProfileInfo; +import org.thingsboard.server.common.data.DeviceProfileProvisionType; import org.thingsboard.server.common.data.DeviceProfileType; import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.EntitySubtype; @@ -33,6 +34,7 @@ import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileConfiguration; import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileTransportConfiguration; import org.thingsboard.server.common.data.device.profile.DeviceProfileData; +import org.thingsboard.server.common.data.device.profile.DisabledDeviceProfileProvisionConfiguration; import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; @@ -112,6 +114,8 @@ public class DeviceProfileServiceImpl extends AbstractEntityService implements D ConstraintViolationException e = extractConstraintViolationException(t).orElse(null); if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("device_profile_name_unq_key")) { throw new DataValidationException("Device profile with such name already exists!"); + } else if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("device_provision_key_unq_key")) { + throw new DataValidationException("Device profile with such provision device key already exists!"); } else { throw t; } @@ -210,12 +214,15 @@ public class DeviceProfileServiceImpl extends AbstractEntityService implements D deviceProfile.setName(profileName); deviceProfile.setType(DeviceProfileType.DEFAULT); deviceProfile.setTransportType(DeviceTransportType.DEFAULT); + deviceProfile.setProvisionType(DeviceProfileProvisionType.DISABLED); deviceProfile.setDescription("Default device profile"); DeviceProfileData deviceProfileData = new DeviceProfileData(); DefaultDeviceProfileConfiguration configuration = new DefaultDeviceProfileConfiguration(); DefaultDeviceProfileTransportConfiguration transportConfiguration = new DefaultDeviceProfileTransportConfiguration(); + DisabledDeviceProfileProvisionConfiguration provisionConfiguration = new DisabledDeviceProfileProvisionConfiguration(null); deviceProfileData.setConfiguration(configuration); deviceProfileData.setTransportConfiguration(transportConfiguration); + deviceProfileData.setProvisionConfiguration(provisionConfiguration); deviceProfile.setProfileData(deviceProfileData); return saveDeviceProfile(deviceProfile); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java index fb8a0194b6..e8f59fdd95 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java @@ -169,10 +169,12 @@ public class ModelConstants { public static final String DEVICE_PROFILE_NAME_PROPERTY = "name"; public static final String DEVICE_PROFILE_TYPE_PROPERTY = "type"; public static final String DEVICE_PROFILE_TRANSPORT_TYPE_PROPERTY = "transport_type"; + public static final String DEVICE_PROFILE_PROVISION_TYPE_PROPERTY = "provision_type"; public static final String DEVICE_PROFILE_PROFILE_DATA_PROPERTY = "profile_data"; public static final String DEVICE_PROFILE_DESCRIPTION_PROPERTY = "description"; public static final String DEVICE_PROFILE_IS_DEFAULT_PROPERTY = "is_default"; public static final String DEVICE_PROFILE_DEFAULT_RULE_CHAIN_ID_PROPERTY = "default_rule_chain_id"; + public static final String DEVICE_PROFILE_PROVISION_DEVICE_KEY = "provision_device_key"; /** * Cassandra entityView constants. diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceProfileEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceProfileEntity.java index 27e77d4eca..264d0725ae 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceProfileEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceProfileEntity.java @@ -23,6 +23,7 @@ import org.hibernate.annotations.Type; import org.hibernate.annotations.TypeDef; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceProfileType; +import org.thingsboard.server.common.data.DeviceProfileProvisionType; import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.device.profile.DeviceProfileData; import org.thingsboard.server.common.data.id.DeviceProfileId; @@ -62,6 +63,10 @@ public final class DeviceProfileEntity extends BaseSqlEntity impl @Column(name = ModelConstants.DEVICE_PROFILE_TRANSPORT_TYPE_PROPERTY) private DeviceTransportType transportType; + @Enumerated(EnumType.STRING) + @Column(name = ModelConstants.DEVICE_PROFILE_PROVISION_TYPE_PROPERTY) + private DeviceProfileProvisionType provisionType; + @Column(name = ModelConstants.DEVICE_PROFILE_DESCRIPTION_PROPERTY) private String description; @@ -78,6 +83,9 @@ public final class DeviceProfileEntity extends BaseSqlEntity impl @Column(name = ModelConstants.DEVICE_PROFILE_PROFILE_DATA_PROPERTY, columnDefinition = "jsonb") private JsonNode profileData; + @Column(name=ModelConstants.DEVICE_PROFILE_PROVISION_DEVICE_KEY) + private String provisionDeviceKey; + public DeviceProfileEntity() { super(); } @@ -93,12 +101,14 @@ public final class DeviceProfileEntity extends BaseSqlEntity impl this.name = deviceProfile.getName(); this.type = deviceProfile.getType(); this.transportType = deviceProfile.getTransportType(); + this.provisionType = deviceProfile.getProvisionType(); this.description = deviceProfile.getDescription(); this.isDefault = deviceProfile.isDefault(); this.profileData = JacksonUtil.convertValue(deviceProfile.getProfileData(), ObjectNode.class); if (deviceProfile.getDefaultRuleChainId() != null) { this.defaultRuleChainId = deviceProfile.getDefaultRuleChainId().getId(); } + this.provisionDeviceKey = deviceProfile.getProvisionDeviceKey(); } @Override @@ -125,12 +135,14 @@ public final class DeviceProfileEntity extends BaseSqlEntity impl deviceProfile.setName(name); deviceProfile.setType(type); deviceProfile.setTransportType(transportType); + deviceProfile.setProvisionType(provisionType); deviceProfile.setDescription(description); deviceProfile.setDefault(isDefault); deviceProfile.setProfileData(JacksonUtil.convertValue(profileData, DeviceProfileData.class)); if (defaultRuleChainId != null) { deviceProfile.setDefaultRuleChainId(new RuleChainId(defaultRuleChainId)); } + deviceProfile.setProvisionDeviceKey(provisionDeviceKey); return deviceProfile; } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceProfileRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceProfileRepository.java index 8116b711d5..0b0efdc8bd 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceProfileRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceProfileRepository.java @@ -20,7 +20,6 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.PagingAndSortingRepository; import org.springframework.data.repository.query.Param; -import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceProfileInfo; import org.thingsboard.server.dao.model.sql.DeviceProfileEntity; @@ -57,4 +56,5 @@ public interface DeviceProfileRepository extends PagingAndSortingRepository + +
+ {{'device-profile.device-provisioning' | translate }} + + +
+
@@ -107,7 +115,7 @@ + (click)="nextStep()">{{ (selectedIndex === 3 ? 'action.add' : 'action.continue') | translate }}
diff --git a/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.ts b/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.ts index 476c69488f..42edc3c613 100644 --- a/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.ts @@ -77,6 +77,8 @@ export class AddDeviceProfileDialogComponent extends alarmRulesFormGroup: FormGroup; + provisionConfigurationFormGroup: FormGroup; + constructor(protected store: Store, protected router: Router, @Inject(MAT_DIALOG_DATA) public data: AddDeviceProfileDialogData, @@ -111,6 +113,12 @@ export class AddDeviceProfileDialogComponent extends alarms: [null] } ); + + this.provisionConfigurationFormGroup = this.fb.group( + { + provisionConfiguration: [null] + } + ) } private deviceProfileTransportTypeChanged() { @@ -131,7 +139,7 @@ export class AddDeviceProfileDialogComponent extends } nextStep() { - if (this.selectedIndex < 2) { + if (this.selectedIndex < 3) { this.addDeviceProfileStepper.next(); } else { this.add(); @@ -146,6 +154,8 @@ export class AddDeviceProfileDialogComponent extends return this.transportConfigFormGroup; case 2: return this.alarmRulesFormGroup; + case 3: + return this.provisionConfigurationFormGroup; } } @@ -154,11 +164,17 @@ export class AddDeviceProfileDialogComponent extends name: this.deviceProfileDetailsFormGroup.get('name').value, type: this.deviceProfileDetailsFormGroup.get('type').value, transportType: this.transportConfigFormGroup.get('transportType').value, + provisionType: this.provisionConfigurationFormGroup.get('provisionConfiguration').value.type, + provisionDeviceKey: this.provisionConfigurationFormGroup.get('provisionConfiguration').value.provisionDeviceKey, description: this.deviceProfileDetailsFormGroup.get('description').value, profileData: { configuration: createDeviceProfileConfiguration(DeviceProfileType.DEFAULT), transportConfiguration: this.transportConfigFormGroup.get('transportConfiguration').value, - alarms: this.alarmRulesFormGroup.get('alarms').value + alarms: this.alarmRulesFormGroup.get('alarms').value, + provisionConfiguration: { + type: this.provisionConfigurationFormGroup.get('provisionConfiguration').value.type, + provisionDeviceSecret: this.provisionConfigurationFormGroup.get('provisionConfiguration').value.provisionDeviceSecret + } } }; if (this.deviceProfileDetailsFormGroup.get('defaultRuleChainId').value) { diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile-data.component.html b/ui-ngx/src/app/modules/home/components/profile/device-profile-data.component.html index daae838285..eb4aef68eb 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device-profile-data.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device-profile-data.component.html @@ -51,5 +51,15 @@ formControlName="alarms"> + + + +
device-profile.device-provisioning
+
+
+ + +
diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile-data.component.ts b/ui-ngx/src/app/modules/home/components/profile/device-profile-data.component.ts index 7d7fc55057..5b7fd9b998 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device-profile-data.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device-profile-data.component.ts @@ -72,7 +72,8 @@ export class DeviceProfileDataComponent implements ControlValueAccessor, OnInit this.deviceProfileDataFormGroup = this.fb.group({ configuration: [null, Validators.required], transportConfiguration: [null, Validators.required], - alarms: [null] + alarms: [null], + provisionConfiguration: [null] }); this.deviceProfileDataFormGroup.valueChanges.subscribe(() => { this.updateModel(); @@ -98,6 +99,7 @@ export class DeviceProfileDataComponent implements ControlValueAccessor, OnInit this.deviceProfileDataFormGroup.patchValue({configuration: value?.configuration}, {emitEvent: false}); this.deviceProfileDataFormGroup.patchValue({transportConfiguration: value?.transportConfiguration}, {emitEvent: false}); this.deviceProfileDataFormGroup.patchValue({alarms: value?.alarms}, {emitEvent: false}); + this.deviceProfileDataFormGroup.patchValue({provisionConfiguration: value?.provisionConfiguration}, {emitEvent: false}); } private updateModel() { diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.html new file mode 100644 index 0000000000..2e9299f0cf --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.html @@ -0,0 +1,46 @@ + +
+ + device-profile.provision-strategy + + + {{deviceProvisionTypeTranslateMap.get(type) | translate}} + + + + {{ 'device-profile.provision-strategy-required' | translate }} + + +
+ + device-profile.provision-device-key + + + {{ 'device-profile.provision-device-key-required' | translate }} + + + + device-profile.provision-device-secret + + + {{ 'device-profile.provision-device-secret-required' | translate }} + + +
+
diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.ts new file mode 100644 index 0000000000..f37d8bb2c6 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.ts @@ -0,0 +1,140 @@ +/// +/// Copyright © 2016-2020 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 { Component, forwardRef, Input, OnInit } from "@angular/core"; +import { + ControlValueAccessor, + FormBuilder, + FormControl, + FormGroup, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + ValidationErrors, + Validator, + Validators +} from "@angular/forms"; +import { coerceBooleanProperty } from "@angular/cdk/coercion"; +import { + DeviceProvisionConfiguration, + DeviceProvisionType, + deviceProvisionTypeTranslationMap +} from "@shared/models/device.models"; +import { isDefinedAndNotNull } from "@core/utils"; + +@Component({ + selector: 'tb-device-profile-provision-configuration', + templateUrl: './device-profile-provision-configuration.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DeviceProfileProvisionConfigurationComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => DeviceProfileProvisionConfigurationComponent), + multi: true, + } + ] +}) +export class DeviceProfileProvisionConfigurationComponent implements ControlValueAccessor, OnInit, Validator { + + provisionConfigurationFormGroup: FormGroup; + + deviceProvisionType = DeviceProvisionType; + deviceProvisionTypes = Object.keys(DeviceProvisionType); + deviceProvisionTypeTranslateMap = deviceProvisionTypeTranslationMap; + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + @Input() + disabled: boolean; + + private propagateChange = (v: any) => { }; + + constructor(private fb: FormBuilder) { + } + + ngOnInit(): void { + this.provisionConfigurationFormGroup = this.fb.group({ + type: [DeviceProvisionType.DISABLED, Validators.required], + provisionDeviceSecret: [{value: null, disabled: true}, Validators.required], + provisionDeviceKey: [{value: null, disabled: true}, Validators.required] + }); + this.provisionConfigurationFormGroup.get('type').valueChanges.subscribe((type) => { + if(type === DeviceProvisionType.DISABLED) { + this.provisionConfigurationFormGroup.get('provisionDeviceSecret').disable({emitEvent: false}); + this.provisionConfigurationFormGroup.get('provisionDeviceSecret').patchValue(null,{emitEvent: false}); + this.provisionConfigurationFormGroup.get('provisionDeviceKey').disable({emitEvent: false}); + this.provisionConfigurationFormGroup.get('provisionDeviceKey').patchValue(null); + } else { + this.provisionConfigurationFormGroup.get('provisionDeviceSecret').enable({emitEvent: false}); + this.provisionConfigurationFormGroup.get('provisionDeviceKey').enable({emitEvent: false}); + } + }); + this.provisionConfigurationFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + writeValue(value: DeviceProvisionConfiguration | null): void { + if(isDefinedAndNotNull(value)){ + this.provisionConfigurationFormGroup.patchValue(value, {emitEvent: false}); + } else { + this.provisionConfigurationFormGroup.patchValue({type: DeviceProvisionType.DISABLED}); + } + } + + setDisabledState(isDisabled: boolean){ + this.disabled = isDisabled; + if(this.disabled){ + this.provisionConfigurationFormGroup.disable(); + } else { + this.provisionConfigurationFormGroup.enable({emitEvent: false}); + } + } + + validate(c: FormControl): ValidationErrors | null { + return (this.provisionConfigurationFormGroup.valid) ? null : { + provisionConfiguration: { + valid: false, + }, + }; + } + + private updateModel(): void { + let deviceProvisionConfiguration: DeviceProvisionConfiguration = null; + if (this.provisionConfigurationFormGroup.valid) { + deviceProvisionConfiguration = this.provisionConfigurationFormGroup.getRawValue(); + } + this.propagateChange(deviceProvisionConfiguration); + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile.component.ts b/ui-ngx/src/app/modules/home/components/profile/device-profile.component.ts index e3c441c8a2..9c5fc9b5a5 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device-profile.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device-profile.component.ts @@ -126,6 +126,10 @@ export class DeviceProfileComponent extends EntityComponent { } updateForm(entity: DeviceProfile) { + if(entity?.profileData?.provisionConfiguration) { + entity.profileData.provisionConfiguration.provisionDeviceKey = entity?.provisionDeviceKey; + } + this.entityForm.patchValue({name: entity.name}); this.entityForm.patchValue({type: entity.type}, {emitEvent: false}); this.entityForm.patchValue({transportType: entity.transportType}, {emitEvent: false}); @@ -138,7 +142,11 @@ export class DeviceProfileComponent extends EntityComponent { if (formValue.defaultRuleChainId) { formValue.defaultRuleChainId = new RuleChainId(formValue.defaultRuleChainId); } - return formValue; + formValue.provisionType = formValue.profileData.provisionConfiguration.type; + formValue.provisionDeviceKey = formValue.profileData.provisionConfiguration.provisionDeviceKey; + delete formValue.profileData.provisionConfiguration.provisionDeviceKey; + + return super.prepareFormValue(formValue); } onDeviceProfileIdCopied(event) { diff --git a/ui-ngx/src/app/shared/models/device.models.ts b/ui-ngx/src/app/shared/models/device.models.ts index addb726ed5..36d3664856 100644 --- a/ui-ngx/src/app/shared/models/device.models.ts +++ b/ui-ngx/src/app/shared/models/device.models.ts @@ -41,6 +41,12 @@ export enum MqttTransportPayloadType { PROTOBUF = 'PROTOBUF' } +export enum DeviceProvisionType { + DISABLED = 'DISABLED', + ALLOW_CREATE_NEW_DEVICES = 'ALLOW_CREATE_NEW_DEVICES', + CHECK_PRE_PROVISIONED_DEVICES = 'CHECK_PRE_PROVISIONED_DEVICES' +} + export interface DeviceConfigurationFormInfo { hasProfileConfiguration: boolean; hasDeviceConfiguration: boolean; @@ -72,6 +78,15 @@ export const deviceTransportTypeTranslationMap = new Map( + [ + [DeviceProvisionType.DISABLED, 'device-profile.provision-strategy-disabled'], + [DeviceProvisionType.ALLOW_CREATE_NEW_DEVICES, 'device-profile.provision-strategy-created-new'], + [DeviceProvisionType.CHECK_PRE_PROVISIONED_DEVICES, 'device-profile.provision-strategy-check-pre-provisioned'] + ] +) + export const mqttTransportPayloadTypeTranslationMap = new Map( [ [MqttTransportPayloadType.JSON, 'device-profile.mqtt-device-payload-type-json'], @@ -138,6 +153,12 @@ export interface DeviceProfileTransportConfiguration extends DeviceProfileTransp type: DeviceTransportType; } +export interface DeviceProvisionConfiguration { + type: DeviceProvisionType; + provisionDeviceSecret?: string; + provisionDeviceKey?: string; +} + export function createDeviceProfileConfiguration(type: DeviceProfileType): DeviceProfileConfiguration { let configuration: DeviceProfileConfiguration = null; if (type) { @@ -285,6 +306,7 @@ export interface DeviceProfileData { configuration: DeviceProfileConfiguration; transportConfiguration: DeviceProfileTransportConfiguration; alarms?: Array; + provisionConfiguration?: DeviceProvisionConfiguration; } export interface DeviceProfile extends BaseData { @@ -294,6 +316,8 @@ export interface DeviceProfile extends BaseData { default?: boolean; type: DeviceProfileType; transportType: DeviceTransportType; + provisionType: DeviceProvisionType; + provisionDeviceKey?: string; defaultRuleChainId?: RuleChainId; profileData: DeviceProfileData; } diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 0fd0e1f7a2..c78d2b83cb 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -846,6 +846,16 @@ "alarm-rule-condition": "Alarm rule condition", "enter-alarm-rule-condition-prompt": "Please add alarm rule condition", "edit-alarm-rule-condition": "Edit alarm rule condition", + "device-provisioning": "Device provisioning", + "provision-strategy": "Provision strategy", + "provision-strategy-required": "Provision strategy is required.", + "provision-strategy-disabled": "Disabled", + "provision-strategy-created-new": "Allow create new devices", + "provision-strategy-check-pre-provisioned": "Check pre provisioned devices", + "provision-device-key": "Provision device key", + "provision-device-key-required": "Provision device key is required.", + "provision-device-secret": "Provision device secret", + "provision-device-secret-required": "Provision device secret is required." "condition": "Condition", "condition-type": "Condition type", "condition-type-simple": "Simple",