Commit 2b1317b10fe432fc0ff666ffb38ed9a9d1393852

Authored by Vladyslav Prykhodko
2 parents 1b1dac30 1bb1f5be

Merge branch 'feature/device-provision-3.2-onlyProfileVersion' of https://github…

….com/zbeacon/thingsboard into zbeacon-feature/device-provision-3.2-onlyProfileVersion
Showing 45 changed files with 1020 additions and 19 deletions
... ... @@ -20,13 +20,16 @@ CREATE TABLE IF NOT EXISTS device_profile (
20 20 name varchar(255),
21 21 type varchar(255),
22 22 transport_type varchar(255),
  23 + provision_type varchar(255),
23 24 profile_data jsonb,
24 25 description varchar,
25 26 search_text varchar(255),
26 27 is_default boolean,
27 28 tenant_id uuid,
28 29 default_rule_chain_id uuid,
  30 + provision_device_key varchar,
29 31 CONSTRAINT device_profile_name_unq_key UNIQUE (tenant_id, name),
  32 + CONSTRAINT device_provision_key_unq_key UNIQUE (provision_device_key),
30 33 CONSTRAINT fk_default_rule_chain_device_profile FOREIGN KEY (default_rule_chain_id) REFERENCES rule_chain(id)
31 34 );
32 35
... ...
... ... @@ -706,6 +706,12 @@ public abstract class BaseController {
706 706 case ASSIGNED_TO_TENANT:
707 707 msgType = DataConstants.ENTITY_ASSIGNED_TO_TENANT;
708 708 break;
  709 + case PROVISION_SUCCESS:
  710 + msgType = DataConstants.PROVISION_SUCCESS;
  711 + break;
  712 + case PROVISION_FAILURE:
  713 + msgType = DataConstants.PROVISION_FAILURE;
  714 + break;
709 715 }
710 716 if (!StringUtils.isEmpty(msgType)) {
711 717 try {
... ...
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.service.device;
  17 +
  18 +import com.fasterxml.jackson.core.JsonProcessingException;
  19 +import com.fasterxml.jackson.databind.node.ObjectNode;
  20 +import com.google.common.util.concurrent.Futures;
  21 +import com.google.common.util.concurrent.ListenableFuture;
  22 +import com.google.common.util.concurrent.MoreExecutors;
  23 +import lombok.extern.slf4j.Slf4j;
  24 +import org.springframework.beans.factory.annotation.Autowired;
  25 +import org.springframework.stereotype.Service;
  26 +import org.springframework.util.StringUtils;
  27 +import org.thingsboard.server.common.data.DataConstants;
  28 +import org.thingsboard.server.common.data.Device;
  29 +import org.thingsboard.server.common.data.DeviceProfile;
  30 +import org.thingsboard.server.common.data.audit.ActionType;
  31 +import org.thingsboard.server.common.data.device.profile.AllowCreateNewDevicesDeviceProfileProvisionConfiguration;
  32 +import org.thingsboard.server.common.data.device.profile.CheckPreProvisionedDevicesDeviceProfileProvisionConfiguration;
  33 +import org.thingsboard.server.common.data.id.CustomerId;
  34 +import org.thingsboard.server.common.data.id.TenantId;
  35 +import org.thingsboard.server.common.data.id.UserId;
  36 +import org.thingsboard.server.common.data.kv.AttributeKvEntry;
  37 +import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
  38 +import org.thingsboard.server.common.data.kv.StringDataEntry;
  39 +import org.thingsboard.server.common.data.security.DeviceCredentials;
  40 +import org.thingsboard.server.common.data.security.DeviceCredentialsType;
  41 +import org.thingsboard.server.common.msg.TbMsg;
  42 +import org.thingsboard.server.common.msg.TbMsgMetaData;
  43 +import org.thingsboard.server.common.msg.queue.ServiceType;
  44 +import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
  45 +import org.thingsboard.server.dao.attributes.AttributesService;
  46 +import org.thingsboard.server.dao.audit.AuditLogService;
  47 +import org.thingsboard.server.dao.device.DeviceCredentialsService;
  48 +import org.thingsboard.server.dao.device.DeviceDao;
  49 +import org.thingsboard.server.dao.device.DeviceProfileDao;
  50 +import org.thingsboard.server.dao.device.DeviceProvisionService;
  51 +import org.thingsboard.server.dao.device.DeviceService;
  52 +import org.thingsboard.server.dao.device.provision.ProvisionRequest;
  53 +import org.thingsboard.server.dao.device.provision.ProvisionResponse;
  54 +import org.thingsboard.server.dao.device.provision.ProvisionResponseStatus;
  55 +import org.thingsboard.server.dao.util.mapping.JacksonUtil;
  56 +import org.thingsboard.server.gen.transport.TransportProtos;
  57 +import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg;
  58 +import org.thingsboard.server.queue.TbQueueCallback;
  59 +import org.thingsboard.server.queue.TbQueueProducer;
  60 +import org.thingsboard.server.queue.common.TbProtoQueueMsg;
  61 +import org.thingsboard.server.queue.discovery.PartitionService;
  62 +import org.thingsboard.server.queue.provider.TbQueueProducerProvider;
  63 +import org.thingsboard.server.service.state.DeviceStateService;
  64 +
  65 +import java.util.Collections;
  66 +import java.util.List;
  67 +import java.util.Optional;
  68 +import java.util.concurrent.locks.ReentrantLock;
  69 +
  70 +
  71 +@Service
  72 +@Slf4j
  73 +public class DeviceProvisionServiceImpl implements DeviceProvisionService {
  74 +
  75 + protected TbQueueProducer<TbProtoQueueMsg<ToRuleEngineMsg>> ruleEngineMsgProducer;
  76 +
  77 + private static final String DEVICE_PROVISION_STATE = "provisionState";
  78 + private static final String PROVISIONED_STATE = "provisioned";
  79 +
  80 + private final ReentrantLock deviceCreationLock = new ReentrantLock();
  81 +
  82 + @Autowired
  83 + DeviceDao deviceDao;
  84 +
  85 + @Autowired
  86 + DeviceProfileDao deviceProfileDao;
  87 +
  88 + @Autowired
  89 + DeviceService deviceService;
  90 +
  91 + @Autowired
  92 + DeviceCredentialsService deviceCredentialsService;
  93 +
  94 + @Autowired
  95 + AttributesService attributesService;
  96 +
  97 + @Autowired
  98 + DeviceStateService deviceStateService;
  99 +
  100 + @Autowired
  101 + AuditLogService auditLogService;
  102 +
  103 + @Autowired
  104 + PartitionService partitionService;
  105 +
  106 + public DeviceProvisionServiceImpl(TbQueueProducerProvider producerProvider) {
  107 + ruleEngineMsgProducer = producerProvider.getRuleEngineMsgProducer();
  108 + }
  109 +
  110 + @Override
  111 + public ListenableFuture<ProvisionResponse> provisionDevice(ProvisionRequest provisionRequest) {
  112 + String provisionRequestKey = provisionRequest.getCredentials().getProvisionDeviceKey();
  113 + String provisionRequestSecret = provisionRequest.getCredentials().getProvisionDeviceSecret();
  114 +
  115 + if (StringUtils.isEmpty(provisionRequestKey) || StringUtils.isEmpty(provisionRequestSecret)) {
  116 + return Futures.immediateFuture(new ProvisionResponse(null, ProvisionResponseStatus.NOT_FOUND));
  117 + }
  118 +
  119 + DeviceProfile targetProfile = deviceProfileDao.findByProvisionDeviceKey(provisionRequestKey);
  120 +
  121 + if (targetProfile == null) {
  122 + return Futures.immediateFuture(new ProvisionResponse(null, ProvisionResponseStatus.NOT_FOUND));
  123 + }
  124 +
  125 + Device targetDevice = deviceDao.findDeviceByTenantIdAndName(targetProfile.getTenantId().getId(), provisionRequest.getDeviceName()).orElse(null);
  126 +
  127 + switch(targetProfile.getProvisionType()) {
  128 + case ALLOW_CREATE_NEW_DEVICES:
  129 + if (((AllowCreateNewDevicesDeviceProfileProvisionConfiguration) targetProfile.getProfileData().getProvisionConfiguration()).getProvisionDeviceSecret().equals(provisionRequestSecret)){
  130 + if (targetDevice != null) {
  131 + log.warn("[{}] The device is present and could not be provisioned once more!", targetDevice.getName());
  132 + notify(targetDevice, provisionRequest, DataConstants.PROVISION_FAILURE, false);
  133 + return Futures.immediateFuture(new ProvisionResponse(null, ProvisionResponseStatus.FAILURE));
  134 + } else {
  135 + return createDevice(provisionRequest, targetProfile);
  136 + }
  137 + }
  138 + break;
  139 + case CHECK_PRE_PROVISIONED_DEVICES:
  140 + if (((CheckPreProvisionedDevicesDeviceProfileProvisionConfiguration) targetProfile.getProfileData().getProvisionConfiguration()).getProvisionDeviceSecret().equals(provisionRequestSecret)) {
  141 + if (targetDevice != null && targetDevice.getDeviceProfileId().equals(targetProfile.getId())) {
  142 + return processProvision(targetDevice, provisionRequest);
  143 + } else {
  144 + log.warn("[{}] Failed to find pre provisioned device!", provisionRequest.getDeviceName());
  145 + return Futures.immediateFuture(new ProvisionResponse(null, ProvisionResponseStatus.FAILURE));
  146 + }
  147 + }
  148 + break;
  149 + }
  150 + return Futures.immediateFuture(new ProvisionResponse(null, ProvisionResponseStatus.NOT_FOUND));
  151 + }
  152 +
  153 + private ListenableFuture<ProvisionResponse> processProvision(Device device, ProvisionRequest provisionRequest) {
  154 + ListenableFuture<Optional<AttributeKvEntry>> provisionStateFuture = attributesService.find(device.getTenantId(), device.getId(),
  155 + DataConstants.SERVER_SCOPE, DEVICE_PROVISION_STATE);
  156 + ListenableFuture<Boolean> provisionedFuture = Futures.transformAsync(provisionStateFuture, optionalAtr -> {
  157 + if (optionalAtr.isPresent()) {
  158 + String state = optionalAtr.get().getValueAsString();
  159 + if (state.equals(PROVISIONED_STATE)) {
  160 + return Futures.immediateFuture(true);
  161 + } else {
  162 + log.error("[{}][{}] Unknown provision state: {}!", device.getName(), DEVICE_PROVISION_STATE, state);
  163 + return Futures.immediateCancelledFuture();
  164 + }
  165 + }
  166 + return Futures.transform(saveProvisionStateAttribute(device), input -> false, MoreExecutors.directExecutor());
  167 + }, MoreExecutors.directExecutor());
  168 + if (provisionedFuture.isCancelled()) {
  169 + throw new RuntimeException("Unknown provision state!");
  170 + }
  171 + return Futures.transform(provisionedFuture, provisioned -> {
  172 + if (provisioned) {
  173 + notify(device, provisionRequest, DataConstants.PROVISION_FAILURE, false);
  174 + return new ProvisionResponse(null, ProvisionResponseStatus.FAILURE);
  175 + }
  176 + notify(device, provisionRequest, DataConstants.PROVISION_SUCCESS, true);
  177 + return new ProvisionResponse(deviceCredentialsService.findDeviceCredentialsByDeviceId(device.getTenantId(), device.getId()), ProvisionResponseStatus.SUCCESS);
  178 + }, MoreExecutors.directExecutor());
  179 + }
  180 +
  181 + private ListenableFuture<ProvisionResponse> createDevice(ProvisionRequest provisionRequest, DeviceProfile profile) {
  182 + deviceCreationLock.lock();
  183 + try {
  184 + return processCreateDevice(provisionRequest, profile);
  185 + } finally {
  186 + deviceCreationLock.unlock();
  187 + }
  188 + }
  189 +
  190 + private void notify(Device device, ProvisionRequest provisionRequest, String type, boolean success) {
  191 + pushProvisionEventToRuleEngine(provisionRequest, device, type);
  192 + logAction(device.getTenantId(), device.getCustomerId(), device, success, provisionRequest);
  193 + }
  194 +
  195 + private ListenableFuture<ProvisionResponse> processCreateDevice(ProvisionRequest provisionRequest, DeviceProfile profile) {
  196 + Device device = deviceService.findDeviceByTenantIdAndName(profile.getTenantId(), provisionRequest.getDeviceName());
  197 + if (device == null) {
  198 + Device savedDevice = saveDevice(provisionRequest, profile);
  199 +
  200 + deviceStateService.onDeviceAdded(savedDevice);
  201 + pushDeviceCreatedEventToRuleEngine(savedDevice);
  202 + notify(savedDevice, provisionRequest, DataConstants.PROVISION_SUCCESS, true);
  203 +
  204 + return Futures.transform(saveProvisionStateAttribute(savedDevice), input ->
  205 + new ProvisionResponse(
  206 + getDeviceCredentials(savedDevice, provisionRequest.getX509CertPubKey()),
  207 + ProvisionResponseStatus.SUCCESS), MoreExecutors.directExecutor());
  208 + }
  209 + log.warn("[{}] The device is already provisioned!", device.getName());
  210 + notify(device, provisionRequest, DataConstants.PROVISION_FAILURE, false);
  211 + return Futures.immediateFuture(new ProvisionResponse(null, ProvisionResponseStatus.FAILURE));
  212 + }
  213 +
  214 + private ListenableFuture<List<Void>> saveProvisionStateAttribute(Device device) {
  215 + return attributesService.save(device.getTenantId(), device.getId(), DataConstants.SERVER_SCOPE,
  216 + Collections.singletonList(new BaseAttributeKvEntry(new StringDataEntry(DEVICE_PROVISION_STATE, PROVISIONED_STATE),
  217 + System.currentTimeMillis())));
  218 + }
  219 +
  220 + private Device saveDevice(ProvisionRequest provisionRequest, DeviceProfile profile) {
  221 + Device device = new Device();
  222 + device.setName(provisionRequest.getDeviceName());
  223 + device.setType(profile.getName());
  224 + device.setTenantId(profile.getTenantId());
  225 + return deviceService.saveDevice(device);
  226 + }
  227 +
  228 + private DeviceCredentials getDeviceCredentials(Device device, String x509CertPubKey) {
  229 + DeviceCredentials credentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(device.getTenantId(), device.getId());
  230 + if (!StringUtils.isEmpty(x509CertPubKey)) {
  231 + credentials.setCredentialsType(DeviceCredentialsType.X509_CERTIFICATE);
  232 + credentials.setCredentialsValue(x509CertPubKey);
  233 + return deviceCredentialsService.updateDeviceCredentials(device.getTenantId(), credentials);
  234 + }
  235 + return credentials;
  236 + }
  237 +
  238 + private void pushProvisionEventToRuleEngine(ProvisionRequest request, Device device, String type) {
  239 + try {
  240 + ObjectNode entityNode = JacksonUtil.OBJECT_MAPPER.valueToTree(request);
  241 + TbMsg msg = TbMsg.newMsg(type, device.getId(), createTbMsgMetaData(device), JacksonUtil.OBJECT_MAPPER.writeValueAsString(entityNode));
  242 + sendToRuleEngine(device.getTenantId(), msg, null);
  243 + } catch (JsonProcessingException | IllegalArgumentException e) {
  244 + log.warn("[{}] Failed to push device action to rule engine: {}", device.getId(), type, e);
  245 + }
  246 + }
  247 +
  248 + private void pushDeviceCreatedEventToRuleEngine(Device device) {
  249 + try {
  250 + ObjectNode entityNode = JacksonUtil.OBJECT_MAPPER.valueToTree(device);
  251 + TbMsg msg = TbMsg.newMsg(DataConstants.ENTITY_CREATED, device.getId(), createTbMsgMetaData(device), JacksonUtil.OBJECT_MAPPER.writeValueAsString(entityNode));
  252 + sendToRuleEngine(device.getTenantId(), msg, null);
  253 + } catch (JsonProcessingException | IllegalArgumentException e) {
  254 + log.warn("[{}] Failed to push device action to rule engine: {}", device.getId(), DataConstants.ENTITY_CREATED, e);
  255 + }
  256 + }
  257 +
  258 + protected void sendToRuleEngine(TenantId tenantId, TbMsg tbMsg, TbQueueCallback callback) {
  259 + TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_RULE_ENGINE, tenantId, tbMsg.getOriginator());
  260 + TransportProtos.ToRuleEngineMsg msg = TransportProtos.ToRuleEngineMsg.newBuilder().setTbMsg(TbMsg.toByteString(tbMsg))
  261 + .setTenantIdMSB(tenantId.getId().getMostSignificantBits())
  262 + .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()).build();
  263 + ruleEngineMsgProducer.send(tpi, new TbProtoQueueMsg<>(tbMsg.getId(), msg), callback);
  264 + }
  265 +
  266 + private TbMsgMetaData createTbMsgMetaData(Device device) {
  267 + TbMsgMetaData metaData = new TbMsgMetaData();
  268 + metaData.putValue("tenantId", device.getTenantId().toString());
  269 + return metaData;
  270 + }
  271 +
  272 + private void logAction(TenantId tenantId, CustomerId customerId, Device device, boolean success, ProvisionRequest provisionRequest) {
  273 + ActionType actionType = success ? ActionType.PROVISION_SUCCESS : ActionType.PROVISION_FAILURE;
  274 + auditLogService.logEntityAction(tenantId, customerId, new UserId(UserId.NULL_UUID), device.getName(), device.getId(), device, actionType, null, provisionRequest);
  275 + }
  276 +}
... ...
... ... @@ -23,7 +23,6 @@ import com.google.common.util.concurrent.ListenableFuture;
23 23 import com.google.common.util.concurrent.MoreExecutors;
24 24 import com.google.protobuf.ByteString;
25 25 import lombok.extern.slf4j.Slf4j;
26   -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
27 26 import org.springframework.stereotype.Service;
28 27 import org.springframework.util.StringUtils;
29 28 import org.thingsboard.server.common.data.DataConstants;
... ... @@ -31,6 +30,7 @@ import org.thingsboard.server.common.data.Device;
31 30 import org.thingsboard.server.common.data.DeviceProfile;
32 31 import org.thingsboard.server.common.data.TenantProfile;
33 32 import org.thingsboard.server.common.data.device.credentials.BasicMqttCredentials;
  33 +import org.thingsboard.server.common.data.device.profile.ProvisionDeviceProfileCredentials;
34 34 import org.thingsboard.server.common.data.id.CustomerId;
35 35 import org.thingsboard.server.common.data.id.DeviceId;
36 36 import org.thingsboard.server.common.data.id.DeviceProfileId;
... ... @@ -45,17 +45,23 @@ import org.thingsboard.server.common.msg.TbMsgMetaData;
45 45 import org.thingsboard.server.common.transport.util.DataDecodingEncodingService;
46 46 import org.thingsboard.server.dao.device.DeviceCredentialsService;
47 47 import org.thingsboard.server.dao.device.DeviceProfileService;
  48 +import org.thingsboard.server.dao.device.DeviceProvisionService;
48 49 import org.thingsboard.server.dao.device.DeviceService;
  50 +import org.thingsboard.server.dao.device.provision.ProvisionRequest;
  51 +import org.thingsboard.server.dao.device.provision.ProvisionResponse;
  52 +import org.thingsboard.server.dao.device.provision.ProvisionResponseStatus;
49 53 import org.thingsboard.server.dao.relation.RelationService;
50 54 import org.thingsboard.server.dao.tenant.TenantProfileService;
51 55 import org.thingsboard.server.dao.tenant.TenantService;
52 56 import org.thingsboard.server.dao.util.mapping.JacksonUtil;
53 57 import org.thingsboard.server.gen.transport.TransportProtos;
  58 +import org.thingsboard.server.gen.transport.TransportProtos.CredentialsType;
54 59 import org.thingsboard.server.gen.transport.TransportProtos.DeviceInfoProto;
55 60 import org.thingsboard.server.gen.transport.TransportProtos.GetOrCreateDeviceFromGatewayRequestMsg;
56 61 import org.thingsboard.server.gen.transport.TransportProtos.GetOrCreateDeviceFromGatewayResponseMsg;
57 62 import org.thingsboard.server.gen.transport.TransportProtos.GetTenantRoutingInfoRequestMsg;
58 63 import org.thingsboard.server.gen.transport.TransportProtos.GetTenantRoutingInfoResponseMsg;
  64 +import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceRequestMsg;
59 65 import org.thingsboard.server.gen.transport.TransportProtos.TransportApiRequestMsg;
60 66 import org.thingsboard.server.gen.transport.TransportProtos.TransportApiResponseMsg;
61 67 import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceCredentialsResponseMsg;
... ... @@ -94,6 +100,7 @@ public class DefaultTransportApiService implements TransportApiService {
94 100 private final DbCallbackExecutorService dbCallbackExecutorService;
95 101 private final TbClusterService tbClusterService;
96 102 private final DataDecodingEncodingService dataDecodingEncodingService;
  103 + private final DeviceProvisionService deviceProvisionService;
97 104
98 105
99 106 private final ConcurrentMap<String, ReentrantLock> deviceCreationLocks = new ConcurrentHashMap<>();
... ... @@ -102,7 +109,8 @@ public class DefaultTransportApiService implements TransportApiService {
102 109 TenantProfileService tenantProfileService, DeviceService deviceService,
103 110 RelationService relationService, DeviceCredentialsService deviceCredentialsService,
104 111 DeviceStateService deviceStateService, DbCallbackExecutorService dbCallbackExecutorService,
105   - TbClusterService tbClusterService, DataDecodingEncodingService dataDecodingEncodingService) {
  112 + TbClusterService tbClusterService, DataDecodingEncodingService dataDecodingEncodingService,
  113 + DeviceProvisionService deviceProvisionService) {
106 114 this.deviceProfileService = deviceProfileService;
107 115 this.tenantService = tenantService;
108 116 this.tenantProfileService = tenantProfileService;
... ... @@ -113,6 +121,7 @@ public class DefaultTransportApiService implements TransportApiService {
113 121 this.dbCallbackExecutorService = dbCallbackExecutorService;
114 122 this.tbClusterService = tbClusterService;
115 123 this.dataDecodingEncodingService = dataDecodingEncodingService;
  124 + this.deviceProvisionService = deviceProvisionService;
116 125 }
117 126
118 127 @Override
... ... @@ -139,6 +148,9 @@ public class DefaultTransportApiService implements TransportApiService {
139 148 } else if (transportApiRequestMsg.hasGetDeviceProfileRequestMsg()) {
140 149 return Futures.transform(handle(transportApiRequestMsg.getGetDeviceProfileRequestMsg()),
141 150 value -> new TbProtoQueueMsg<>(tbProtoQueueMsg.getKey(), value, tbProtoQueueMsg.getHeaders()), MoreExecutors.directExecutor());
  151 + } else if (transportApiRequestMsg.hasProvisionDeviceRequestMsg()) {
  152 + return Futures.transform(handle(transportApiRequestMsg.getProvisionDeviceRequestMsg()),
  153 + value -> new TbProtoQueueMsg<>(tbProtoQueueMsg.getKey(), value, tbProtoQueueMsg.getHeaders()), MoreExecutors.directExecutor());
142 154 }
143 155 return Futures.transform(getEmptyTransportApiResponseFuture(),
144 156 value -> new TbProtoQueueMsg<>(tbProtoQueueMsg.getKey(), value, tbProtoQueueMsg.getHeaders()), MoreExecutors.directExecutor());
... ... @@ -261,6 +273,46 @@ public class DefaultTransportApiService implements TransportApiService {
261 273 }, dbCallbackExecutorService);
262 274 }
263 275
  276 +
  277 + private ListenableFuture<TransportApiResponseMsg> handle(ProvisionDeviceRequestMsg requestMsg) {
  278 + ListenableFuture<ProvisionResponse> provisionResponseFuture = null;
  279 + provisionResponseFuture = deviceProvisionService.provisionDevice(
  280 + new ProvisionRequest(
  281 + requestMsg.getDeviceName(),
  282 + requestMsg.getX509CertPubKey(),
  283 + new ProvisionDeviceProfileCredentials(
  284 + requestMsg.getProvisionDeviceCredentialsMsg().getProvisionDeviceKey(),
  285 + requestMsg.getProvisionDeviceCredentialsMsg().getProvisionDeviceSecret())));
  286 + return Futures.transform(provisionResponseFuture, provisionResponse -> {
  287 + if (provisionResponse.getResponseStatus() == ProvisionResponseStatus.NOT_FOUND) {
  288 + return getTransportApiResponseMsg(TransportProtos.DeviceCredentialsProto.getDefaultInstance(), TransportProtos.ProvisionResponseStatus.NOT_FOUND);
  289 + } else if (provisionResponse.getResponseStatus() == ProvisionResponseStatus.FAILURE) {
  290 + return getTransportApiResponseMsg(TransportProtos.DeviceCredentialsProto.getDefaultInstance(), TransportProtos.ProvisionResponseStatus.FAILURE);
  291 + } else {
  292 + return getTransportApiResponseMsg(getDeviceCredentials(provisionResponse.getDeviceCredentials()), TransportProtos.ProvisionResponseStatus.SUCCESS);
  293 + }
  294 + }, dbCallbackExecutorService);
  295 + }
  296 +
  297 + private TransportApiResponseMsg getTransportApiResponseMsg(TransportProtos.DeviceCredentialsProto deviceCredentials, TransportProtos.ProvisionResponseStatus status) {
  298 + return TransportApiResponseMsg.newBuilder()
  299 + .setProvisionDeviceResponseMsg(TransportProtos.ProvisionDeviceResponseMsg.newBuilder()
  300 + .setDeviceCredentials(deviceCredentials)
  301 + .setProvisionResponseStatus(status)
  302 + .build())
  303 + .build();
  304 + }
  305 +
  306 + private TransportProtos.DeviceCredentialsProto getDeviceCredentials(DeviceCredentials deviceCredentials) {
  307 + return TransportProtos.DeviceCredentialsProto.newBuilder()
  308 + .setDeviceIdMSB(deviceCredentials.getDeviceId().getId().getMostSignificantBits())
  309 + .setDeviceIdLSB(deviceCredentials.getDeviceId().getId().getLeastSignificantBits())
  310 + .setCredentialsType(deviceCredentials.getCredentialsType() == DeviceCredentialsType.ACCESS_TOKEN ?
  311 + CredentialsType.ACCESS_TOKEN : CredentialsType.X509_CERTIFICATE)
  312 + .setCredentialsId(deviceCredentials.getCredentialsId())
  313 + .setCredentialsValue(deviceCredentials.getCredentialsValue() != null ? deviceCredentials.getCredentialsValue() : "")
  314 + .build();
  315 + }
264 316 private ListenableFuture<TransportApiResponseMsg> handle(GetTenantRoutingInfoRequestMsg requestMsg) {
265 317 TenantId tenantId = new TenantId(new UUID(requestMsg.getTenantIdMSB(), requestMsg.getTenantIdLSB()));
266 318 // TODO: Tenant Profile from cache
... ...
... ... @@ -68,6 +68,7 @@ import org.thingsboard.server.common.data.User;
68 68 import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileConfiguration;
69 69 import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileTransportConfiguration;
70 70 import org.thingsboard.server.common.data.device.profile.DeviceProfileData;
  71 +import org.thingsboard.server.common.data.device.profile.ProvisionDeviceProfileCredentials;
71 72 import org.thingsboard.server.common.data.id.HasId;
72 73 import org.thingsboard.server.common.data.id.RuleChainId;
73 74 import org.thingsboard.server.common.data.id.TenantId;
... ...
... ... @@ -28,6 +28,7 @@ import org.thingsboard.server.common.data.DeviceProfileType;
28 28 import org.thingsboard.server.common.data.DeviceTransportType;
29 29 import org.thingsboard.server.common.data.Tenant;
30 30 import org.thingsboard.server.common.data.User;
  31 +import org.thingsboard.server.common.data.device.profile.ProvisionDeviceProfileCredentials;
31 32 import org.thingsboard.server.common.data.page.PageData;
32 33 import org.thingsboard.server.common.data.page.PageLink;
33 34 import org.thingsboard.server.common.data.security.Authority;
... ... @@ -153,6 +154,17 @@ public abstract class BaseDeviceProfileControllerTest extends AbstractController
153 154 .andExpect(statusReason(containsString("Device profile with such name already exists")));
154 155 }
155 156
  157 + @Test
  158 + public void testSaveDeviceProfileWithSameProvisionDeviceKey() throws Exception {
  159 + DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile");
  160 + deviceProfile.setProvisionDeviceKey("testProvisionDeviceKey");
  161 + doPost("/api/deviceProfile", deviceProfile).andExpect(status().isOk());
  162 + DeviceProfile deviceProfile2 = this.createDeviceProfile("Device Profile 2");
  163 + deviceProfile2.setProvisionDeviceKey("testProvisionDeviceKey");
  164 + doPost("/api/deviceProfile", deviceProfile2).andExpect(status().isBadRequest())
  165 + .andExpect(statusReason(containsString("Device profile with such provision device key already exists")));
  166 + }
  167 +
156 168 @Ignore
157 169 @Test
158 170 public void testChangeDeviceProfileTypeWithExistingDevices() throws Exception {
... ...
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.dao.device;
  17 +
  18 +import com.google.common.util.concurrent.ListenableFuture;
  19 +import org.thingsboard.server.dao.device.provision.ProvisionRequest;
  20 +import org.thingsboard.server.dao.device.provision.ProvisionResponse;
  21 +
  22 +public interface DeviceProvisionService {
  23 + ListenableFuture<ProvisionResponse> provisionDevice(ProvisionRequest provisionRequest);
  24 +}
... ...
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.dao.device.provision;
  17 +
  18 +import lombok.AllArgsConstructor;
  19 +import lombok.Data;
  20 +import org.thingsboard.server.common.data.device.profile.ProvisionDeviceProfileCredentials;
  21 +
  22 +@Data
  23 +@AllArgsConstructor
  24 +public class ProvisionRequest {
  25 + private String deviceName;
  26 + private String x509CertPubKey;
  27 + private ProvisionDeviceProfileCredentials credentials;
  28 +}
... ...
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.dao.device.provision;
  17 +
  18 +import lombok.Data;
  19 +import org.thingsboard.server.common.data.security.DeviceCredentials;
  20 +
  21 +@Data
  22 +public class ProvisionResponse {
  23 + private final DeviceCredentials deviceCredentials;
  24 + private final ProvisionResponseStatus responseStatus;
  25 +}
... ...
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.dao.device.provision;
  17 +
  18 +public enum ProvisionResponseStatus {
  19 +
  20 + SUCCESS,
  21 + NOT_FOUND,
  22 + FAILURE
  23 +
  24 +}
... ...
... ... @@ -63,6 +63,8 @@ public class DataConstants {
63 63 public static final String ALARM_CLEAR = "ALARM_CLEAR";
64 64 public static final String ENTITY_ASSIGNED_FROM_TENANT = "ENTITY_ASSIGNED_FROM_TENANT";
65 65 public static final String ENTITY_ASSIGNED_TO_TENANT = "ENTITY_ASSIGNED_TO_TENANT";
  66 + public static final String PROVISION_SUCCESS = "PROVISION_SUCCESS";
  67 + public static final String PROVISION_FAILURE = "PROVISION_FAILURE";
66 68
67 69 public static final String RPC_CALL_FROM_SERVER_TO_DEVICE = "RPC_CALL_FROM_SERVER_TO_DEVICE";
68 70
... ... @@ -70,4 +72,12 @@ public class DataConstants {
70 72 public static final String SECRET_KEY_FIELD_NAME = "secretKey";
71 73 public static final String DURATION_MS_FIELD_NAME = "durationMs";
72 74
  75 + public static final String PROVISION = "provision";
  76 + public static final String PROVISION_KEY = "provisionDeviceKey";
  77 + public static final String PROVISION_SECRET = "provisionDeviceSecret";
  78 +
  79 + public static final String DEVICE_NAME = "deviceName";
  80 + public static final String DEVICE_TYPE = "deviceType";
  81 + public static final String CERT_PUB_KEY = "x509CertPubKey";
  82 +
73 83 }
... ...
... ... @@ -41,10 +41,12 @@ public class DeviceProfile extends SearchTextBased<DeviceProfileId> implements H
41 41 private boolean isDefault;
42 42 private DeviceProfileType type;
43 43 private DeviceTransportType transportType;
  44 + private DeviceProfileProvisionType provisionType;
44 45 private RuleChainId defaultRuleChainId;
45 46 private transient DeviceProfileData profileData;
46 47 @JsonIgnore
47 48 private byte[] profileDataBytes;
  49 + private String provisionDeviceKey;
48 50
49 51 public DeviceProfile() {
50 52 super();
... ... @@ -62,6 +64,7 @@ public class DeviceProfile extends SearchTextBased<DeviceProfileId> implements H
62 64 this.isDefault = deviceProfile.isDefault();
63 65 this.defaultRuleChainId = deviceProfile.getDefaultRuleChainId();
64 66 this.setProfileData(deviceProfile.getProfileData());
  67 + this.provisionDeviceKey = deviceProfile.getProvisionDeviceKey();
65 68 }
66 69
67 70 @Override
... ...
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.common.data;
  17 +
  18 +public enum DeviceProfileProvisionType {
  19 + DISABLED,
  20 + ALLOW_CREATE_NEW_DEVICES,
  21 + CHECK_PRE_PROVISIONED_DEVICES
  22 +}
... ...
... ... @@ -42,7 +42,9 @@ public enum ActionType {
42 42 LOGOUT(false),
43 43 LOCKOUT(false),
44 44 ASSIGNED_FROM_TENANT(false),
45   - ASSIGNED_TO_TENANT(false);
  45 + ASSIGNED_TO_TENANT(false),
  46 + PROVISION_SUCCESS(false),
  47 + PROVISION_FAILURE(false);
46 48
47 49 private final boolean isRead;
48 50
... ...
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.common.data.device.profile;
  17 +
  18 +import lombok.Data;
  19 +import org.thingsboard.server.common.data.DeviceProfileProvisionType;
  20 +
  21 +@Data
  22 +public class AllowCreateNewDevicesDeviceProfileProvisionConfiguration implements DeviceProfileProvisionConfiguration {
  23 +
  24 + private final String provisionDeviceSecret;
  25 +
  26 + @Override
  27 + public DeviceProfileProvisionType getType() {
  28 + return DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES;
  29 + }
  30 +
  31 +}
... ...
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.common.data.device.profile;
  17 +
  18 +import lombok.Data;
  19 +import org.thingsboard.server.common.data.DeviceProfileProvisionType;
  20 +
  21 +@Data
  22 +public class CheckPreProvisionedDevicesDeviceProfileProvisionConfiguration implements DeviceProfileProvisionConfiguration {
  23 +
  24 + private final String provisionDeviceSecret;
  25 +
  26 + @Override
  27 + public DeviceProfileProvisionType getType() {
  28 + return DeviceProfileProvisionType.CHECK_PRE_PROVISIONED_DEVICES;
  29 + }
  30 +
  31 +}
... ...
... ... @@ -24,6 +24,7 @@ public class DeviceProfileData {
24 24
25 25 private DeviceProfileConfiguration configuration;
26 26 private DeviceProfileTransportConfiguration transportConfiguration;
  27 + private DeviceProfileProvisionConfiguration provisionConfiguration;
27 28 private List<DeviceProfileAlarm> alarms;
28 29
29 30 }
... ...
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.common.data.device.profile;
  17 +
  18 +import com.fasterxml.jackson.annotation.JsonIgnore;
  19 +import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
  20 +import com.fasterxml.jackson.annotation.JsonSubTypes;
  21 +import com.fasterxml.jackson.annotation.JsonTypeInfo;
  22 +import org.thingsboard.server.common.data.DeviceProfileProvisionType;
  23 +
  24 +
  25 +@JsonIgnoreProperties(ignoreUnknown = true)
  26 +@JsonTypeInfo(
  27 + use = JsonTypeInfo.Id.NAME,
  28 + include = JsonTypeInfo.As.PROPERTY,
  29 + property = "type")
  30 +@JsonSubTypes({
  31 + @JsonSubTypes.Type(value = DisabledDeviceProfileProvisionConfiguration.class, name = "DISABLED"),
  32 + @JsonSubTypes.Type(value = AllowCreateNewDevicesDeviceProfileProvisionConfiguration.class, name = "ALLOW_CREATE_NEW_DEVICES"),
  33 + @JsonSubTypes.Type(value = CheckPreProvisionedDevicesDeviceProfileProvisionConfiguration.class, name = "CHECK_PRE_PROVISIONED_DEVICES")})
  34 +public interface DeviceProfileProvisionConfiguration {
  35 +
  36 + @JsonIgnore
  37 + DeviceProfileProvisionType getType();
  38 +
  39 +}
\ No newline at end of file
... ...
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.common.data.device.profile;
  17 +
  18 +import lombok.Data;
  19 +import org.thingsboard.server.common.data.DeviceProfileProvisionType;
  20 +
  21 +@Data
  22 +public class DisabledDeviceProfileProvisionConfiguration implements DeviceProfileProvisionConfiguration {
  23 +
  24 + private final String provisionDeviceSecret;
  25 +
  26 + @Override
  27 + public DeviceProfileProvisionType getType() {
  28 + return DeviceProfileProvisionType.DISABLED;
  29 + }
  30 +
  31 +}
... ...
... ... @@ -31,6 +31,8 @@ public class MqttTopics {
31 31 public static final String DEVICE_TELEMETRY_TOPIC = BASE_DEVICE_API_TOPIC + "/telemetry";
32 32 public static final String DEVICE_CLAIM_TOPIC = BASE_DEVICE_API_TOPIC + "/claim";
33 33 public static final String DEVICE_ATTRIBUTES_TOPIC = BASE_DEVICE_API_TOPIC + "/attributes";
  34 + public static final String DEVICE_PROVISION_REQUEST_TOPIC = BASE_DEVICE_API_TOPIC + "/provision";
  35 + public static final String DEVICE_PROVISION_RESPONSE_TOPIC = BASE_DEVICE_API_TOPIC + "/provision/response";
34 36
35 37 public static final String BASE_GATEWAY_API_TOPIC = "v1/gateway";
36 38 public static final String GATEWAY_CONNECT_TOPIC = BASE_GATEWAY_API_TOPIC + "/connect";
... ... @@ -41,6 +43,8 @@ public class MqttTopics {
41 43 public static final String GATEWAY_RPC_TOPIC = BASE_GATEWAY_API_TOPIC + "/rpc";
42 44 public static final String GATEWAY_ATTRIBUTES_REQUEST_TOPIC = BASE_GATEWAY_API_TOPIC + "/attributes/request";
43 45 public static final String GATEWAY_ATTRIBUTES_RESPONSE_TOPIC = BASE_GATEWAY_API_TOPIC + "/attributes/response";
  46 + public static final String GATEWAY_PROVISION_REQUEST_TOPIC = BASE_GATEWAY_API_TOPIC + "/provision";
  47 + public static final String GATEWAY_PROVISION_RESPONSE_TOPIC = BASE_GATEWAY_API_TOPIC + "/provision/response";
44 48
45 49
46 50 private MqttTopics() {
... ...
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.common.data.device.profile;
  17 +
  18 +import lombok.Data;
  19 +
  20 +@Data
  21 +public class ProvisionDeviceProfileCredentials {
  22 + private final String provisionDeviceKey;
  23 + private final String provisionDeviceSecret;
  24 +}
... ...
... ... @@ -16,5 +16,5 @@
16 16 package org.thingsboard.server.common.msg.session;
17 17
18 18 public enum FeatureType {
19   - ATTRIBUTES, TELEMETRY, RPC, CLAIM
  19 + ATTRIBUTES, TELEMETRY, RPC, CLAIM, PROVISION
20 20 }
... ...
... ... @@ -73,6 +73,11 @@ enum KeyValueType {
73 73 JSON_V = 4;
74 74 }
75 75
  76 +enum CredentialsType {
  77 + ACCESS_TOKEN = 0;
  78 + X509_CERTIFICATE = 1;
  79 +}
  80 +
76 81 message KeyValueProto {
77 82 string key = 1;
78 83 KeyValueType type = 2;
... ... @@ -241,6 +246,35 @@ message ClaimDeviceMsg {
241 246 int64 durationMs = 4;
242 247 }
243 248
  249 +message DeviceCredentialsProto {
  250 + int64 deviceIdMSB = 1;
  251 + int64 deviceIdLSB = 2;
  252 + CredentialsType credentialsType = 3;
  253 + string credentialsId = 4;
  254 + string credentialsValue = 5;
  255 +}
  256 +
  257 +message ProvisionDeviceRequestMsg {
  258 + string deviceName = 1;
  259 + string x509CertPubKey = 2;
  260 + ProvisionDeviceCredentialsMsg provisionDeviceCredentialsMsg = 3;
  261 +}
  262 +
  263 +message ProvisionDeviceCredentialsMsg {
  264 + string provisionDeviceKey = 1;
  265 + string provisionDeviceSecret = 2;
  266 +}
  267 +
  268 +message ProvisionDeviceResponseMsg {
  269 + DeviceCredentialsProto deviceCredentials = 1;
  270 + ProvisionResponseStatus provisionResponseStatus = 2;
  271 +}
  272 +
  273 +enum ProvisionResponseStatus {
  274 + SUCCESS = 0;
  275 + NOT_FOUND = 1;
  276 + FAILURE = 2;
  277 +}
244 278 //Used to report session state to tb-Service and persist this state in the cache on the tb-Service level.
245 279 message SubscriptionInfoProto {
246 280 int64 lastActivityTime = 1;
... ... @@ -266,6 +300,7 @@ message TransportToDeviceActorMsg {
266 300 ToDeviceRpcResponseMsg toDeviceRPCCallResponse = 6;
267 301 SubscriptionInfoProto subscriptionInfo = 7;
268 302 ClaimDeviceMsg claimDevice = 8;
  303 + ProvisionDeviceRequestMsg provisionDevice = 9;
269 304 }
270 305
271 306 message TransportToRuleEngineMsg {
... ... @@ -441,6 +476,7 @@ message TransportApiRequestMsg {
441 476 GetTenantRoutingInfoRequestMsg getTenantRoutingInfoRequestMsg = 4;
442 477 GetDeviceProfileRequestMsg getDeviceProfileRequestMsg = 5;
443 478 ValidateBasicMqttCredRequestMsg validateBasicMqttCredRequestMsg = 6;
  479 + ProvisionDeviceRequestMsg provisionDeviceRequestMsg = 7;
444 480 }
445 481
446 482 /* Response from ThingsBoard Core Service to Transport Service */
... ... @@ -449,6 +485,7 @@ message TransportApiResponseMsg {
449 485 GetOrCreateDeviceFromGatewayResponseMsg getOrCreateDeviceResponseMsg = 2;
450 486 GetTenantRoutingInfoResponseMsg getTenantRoutingInfoResponseMsg = 4;
451 487 GetDeviceProfileResponseMsg getDeviceProfileResponseMsg = 5;
  488 + ProvisionDeviceResponseMsg provisionDeviceResponseMsg = 6;
452 489 }
453 490
454 491 /* Messages that are handled by ThingsBoard Core Service */
... ... @@ -491,4 +528,5 @@ message ToTransportMsg {
491 528 ToServerRpcResponseMsg toServerResponse = 7;
492 529 DeviceProfileUpdateMsg deviceProfileUpdateMsg = 8;
493 530 DeviceProfileDeleteMsg deviceProfileDeleteMsg = 9;
  531 + ProvisionDeviceResponseMsg provisionResponse = 10;
494 532 }
... ...
... ... @@ -24,6 +24,7 @@ import org.eclipse.californium.core.network.ExchangeObserver;
24 24 import org.eclipse.californium.core.server.resources.CoapExchange;
25 25 import org.eclipse.californium.core.server.resources.Resource;
26 26 import org.springframework.util.ReflectionUtils;
  27 +import org.thingsboard.server.common.data.DataConstants;
27 28 import org.thingsboard.server.common.data.DeviceTransportType;
28 29 import org.thingsboard.server.common.data.security.DeviceTokenCredentials;
29 30 import org.thingsboard.server.common.msg.session.FeatureType;
... ... @@ -33,9 +34,11 @@ import org.thingsboard.server.common.transport.TransportContext;
33 34 import org.thingsboard.server.common.transport.TransportService;
34 35 import org.thingsboard.server.common.transport.TransportServiceCallback;
35 36 import org.thingsboard.server.common.transport.adaptor.AdaptorException;
  37 +import org.thingsboard.server.common.transport.adaptor.JsonConverter;
36 38 import org.thingsboard.server.common.transport.auth.SessionInfoCreator;
37 39 import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse;
38 40 import org.thingsboard.server.gen.transport.TransportProtos;
  41 +import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceResponseMsg;
39 42
40 43 import java.lang.reflect.Field;
41 44 import java.util.List;
... ... @@ -130,10 +133,25 @@ public class CoapTransportResource extends CoapResource {
130 133 case CLAIM:
131 134 processRequest(exchange, SessionMsgType.CLAIM_REQUEST);
132 135 break;
  136 + case PROVISION:
  137 + processProvision(exchange);
  138 + break;
133 139 }
134 140 }
135 141 }
136 142
  143 + private void processProvision(CoapExchange exchange) {
  144 + log.trace("Processing {}", exchange.advanced().getRequest());
  145 + exchange.accept();
  146 + try {
  147 + transportService.process(transportContext.getAdaptor().convertToProvisionRequestMsg(UUID.randomUUID(), exchange.advanced().getRequest()),
  148 + new DeviceProvisionCallback(exchange));
  149 + } catch (AdaptorException e) {
  150 + log.trace("Failed to decode message: ", e);
  151 + exchange.respond(ResponseCode.BAD_REQUEST);
  152 + }
  153 + }
  154 +
137 155 private void processRequest(CoapExchange exchange, SessionMsgType type) {
138 156 log.trace("Processing {}", exchange.advanced().getRequest());
139 157 exchange.accept();
... ... @@ -274,6 +292,8 @@ public class CoapTransportResource extends CoapResource {
274 292 try {
275 293 if (uriPath.size() >= FEATURE_TYPE_POSITION) {
276 294 return Optional.of(FeatureType.valueOf(uriPath.get(FEATURE_TYPE_POSITION - 1).toUpperCase()));
  295 + } else if (uriPath.size() == 3 && uriPath.contains(DataConstants.PROVISION)) {
  296 + return Optional.of(FeatureType.valueOf(DataConstants.PROVISION.toUpperCase()));
277 297 }
278 298 } catch (RuntimeException e) {
279 299 log.warn("Failed to decode feature type: {}", uriPath);
... ... @@ -325,6 +345,25 @@ public class CoapTransportResource extends CoapResource {
325 345 }
326 346 }
327 347
  348 + private static class DeviceProvisionCallback implements TransportServiceCallback<ProvisionDeviceResponseMsg> {
  349 + private final CoapExchange exchange;
  350 +
  351 + DeviceProvisionCallback(CoapExchange exchange) {
  352 + this.exchange = exchange;
  353 + }
  354 +
  355 + @Override
  356 + public void onSuccess(TransportProtos.ProvisionDeviceResponseMsg msg) {
  357 + exchange.respond(JsonConverter.toJson(msg).toString());
  358 + }
  359 +
  360 + @Override
  361 + public void onError(Throwable e) {
  362 + log.warn("Failed to process request", e);
  363 + exchange.respond(ResponseCode.INTERNAL_SERVER_ERROR);
  364 + }
  365 + }
  366 +
328 367 private static class CoapOkCallback implements TransportServiceCallback<Void> {
329 368 private final CoapExchange exchange;
330 369
... ...
... ... @@ -19,6 +19,7 @@ import org.eclipse.californium.core.coap.Request;
19 19 import org.eclipse.californium.core.coap.Response;
20 20 import org.thingsboard.server.common.transport.adaptor.AdaptorException;
21 21 import org.thingsboard.server.gen.transport.TransportProtos;
  22 +import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceRequestMsg;
22 23 import org.thingsboard.server.transport.coap.CoapTransportResource;
23 24
24 25 import java.util.UUID;
... ... @@ -45,4 +46,6 @@ public interface CoapTransportAdaptor {
45 46
46 47 Response convertToPublish(CoapTransportResource.CoapSessionListener coapSessionListener, TransportProtos.ToServerRpcResponseMsg msg) throws AdaptorException;
47 48
  49 + ProvisionDeviceRequestMsg convertToProvisionRequestMsg(UUID sessionId, Request inbound) throws AdaptorException;
  50 +
48 51 }
... ...
... ... @@ -124,6 +124,16 @@ public class JsonCoapAdaptor implements CoapTransportAdaptor {
124 124 }
125 125
126 126 @Override
  127 + public TransportProtos.ProvisionDeviceRequestMsg convertToProvisionRequestMsg(UUID sessionId, Request inbound) throws AdaptorException {
  128 + String payload = validatePayload(sessionId, inbound, false);
  129 + try {
  130 + return JsonConverter.convertToProvisionRequestMsg(payload);
  131 + } catch (IllegalStateException | JsonSyntaxException ex) {
  132 + throw new AdaptorException(ex);
  133 + }
  134 + }
  135 +
  136 + @Override
127 137 public Response convertToPublish(CoapTransportResource.CoapSessionListener session, TransportProtos.GetAttributeResponseMsg msg) throws AdaptorException {
128 138 if (msg.getClientAttributeListCount() == 0 && msg.getSharedAttributeListCount() == 0) {
129 139 return new Response(CoAP.ResponseCode.NOT_FOUND);
... ...
... ... @@ -44,6 +44,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.AttributeUpdateNotif
44 44 import org.thingsboard.server.gen.transport.TransportProtos.DeviceInfoProto;
45 45 import org.thingsboard.server.gen.transport.TransportProtos.GetAttributeRequestMsg;
46 46 import org.thingsboard.server.gen.transport.TransportProtos.GetAttributeResponseMsg;
  47 +import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceResponseMsg;
47 48 import org.thingsboard.server.gen.transport.TransportProtos.SessionCloseNotificationProto;
48 49 import org.thingsboard.server.gen.transport.TransportProtos.SessionInfoProto;
49 50 import org.thingsboard.server.gen.transport.TransportProtos.SubscribeToAttributeUpdatesMsg;
... ... @@ -203,6 +204,14 @@ public class DeviceApiController {
203 204 return responseWriter;
204 205 }
205 206
  207 + @RequestMapping(value = "/provision", method = RequestMethod.POST)
  208 + public DeferredResult<ResponseEntity> provisionDevice(@RequestBody String json, HttpServletRequest httpRequest) {
  209 + DeferredResult<ResponseEntity> responseWriter = new DeferredResult<>();
  210 + transportContext.getTransportService().process(JsonConverter.convertToProvisionRequestMsg(json),
  211 + new DeviceProvisionCallback(responseWriter));
  212 + return responseWriter;
  213 + }
  214 +
206 215 private static class DeviceAuthCallback implements TransportServiceCallback<ValidateDeviceCredentialsResponse> {
207 216 private final TransportContext transportContext;
208 217 private final DeferredResult<ResponseEntity> responseWriter;
... ... @@ -230,6 +239,25 @@ public class DeviceApiController {
230 239 }
231 240 }
232 241
  242 + private static class DeviceProvisionCallback implements TransportServiceCallback<ProvisionDeviceResponseMsg> {
  243 + private final DeferredResult<ResponseEntity> responseWriter;
  244 +
  245 + DeviceProvisionCallback(DeferredResult<ResponseEntity> responseWriter) {
  246 + this.responseWriter = responseWriter;
  247 + }
  248 +
  249 + @Override
  250 + public void onSuccess(ProvisionDeviceResponseMsg msg) {
  251 + responseWriter.setResult(new ResponseEntity<>(JsonConverter.toJson(msg).toString(), HttpStatus.OK));
  252 + }
  253 +
  254 + @Override
  255 + public void onError(Throwable e) {
  256 + log.warn("Failed to process request", e);
  257 + responseWriter.setResult(new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR));
  258 + }
  259 + }
  260 +
233 261 private static class SessionCloseOnErrorCallback implements TransportServiceCallback<Void> {
234 262 private final TransportService transportService;
235 263 private final SessionInfoProto sessionInfo;
... ...
... ... @@ -34,11 +34,12 @@ import io.netty.handler.codec.mqtt.MqttSubscribeMessage;
34 34 import io.netty.handler.codec.mqtt.MqttTopicSubscription;
35 35 import io.netty.handler.codec.mqtt.MqttUnsubscribeMessage;
36 36 import io.netty.handler.ssl.SslHandler;
  37 +import io.netty.util.CharsetUtil;
37 38 import io.netty.util.ReferenceCountUtil;
38 39 import io.netty.util.concurrent.Future;
39 40 import io.netty.util.concurrent.GenericFutureListener;
40 41 import lombok.extern.slf4j.Slf4j;
41   -import org.springframework.util.StringUtils;
  42 +import org.thingsboard.server.common.data.DataConstants;
42 43 import org.thingsboard.server.common.data.DeviceProfile;
43 44 import org.thingsboard.server.common.data.DeviceTransportType;
44 45 import org.thingsboard.server.common.data.device.profile.MqttTopics;
... ... @@ -52,9 +53,9 @@ import org.thingsboard.server.common.transport.auth.TransportDeviceInfo;
52 53 import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse;
53 54 import org.thingsboard.server.common.transport.service.DefaultTransportService;
54 55 import org.thingsboard.server.gen.transport.TransportProtos;
  56 +import org.thingsboard.server.gen.transport.TransportProtos.DeviceInfoProto;
  57 +import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceResponseMsg;
55 58 import org.thingsboard.server.gen.transport.TransportProtos.SessionEvent;
56   -import org.thingsboard.server.gen.transport.TransportProtos.SessionInfoProto;
57   -import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceTokenRequestMsg;
58 59 import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceX509CertRequestMsg;
59 60 import org.thingsboard.server.transport.mqtt.adaptors.MqttTransportAdaptor;
60 61 import org.thingsboard.server.transport.mqtt.session.DeviceSessionCtx;
... ... @@ -66,7 +67,6 @@ import javax.net.ssl.SSLPeerUnverifiedException;
66 67 import javax.security.cert.X509Certificate;
67 68 import java.io.IOException;
68 69 import java.net.InetSocketAddress;
69   -import java.nio.charset.StandardCharsets;
70 70 import java.util.ArrayList;
71 71 import java.util.List;
72 72 import java.util.UUID;
... ... @@ -74,9 +74,9 @@ import java.util.concurrent.ConcurrentHashMap;
74 74 import java.util.concurrent.ConcurrentMap;
75 75
76 76 import static io.netty.handler.codec.mqtt.MqttConnectReturnCode.CONNECTION_ACCEPTED;
77   -import static io.netty.handler.codec.mqtt.MqttConnectReturnCode.CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD;
78 77 import static io.netty.handler.codec.mqtt.MqttConnectReturnCode.CONNECTION_REFUSED_NOT_AUTHORIZED;
79 78 import static io.netty.handler.codec.mqtt.MqttMessageType.CONNACK;
  79 +import static io.netty.handler.codec.mqtt.MqttMessageType.CONNECT;
80 80 import static io.netty.handler.codec.mqtt.MqttMessageType.PINGRESP;
81 81 import static io.netty.handler.codec.mqtt.MqttMessageType.PUBACK;
82 82 import static io.netty.handler.codec.mqtt.MqttMessageType.SUBACK;
... ... @@ -136,10 +136,45 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
136 136 return;
137 137 }
138 138 deviceSessionCtx.setChannel(ctx);
  139 + if (CONNECT.equals(msg.fixedHeader().messageType())) {
  140 + processConnect(ctx, (MqttConnectMessage) msg);
  141 + } else if (deviceSessionCtx.isProvisionOnly()) {
  142 + processProvisionSessionMsg(ctx, msg);
  143 + } else {
  144 + processRegularSessionMsg(ctx, msg);
  145 + }
  146 + }
  147 +
  148 + private void processProvisionSessionMsg(ChannelHandlerContext ctx, MqttMessage msg) {
139 149 switch (msg.fixedHeader().messageType()) {
140   - case CONNECT:
141   - processConnect(ctx, (MqttConnectMessage) msg);
  150 + case PUBLISH:
  151 + MqttPublishMessage mqttMsg = (MqttPublishMessage) msg;
  152 + String topicName = mqttMsg.variableHeader().topicName();
  153 + int msgId = mqttMsg.variableHeader().packetId();
  154 + try {
  155 + if (topicName.equals(MqttTopics.DEVICE_PROVISION_REQUEST_TOPIC)) {
  156 + TransportProtos.ProvisionDeviceRequestMsg provisionRequestMsg = adaptor.convertToProvisionRequestMsg(deviceSessionCtx, mqttMsg);
  157 + transportService.process(provisionRequestMsg, new DeviceProvisionCallback(ctx, msgId, provisionRequestMsg));
  158 + log.trace("[{}][{}] Processing publish msg [{}][{}]!", sessionId, deviceSessionCtx.getDeviceId(), topicName, msgId);
  159 + } else {
  160 + throw new RuntimeException("Unsupported topic for provisioning requests!");
  161 + }
  162 + } catch (RuntimeException | AdaptorException e) {
  163 + log.warn("[{}] Failed to process publish msg [{}][{}]", sessionId, topicName, msgId, e);
  164 + ctx.close();
  165 + }
142 166 break;
  167 + case PINGREQ:
  168 + ctx.writeAndFlush(new MqttMessage(new MqttFixedHeader(PINGRESP, false, AT_MOST_ONCE, false, 0)));
  169 + break;
  170 + case DISCONNECT:
  171 + ctx.close();
  172 + break;
  173 + }
  174 + }
  175 +
  176 + private void processRegularSessionMsg(ChannelHandlerContext ctx, MqttMessage msg) {
  177 + switch (msg.fixedHeader().messageType()) {
143 178 case PUBLISH:
144 179 processPublish(ctx, (MqttPublishMessage) msg);
145 180 break;
... ... @@ -263,6 +298,37 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
263 298 };
264 299 }
265 300
  301 + private class DeviceProvisionCallback implements TransportServiceCallback<ProvisionDeviceResponseMsg> {
  302 + private final ChannelHandlerContext ctx;
  303 + private final int msgId;
  304 + private final TransportProtos.ProvisionDeviceRequestMsg msg;
  305 +
  306 + DeviceProvisionCallback(ChannelHandlerContext ctx, int msgId, TransportProtos.ProvisionDeviceRequestMsg msg) {
  307 + this.ctx = ctx;
  308 + this.msgId = msgId;
  309 + this.msg = msg;
  310 + }
  311 +
  312 + @Override
  313 + public void onSuccess(TransportProtos.ProvisionDeviceResponseMsg provisionResponseMsg) {
  314 + log.trace("[{}] Published msg: {}", sessionId, msg);
  315 + if (msgId > 0) {
  316 + ctx.writeAndFlush(createMqttPubAckMsg(msgId));
  317 + }
  318 + try {
  319 + adaptor.convertToPublish(deviceSessionCtx, provisionResponseMsg).ifPresent(deviceSessionCtx.getChannel()::writeAndFlush);
  320 + } catch (Exception e) {
  321 + log.trace("[{}] Failed to convert device attributes response to MQTT msg", sessionId, e);
  322 + }
  323 + }
  324 +
  325 + @Override
  326 + public void onError(Throwable e) {
  327 + log.trace("[{}] Failed to publish msg: {}", sessionId, msg, e);
  328 + processDisconnect(ctx);
  329 + }
  330 + }
  331 +
266 332 private void processSubscribe(ChannelHandlerContext ctx, MqttSubscribeMessage mqttMsg) {
267 333 if (!checkConnected(ctx, mqttMsg)) {
268 334 return;
... ... @@ -292,6 +358,8 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
292 358 case MqttTopics.GATEWAY_RPC_TOPIC:
293 359 case MqttTopics.GATEWAY_ATTRIBUTES_RESPONSE_TOPIC:
294 360 case MqttTopics.DEVICE_ATTRIBUTES_RESPONSES_TOPIC:
  361 + case MqttTopics.GATEWAY_PROVISION_RESPONSE_TOPIC:
  362 + case MqttTopics.DEVICE_PROVISION_RESPONSE_TOPIC:
295 363 registerSubQoS(topic, grantedQoSList, reqQoS);
296 364 break;
297 365 default:
... ... @@ -357,11 +425,18 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
357 425
358 426 private void processConnect(ChannelHandlerContext ctx, MqttConnectMessage msg) {
359 427 log.info("[{}] Processing connect msg for client: {}!", sessionId, msg.payload().clientIdentifier());
360   - X509Certificate cert;
361   - if (sslHandler != null && (cert = getX509Certificate()) != null) {
362   - processX509CertConnect(ctx, cert);
  428 + String userName = msg.payload().userName();
  429 + if (DataConstants.PROVISION.equals(userName)) {
  430 + deviceSessionCtx.setProvisionOnly(true);
  431 + ctx.writeAndFlush(createMqttConnAckMsg(CONNECTION_ACCEPTED));
363 432 } else {
364   - processAuthTokenConnect(ctx, msg);
  433 + X509Certificate cert;
  434 +
  435 + if (sslHandler != null && (cert = getX509Certificate()) != null) {
  436 + processX509CertConnect(ctx, cert);
  437 + } else {
  438 + processAuthTokenConnect(ctx, msg);
  439 + }
365 440 }
366 441 }
367 442
... ...
... ... @@ -184,6 +184,28 @@ public class JsonMqttAdaptor implements MqttTransportAdaptor {
184 184 return Optional.of(createMqttPublishMsg(ctx, MqttTopics.DEVICE_RPC_RESPONSE_TOPIC + rpcResponse.getRequestId(), JsonConverter.toJson(rpcResponse)));
185 185 }
186 186
  187 + @Override
  188 + public Optional<MqttMessage> convertToPublish(MqttDeviceAwareSessionContext ctx, TransportProtos.ProvisionDeviceResponseMsg provisionResponse) {
  189 + return Optional.of(createMqttPublishMsg(ctx, MqttTopics.DEVICE_PROVISION_RESPONSE_TOPIC, JsonConverter.toJson(provisionResponse)));
  190 + }
  191 +
  192 + @Override
  193 + public TransportProtos.ProvisionDeviceRequestMsg convertToProvisionRequestMsg(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException {
  194 + String payload = validatePayload(ctx.getSessionId(), inbound.payload(), false);
  195 + try {
  196 + return JsonConverter.convertToProvisionRequestMsg(payload);
  197 + } catch (IllegalStateException | JsonSyntaxException ex) {
  198 + throw new AdaptorException(ex);
  199 + }
  200 + }
  201 +
  202 + @Override
  203 + public Optional<MqttMessage> convertToGatewayPublish(MqttDeviceAwareSessionContext ctx, TransportProtos.ProvisionDeviceResponseMsg responseMsg, int requestId) {
  204 + return Optional.of(createMqttPublishMsg(ctx,
  205 + MqttTopics.GATEWAY_PROVISION_REQUEST_TOPIC,
  206 + JsonConverter.toJson(responseMsg, requestId)));
  207 + }
  208 +
187 209 private MqttPublishMessage createMqttPublishMsg(MqttDeviceAwareSessionContext ctx, String topic, JsonElement json) {
188 210 MqttFixedHeader mqttFixedHeader =
189 211 new MqttFixedHeader(MqttMessageType.PUBLISH, false, ctx.getQoSForTopic(topic), false, 0);
... ...
... ... @@ -24,6 +24,8 @@ import org.thingsboard.server.gen.transport.TransportProtos.GetAttributeRequestM
24 24 import org.thingsboard.server.gen.transport.TransportProtos.GetAttributeResponseMsg;
25 25 import org.thingsboard.server.gen.transport.TransportProtos.PostAttributeMsg;
26 26 import org.thingsboard.server.gen.transport.TransportProtos.PostTelemetryMsg;
  27 +import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceRequestMsg;
  28 +import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceResponseMsg;
27 29 import org.thingsboard.server.gen.transport.TransportProtos.ToDeviceRpcRequestMsg;
28 30 import org.thingsboard.server.gen.transport.TransportProtos.ToDeviceRpcResponseMsg;
29 31 import org.thingsboard.server.gen.transport.TransportProtos.ToServerRpcRequestMsg;
... ... @@ -63,4 +65,10 @@ public interface MqttTransportAdaptor {
63 65
64 66 Optional<MqttMessage> convertToPublish(MqttDeviceAwareSessionContext ctx, ToServerRpcResponseMsg rpcResponse) throws AdaptorException;
65 67
  68 + ProvisionDeviceRequestMsg convertToProvisionRequestMsg(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException;
  69 +
  70 + Optional<MqttMessage> convertToPublish(MqttDeviceAwareSessionContext ctx, ProvisionDeviceResponseMsg provisionResponse) throws AdaptorException;
  71 +
  72 + Optional<MqttMessage> convertToGatewayPublish(MqttDeviceAwareSessionContext ctx, ProvisionDeviceResponseMsg provisionResponse, int requestId) throws AdaptorException;
  73 +
66 74 }
... ...
... ... @@ -17,6 +17,7 @@ package org.thingsboard.server.transport.mqtt.session;
17 17
18 18 import io.netty.channel.ChannelHandlerContext;
19 19 import lombok.Getter;
  20 +import lombok.Setter;
20 21 import lombok.extern.slf4j.Slf4j;
21 22 import org.thingsboard.server.common.data.DeviceProfile;
22 23 import org.thingsboard.server.common.data.DeviceTransportType;
... ... @@ -40,6 +41,10 @@ public class DeviceSessionCtx extends MqttDeviceAwareSessionContext {
40 41 private ChannelHandlerContext channel;
41 42 private final AtomicInteger msgIdSeq = new AtomicInteger(0);
42 43
  44 + @Getter
  45 + @Setter
  46 + private boolean provisionOnly;
  47 +
43 48 private volatile MqttTopicFilter telemetryTopicFilter = MqttTopicFilterFactory.getDefaultTelemetryFilter();
44 49 private volatile MqttTopicFilter attributesTopicFilter = MqttTopicFilterFactory.getDefaultAttributesFilter();
45 50
... ...
... ... @@ -27,6 +27,8 @@ import org.thingsboard.server.gen.transport.TransportProtos.GetTenantRoutingInfo
27 27 import org.thingsboard.server.gen.transport.TransportProtos.GetTenantRoutingInfoResponseMsg;
28 28 import org.thingsboard.server.gen.transport.TransportProtos.PostAttributeMsg;
29 29 import org.thingsboard.server.gen.transport.TransportProtos.PostTelemetryMsg;
  30 +import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceRequestMsg;
  31 +import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceResponseMsg;
30 32 import org.thingsboard.server.gen.transport.TransportProtos.SessionEventMsg;
31 33 import org.thingsboard.server.gen.transport.TransportProtos.SessionInfoProto;
32 34 import org.thingsboard.server.gen.transport.TransportProtos.SubscribeToAttributeUpdatesMsg;
... ... @@ -57,6 +59,9 @@ public interface TransportService {
57 59 void process(GetOrCreateDeviceFromGatewayRequestMsg msg,
58 60 TransportServiceCallback<GetOrCreateDeviceFromGatewayResponse> callback);
59 61
  62 + void process(ProvisionDeviceRequestMsg msg,
  63 + TransportServiceCallback<ProvisionDeviceResponseMsg> callback);
  64 +
60 65 void getDeviceProfile(DeviceProfileId deviceProfileId, TransportServiceCallback<DeviceProfile> callback);
61 66
62 67 void onProfileUpdate(DeviceProfile deviceProfile);
... ... @@ -90,5 +95,4 @@ public interface TransportService {
90 95 void reportActivity(SessionInfoProto sessionInfo);
91 96
92 97 void deregisterSession(SessionInfoProto sessionInfo);
93   -
94 98 }
... ...
... ... @@ -42,6 +42,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.KeyValueProto;
42 42 import org.thingsboard.server.gen.transport.TransportProtos.KeyValueType;
43 43 import org.thingsboard.server.gen.transport.TransportProtos.PostAttributeMsg;
44 44 import org.thingsboard.server.gen.transport.TransportProtos.PostTelemetryMsg;
  45 +import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceResponseMsg;
45 46 import org.thingsboard.server.gen.transport.TransportProtos.TsKvListProto;
46 47 import org.thingsboard.server.gen.transport.TransportProtos.TsKvProto;
47 48
... ... @@ -53,6 +54,7 @@ import java.util.Map;
53 54 import java.util.Map.Entry;
54 55 import java.util.Set;
55 56 import java.util.TreeMap;
  57 +import java.util.UUID;
56 58 import java.util.function.Consumer;
57 59 import java.util.stream.Collectors;
58 60
... ... @@ -397,6 +399,33 @@ public class JsonConverter {
397 399 }
398 400 }
399 401
  402 + public static JsonObject toJson(ProvisionDeviceResponseMsg payload) {
  403 + return toJson(payload, false, 0);
  404 + }
  405 +
  406 + public static JsonObject toJson(ProvisionDeviceResponseMsg payload, int requestId) {
  407 + return toJson(payload, true, requestId);
  408 + }
  409 +
  410 + private static JsonObject toJson(ProvisionDeviceResponseMsg payload, boolean toGateway, int requestId) {
  411 + JsonObject result = new JsonObject();
  412 + if (payload.getProvisionResponseStatus() == TransportProtos.ProvisionResponseStatus.NOT_FOUND) {
  413 + result.addProperty("errorMsg", "Provision data was not found!");
  414 + } else if (payload.getProvisionResponseStatus() == TransportProtos.ProvisionResponseStatus.FAILURE) {
  415 + result.addProperty("errorMsg", "Failed to provision device!");
  416 + } else {
  417 + if (toGateway) {
  418 + result.addProperty("id", requestId);
  419 + }
  420 + result.addProperty("deviceId", new UUID(payload.getDeviceCredentials().getDeviceIdMSB(), payload.getDeviceCredentials().getDeviceIdLSB()).toString());
  421 + result.addProperty("credentialsType", payload.getDeviceCredentials().getCredentialsType().name());
  422 + result.addProperty("credentialsId", payload.getDeviceCredentials().getCredentialsId());
  423 + result.addProperty("credentialsValue",
  424 + StringUtils.isEmpty(payload.getDeviceCredentials().getCredentialsValue()) ? null : payload.getDeviceCredentials().getCredentialsValue());
  425 + }
  426 + return result;
  427 + }
  428 +
400 429 public static JsonElement toErrorJson(String errorMsg) {
401 430 JsonObject error = new JsonObject();
402 431 error.addProperty("error", errorMsg);
... ... @@ -498,4 +527,43 @@ public class JsonConverter {
498 527 maxStringValueLength = length;
499 528 }
500 529
  530 + public static TransportProtos.ProvisionDeviceRequestMsg convertToProvisionRequestMsg(String json) {
  531 + JsonElement jsonElement = new JsonParser().parse(json);
  532 + if (jsonElement.isJsonObject()) {
  533 + return buildProvisionRequestMsg(jsonElement.getAsJsonObject());
  534 + } else {
  535 + throw new JsonSyntaxException(CAN_T_PARSE_VALUE + jsonElement);
  536 + }
  537 + }
  538 +
  539 + public static TransportProtos.ProvisionDeviceRequestMsg convertToProvisionRequestMsg(JsonObject jo) {
  540 + return buildProvisionRequestMsg(jo);
  541 + }
  542 +
  543 + private static TransportProtos.ProvisionDeviceRequestMsg buildProvisionRequestMsg(JsonObject jo) {
  544 + return TransportProtos.ProvisionDeviceRequestMsg.newBuilder()
  545 + .setDeviceName(getStrValue(jo, DataConstants.DEVICE_NAME, true))
  546 + .setX509CertPubKey(getStrValue(jo, DataConstants.CERT_PUB_KEY, false))
  547 + .setProvisionDeviceCredentialsMsg(buildProvisionDeviceCredentialsMsg(
  548 + getStrValue(jo, DataConstants.PROVISION_KEY, true),
  549 + getStrValue(jo, DataConstants.PROVISION_SECRET, true)))
  550 + .build();
  551 + }
  552 +
  553 + private static TransportProtos.ProvisionDeviceCredentialsMsg buildProvisionDeviceCredentialsMsg(String provisionKey, String provisionSecret) {
  554 + return TransportProtos.ProvisionDeviceCredentialsMsg.newBuilder()
  555 + .setProvisionDeviceKey(provisionKey)
  556 + .setProvisionDeviceSecret(provisionSecret)
  557 + .build();
  558 + }
  559 + private static String getStrValue(JsonObject jo, String field, boolean requiredField) {
  560 + if (jo.has(field)) {
  561 + return jo.get(field).getAsString();
  562 + } else {
  563 + if (requiredField) {
  564 + throw new RuntimeException("Failed to find the field " + field + " in JSON body " + jo + "!");
  565 + }
  566 + return "";
  567 + }
  568 + }
501 569 }
... ...
... ... @@ -52,6 +52,8 @@ import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsRes
52 52 import org.thingsboard.server.common.transport.util.DataDecodingEncodingService;
53 53 import org.thingsboard.server.common.transport.util.JsonUtils;
54 54 import org.thingsboard.server.gen.transport.TransportProtos;
  55 +import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceRequestMsg;
  56 +import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceResponseMsg;
55 57 import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg;
56 58 import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg;
57 59 import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg;
... ... @@ -333,6 +335,16 @@ public class DefaultTransportService implements TransportService {
333 335 }
334 336
335 337 @Override
  338 + public void process(ProvisionDeviceRequestMsg requestMsg, TransportServiceCallback<ProvisionDeviceResponseMsg> callback) {
  339 + log.trace("Processing msg: {}", requestMsg);
  340 + TbProtoQueueMsg<TransportApiRequestMsg> protoMsg = new TbProtoQueueMsg<>(UUID.randomUUID(), TransportApiRequestMsg.newBuilder().setProvisionDeviceRequestMsg(requestMsg).build());
  341 + ListenableFuture<ProvisionDeviceResponseMsg> response = Futures.transform(transportApiRequestTemplate.send(protoMsg), tmp ->
  342 + tmp.getValue().getProvisionDeviceResponseMsg()
  343 + , MoreExecutors.directExecutor());
  344 + AsyncCallbackTemplate.withCallback(response, callback::onSuccess, callback::onError, transportCallbackExecutor);
  345 + }
  346 +
  347 + @Override
336 348 public void process(TransportProtos.SessionInfoProto sessionInfo, TransportProtos.SubscriptionInfoProto msg, TransportServiceCallback<Void> callback) {
337 349 if (log.isTraceEnabled()) {
338 350 log.trace("[{}] Processing msg: {}", toSessionId(sessionInfo), msg);
... ...
... ... @@ -47,6 +47,7 @@ import org.thingsboard.server.common.data.security.DeviceCredentials;
47 47 import org.thingsboard.server.dao.audit.sink.AuditLogSink;
48 48 import org.thingsboard.server.dao.entity.EntityService;
49 49 import org.thingsboard.server.dao.exception.DataValidationException;
  50 +import org.thingsboard.server.dao.device.provision.ProvisionRequest;
50 51 import org.thingsboard.server.dao.service.DataValidator;
51 52
52 53 import java.io.PrintWriter;
... ... @@ -257,6 +258,13 @@ public class AuditLogServiceImpl implements AuditLogService {
257 258 actionData.put("os", os);
258 259 actionData.put("device", device);
259 260 break;
  261 + case PROVISION_SUCCESS:
  262 + case PROVISION_FAILURE:
  263 + ProvisionRequest request = extractParameter(ProvisionRequest.class, additionalInfo);
  264 + if (request != null) {
  265 + actionData.set("provisionRequest", objectMapper.valueToTree(request));
  266 + }
  267 + break;
260 268 }
261 269 return actionData;
262 270 }
... ...
... ... @@ -214,5 +214,4 @@ public interface DeviceDao extends Dao<Device> {
214 214 * @return the list of device objects
215 215 */
216 216 PageData<Device> findDevicesByTenantIdAndProfileId(UUID tenantId, UUID profileId, PageLink pageLink);
217   -
218 217 }
... ...
... ... @@ -38,5 +38,7 @@ public interface DeviceProfileDao extends Dao<DeviceProfile> {
38 38
39 39 DeviceProfileInfo findDefaultDeviceProfileInfo(TenantId tenantId);
40 40
  41 + DeviceProfile findByProvisionDeviceKey(String provisionDeviceKey);
  42 +
41 43 DeviceProfile findByName(TenantId tenantId, String profileName);
42 44 }
... ...
... ... @@ -26,6 +26,7 @@ import org.springframework.stereotype.Service;
26 26 import org.thingsboard.server.common.data.Device;
27 27 import org.thingsboard.server.common.data.DeviceProfile;
28 28 import org.thingsboard.server.common.data.DeviceProfileInfo;
  29 +import org.thingsboard.server.common.data.DeviceProfileProvisionType;
29 30 import org.thingsboard.server.common.data.DeviceProfileType;
30 31 import org.thingsboard.server.common.data.DeviceTransportType;
31 32 import org.thingsboard.server.common.data.EntitySubtype;
... ... @@ -33,6 +34,7 @@ import org.thingsboard.server.common.data.Tenant;
33 34 import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileConfiguration;
34 35 import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileTransportConfiguration;
35 36 import org.thingsboard.server.common.data.device.profile.DeviceProfileData;
  37 +import org.thingsboard.server.common.data.device.profile.DisabledDeviceProfileProvisionConfiguration;
36 38 import org.thingsboard.server.common.data.id.DeviceProfileId;
37 39 import org.thingsboard.server.common.data.id.TenantId;
38 40 import org.thingsboard.server.common.data.page.PageData;
... ... @@ -112,6 +114,8 @@ public class DeviceProfileServiceImpl extends AbstractEntityService implements D
112 114 ConstraintViolationException e = extractConstraintViolationException(t).orElse(null);
113 115 if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("device_profile_name_unq_key")) {
114 116 throw new DataValidationException("Device profile with such name already exists!");
  117 + } else if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("device_provision_key_unq_key")) {
  118 + throw new DataValidationException("Device profile with such provision device key already exists!");
115 119 } else {
116 120 throw t;
117 121 }
... ... @@ -210,12 +214,15 @@ public class DeviceProfileServiceImpl extends AbstractEntityService implements D
210 214 deviceProfile.setName(profileName);
211 215 deviceProfile.setType(DeviceProfileType.DEFAULT);
212 216 deviceProfile.setTransportType(DeviceTransportType.DEFAULT);
  217 + deviceProfile.setProvisionType(DeviceProfileProvisionType.DISABLED);
213 218 deviceProfile.setDescription("Default device profile");
214 219 DeviceProfileData deviceProfileData = new DeviceProfileData();
215 220 DefaultDeviceProfileConfiguration configuration = new DefaultDeviceProfileConfiguration();
216 221 DefaultDeviceProfileTransportConfiguration transportConfiguration = new DefaultDeviceProfileTransportConfiguration();
  222 + DisabledDeviceProfileProvisionConfiguration provisionConfiguration = new DisabledDeviceProfileProvisionConfiguration(null);
217 223 deviceProfileData.setConfiguration(configuration);
218 224 deviceProfileData.setTransportConfiguration(transportConfiguration);
  225 + deviceProfileData.setProvisionConfiguration(provisionConfiguration);
219 226 deviceProfile.setProfileData(deviceProfileData);
220 227 return saveDeviceProfile(deviceProfile);
221 228 }
... ...
... ... @@ -169,10 +169,12 @@ public class ModelConstants {
169 169 public static final String DEVICE_PROFILE_NAME_PROPERTY = "name";
170 170 public static final String DEVICE_PROFILE_TYPE_PROPERTY = "type";
171 171 public static final String DEVICE_PROFILE_TRANSPORT_TYPE_PROPERTY = "transport_type";
  172 + public static final String DEVICE_PROFILE_PROVISION_TYPE_PROPERTY = "provision_type";
172 173 public static final String DEVICE_PROFILE_PROFILE_DATA_PROPERTY = "profile_data";
173 174 public static final String DEVICE_PROFILE_DESCRIPTION_PROPERTY = "description";
174 175 public static final String DEVICE_PROFILE_IS_DEFAULT_PROPERTY = "is_default";
175 176 public static final String DEVICE_PROFILE_DEFAULT_RULE_CHAIN_ID_PROPERTY = "default_rule_chain_id";
  177 + public static final String DEVICE_PROFILE_PROVISION_DEVICE_KEY = "provision_device_key";
176 178
177 179 /**
178 180 * Cassandra entityView constants.
... ...
... ... @@ -23,6 +23,7 @@ import org.hibernate.annotations.Type;
23 23 import org.hibernate.annotations.TypeDef;
24 24 import org.thingsboard.server.common.data.DeviceProfile;
25 25 import org.thingsboard.server.common.data.DeviceProfileType;
  26 +import org.thingsboard.server.common.data.DeviceProfileProvisionType;
26 27 import org.thingsboard.server.common.data.DeviceTransportType;
27 28 import org.thingsboard.server.common.data.device.profile.DeviceProfileData;
28 29 import org.thingsboard.server.common.data.id.DeviceProfileId;
... ... @@ -62,6 +63,10 @@ public final class DeviceProfileEntity extends BaseSqlEntity<DeviceProfile> impl
62 63 @Column(name = ModelConstants.DEVICE_PROFILE_TRANSPORT_TYPE_PROPERTY)
63 64 private DeviceTransportType transportType;
64 65
  66 + @Enumerated(EnumType.STRING)
  67 + @Column(name = ModelConstants.DEVICE_PROFILE_PROVISION_TYPE_PROPERTY)
  68 + private DeviceProfileProvisionType provisionType;
  69 +
65 70 @Column(name = ModelConstants.DEVICE_PROFILE_DESCRIPTION_PROPERTY)
66 71 private String description;
67 72
... ... @@ -78,6 +83,9 @@ public final class DeviceProfileEntity extends BaseSqlEntity<DeviceProfile> impl
78 83 @Column(name = ModelConstants.DEVICE_PROFILE_PROFILE_DATA_PROPERTY, columnDefinition = "jsonb")
79 84 private JsonNode profileData;
80 85
  86 + @Column(name=ModelConstants.DEVICE_PROFILE_PROVISION_DEVICE_KEY)
  87 + private String provisionDeviceKey;
  88 +
81 89 public DeviceProfileEntity() {
82 90 super();
83 91 }
... ... @@ -93,12 +101,14 @@ public final class DeviceProfileEntity extends BaseSqlEntity<DeviceProfile> impl
93 101 this.name = deviceProfile.getName();
94 102 this.type = deviceProfile.getType();
95 103 this.transportType = deviceProfile.getTransportType();
  104 + this.provisionType = deviceProfile.getProvisionType();
96 105 this.description = deviceProfile.getDescription();
97 106 this.isDefault = deviceProfile.isDefault();
98 107 this.profileData = JacksonUtil.convertValue(deviceProfile.getProfileData(), ObjectNode.class);
99 108 if (deviceProfile.getDefaultRuleChainId() != null) {
100 109 this.defaultRuleChainId = deviceProfile.getDefaultRuleChainId().getId();
101 110 }
  111 + this.provisionDeviceKey = deviceProfile.getProvisionDeviceKey();
102 112 }
103 113
104 114 @Override
... ... @@ -125,12 +135,14 @@ public final class DeviceProfileEntity extends BaseSqlEntity<DeviceProfile> impl
125 135 deviceProfile.setName(name);
126 136 deviceProfile.setType(type);
127 137 deviceProfile.setTransportType(transportType);
  138 + deviceProfile.setProvisionType(provisionType);
128 139 deviceProfile.setDescription(description);
129 140 deviceProfile.setDefault(isDefault);
130 141 deviceProfile.setProfileData(JacksonUtil.convertValue(profileData, DeviceProfileData.class));
131 142 if (defaultRuleChainId != null) {
132 143 deviceProfile.setDefaultRuleChainId(new RuleChainId(defaultRuleChainId));
133 144 }
  145 + deviceProfile.setProvisionDeviceKey(provisionDeviceKey);
134 146 return deviceProfile;
135 147 }
136 148 }
... ...
... ... @@ -20,7 +20,6 @@ import org.springframework.data.domain.Pageable;
20 20 import org.springframework.data.jpa.repository.Query;
21 21 import org.springframework.data.repository.PagingAndSortingRepository;
22 22 import org.springframework.data.repository.query.Param;
23   -import org.thingsboard.server.common.data.DeviceProfile;
24 23 import org.thingsboard.server.common.data.DeviceProfileInfo;
25 24 import org.thingsboard.server.dao.model.sql.DeviceProfileEntity;
26 25
... ... @@ -57,4 +56,5 @@ public interface DeviceProfileRepository extends PagingAndSortingRepository<Devi
57 56
58 57 DeviceProfileEntity findByTenantIdAndName(UUID id, String profileName);
59 58
  59 + DeviceProfileEntity findByProvisionDeviceKey(@Param("provisionDeviceKey") String provisionDeviceKey);
60 60 }
... ...
... ... @@ -168,5 +168,4 @@ public interface DeviceRepository extends PagingAndSortingRepository<DeviceEntit
168 168 DeviceEntity findByTenantIdAndId(UUID tenantId, UUID id);
169 169
170 170 Long countByDeviceProfileId(UUID deviceProfileId);
171   -
172 171 }
... ...
... ... @@ -81,6 +81,11 @@ public class JpaDeviceProfileDao extends JpaAbstractSearchTextDao<DeviceProfileE
81 81 }
82 82
83 83 @Override
  84 + public DeviceProfile findByProvisionDeviceKey(String provisionDeviceKey) {
  85 + return DaoUtil.getData(deviceProfileRepository.findByProvisionDeviceKey(provisionDeviceKey));
  86 + }
  87 +
  88 + @Override
84 89 public DeviceProfile findByName(TenantId tenantId, String profileName) {
85 90 return DaoUtil.getData(deviceProfileRepository.findByTenantIdAndName(tenantId.getId(), profileName));
86 91 }
... ...
... ... @@ -152,13 +152,16 @@ CREATE TABLE IF NOT EXISTS device_profile (
152 152 name varchar(255),
153 153 type varchar(255),
154 154 transport_type varchar(255),
  155 + provision_type varchar(255),
155 156 profile_data jsonb,
156 157 description varchar,
157 158 search_text varchar(255),
158 159 is_default boolean,
159 160 tenant_id uuid,
160 161 default_rule_chain_id uuid,
  162 + provision_device_key varchar,
161 163 CONSTRAINT device_profile_name_unq_key UNIQUE (tenant_id, name),
  164 + CONSTRAINT device_provision_key_unq_key UNIQUE (provision_device_key),
162 165 CONSTRAINT fk_default_rule_chain_device_profile FOREIGN KEY (default_rule_chain_id) REFERENCES rule_chain(id)
163 166 );
164 167
... ...
... ... @@ -170,13 +170,16 @@ CREATE TABLE IF NOT EXISTS device_profile (
170 170 name varchar(255),
171 171 type varchar(255),
172 172 transport_type varchar(255),
  173 + provision_type varchar(255),
173 174 profile_data jsonb,
174 175 description varchar,
175 176 search_text varchar(255),
176 177 is_default boolean,
177 178 tenant_id uuid,
178 179 default_rule_chain_id uuid,
  180 + provision_device_key varchar,
179 181 CONSTRAINT device_profile_name_unq_key UNIQUE (tenant_id, name),
  182 + CONSTRAINT device_provision_key_unq_key UNIQUE (provision_device_key),
180 183 CONSTRAINT fk_default_rule_chain_device_profile FOREIGN KEY (default_rule_chain_id) REFERENCES rule_chain(id)
181 184 );
182 185
... ...