Commit 969a686c74fd33269500227685dfb8834f3bcf59

Authored by Igor Kulikov
2 parents d9321b48 a3326b44

Merge branch 'develop/3.2' of github.com:thingsboard/thingsboard into develop/3.2

... ... @@ -23,12 +23,14 @@ 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;
26 27 import org.springframework.stereotype.Service;
27 28 import org.springframework.util.StringUtils;
28 29 import org.thingsboard.server.common.data.DataConstants;
29 30 import org.thingsboard.server.common.data.Device;
30 31 import org.thingsboard.server.common.data.DeviceProfile;
31 32 import org.thingsboard.server.common.data.TenantProfile;
  33 +import org.thingsboard.server.common.data.device.credentials.BasicMqttCredentials;
32 34 import org.thingsboard.server.common.data.id.CustomerId;
33 35 import org.thingsboard.server.common.data.id.DeviceId;
34 36 import org.thingsboard.server.common.data.id.DeviceProfileId;
... ... @@ -36,6 +38,7 @@ import org.thingsboard.server.common.data.id.TenantId;
36 38 import org.thingsboard.server.common.data.relation.EntityRelation;
37 39 import org.thingsboard.server.common.data.security.DeviceCredentials;
38 40 import org.thingsboard.server.common.data.security.DeviceCredentialsType;
  41 +import org.thingsboard.server.common.msg.EncryptionUtil;
39 42 import org.thingsboard.server.common.msg.TbMsg;
40 43 import org.thingsboard.server.common.msg.TbMsgDataType;
41 44 import org.thingsboard.server.common.msg.TbMsgMetaData;
... ... @@ -46,6 +49,7 @@ import org.thingsboard.server.dao.device.DeviceService;
46 49 import org.thingsboard.server.dao.relation.RelationService;
47 50 import org.thingsboard.server.dao.tenant.TenantProfileService;
48 51 import org.thingsboard.server.dao.tenant.TenantService;
  52 +import org.thingsboard.server.dao.util.mapping.JacksonUtil;
49 53 import org.thingsboard.server.gen.transport.TransportProtos;
50 54 import org.thingsboard.server.gen.transport.TransportProtos.DeviceInfoProto;
51 55 import org.thingsboard.server.gen.transport.TransportProtos.GetOrCreateDeviceFromGatewayRequestMsg;
... ... @@ -91,6 +95,7 @@ public class DefaultTransportApiService implements TransportApiService {
91 95 private final TbClusterService tbClusterService;
92 96 private final DataDecodingEncodingService dataDecodingEncodingService;
93 97
  98 +
94 99 private final ConcurrentMap<String, ReentrantLock> deviceCreationLocks = new ConcurrentHashMap<>();
95 100
96 101 public DefaultTransportApiService(DeviceProfileService deviceProfileService, TenantService tenantService,
... ... @@ -117,6 +122,10 @@ public class DefaultTransportApiService implements TransportApiService {
117 122 ValidateDeviceTokenRequestMsg msg = transportApiRequestMsg.getValidateTokenRequestMsg();
118 123 return Futures.transform(validateCredentials(msg.getToken(), DeviceCredentialsType.ACCESS_TOKEN),
119 124 value -> new TbProtoQueueMsg<>(tbProtoQueueMsg.getKey(), value, tbProtoQueueMsg.getHeaders()), MoreExecutors.directExecutor());
  125 + } else if (transportApiRequestMsg.hasValidateBasicMqttCredRequestMsg()) {
  126 + TransportProtos.ValidateBasicMqttCredRequestMsg msg = transportApiRequestMsg.getValidateBasicMqttCredRequestMsg();
  127 + return Futures.transform(validateCredentials(msg),
  128 + value -> new TbProtoQueueMsg<>(tbProtoQueueMsg.getKey(), value, tbProtoQueueMsg.getHeaders()), MoreExecutors.directExecutor());
120 129 } else if (transportApiRequestMsg.hasValidateX509CertRequestMsg()) {
121 130 ValidateDeviceX509CertRequestMsg msg = transportApiRequestMsg.getValidateX509CertRequestMsg();
122 131 return Futures.transform(validateCredentials(msg.getHash(), DeviceCredentialsType.X509_CERTIFICATE),
... ... @@ -130,7 +139,6 @@ public class DefaultTransportApiService implements TransportApiService {
130 139 } else if (transportApiRequestMsg.hasGetDeviceProfileRequestMsg()) {
131 140 return Futures.transform(handle(transportApiRequestMsg.getGetDeviceProfileRequestMsg()),
132 141 value -> new TbProtoQueueMsg<>(tbProtoQueueMsg.getKey(), value, tbProtoQueueMsg.getHeaders()), MoreExecutors.directExecutor());
133   -
134 142 }
135 143 return Futures.transform(getEmptyTransportApiResponseFuture(),
136 144 value -> new TbProtoQueueMsg<>(tbProtoQueueMsg.getKey(), value, tbProtoQueueMsg.getHeaders()), MoreExecutors.directExecutor());
... ... @@ -146,6 +154,62 @@ public class DefaultTransportApiService implements TransportApiService {
146 154 }
147 155 }
148 156
  157 + private ListenableFuture<TransportApiResponseMsg> validateCredentials(TransportProtos.ValidateBasicMqttCredRequestMsg mqtt) {
  158 + DeviceCredentials credentials = deviceCredentialsService.findDeviceCredentialsByCredentialsId(mqtt.getUserName());
  159 + if (credentials != null) {
  160 + if (credentials.getCredentialsType() == DeviceCredentialsType.ACCESS_TOKEN) {
  161 + return getDeviceInfo(credentials.getDeviceId(), credentials);
  162 + } else if (credentials.getCredentialsType() == DeviceCredentialsType.MQTT_BASIC) {
  163 + if (!checkMqttCredentials(mqtt, credentials)) {
  164 + credentials = null;
  165 + }
  166 + }
  167 + }
  168 + if (credentials == null) {
  169 + credentials = checkMqttCredentials(mqtt, EncryptionUtil.getSha3Hash("|", mqtt.getClientId(), mqtt.getUserName()));
  170 + if (credentials == null) {
  171 + credentials = checkMqttCredentials(mqtt, EncryptionUtil.getSha3Hash(mqtt.getClientId()));
  172 + }
  173 + }
  174 + if (credentials != null) {
  175 + return getDeviceInfo(credentials.getDeviceId(), credentials);
  176 + } else {
  177 + return getEmptyTransportApiResponseFuture();
  178 + }
  179 + }
  180 +
  181 + private DeviceCredentials checkMqttCredentials(TransportProtos.ValidateBasicMqttCredRequestMsg clientCred, String credId) {
  182 + DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByCredentialsId(credId);
  183 + if (deviceCredentials != null && deviceCredentials.getCredentialsType() == DeviceCredentialsType.MQTT_BASIC) {
  184 + if (!checkMqttCredentials(clientCred, deviceCredentials)) {
  185 + return null;
  186 + } else {
  187 + return deviceCredentials;
  188 + }
  189 + }
  190 + return null;
  191 + }
  192 +
  193 + private boolean checkMqttCredentials(TransportProtos.ValidateBasicMqttCredRequestMsg clientCred, DeviceCredentials deviceCredentials) {
  194 + BasicMqttCredentials dbCred = JacksonUtil.fromString(deviceCredentials.getCredentialsValue(), BasicMqttCredentials.class);
  195 + if (!StringUtils.isEmpty(dbCred.getClientId()) && !dbCred.getClientId().equals(clientCred.getClientId())) {
  196 + return false;
  197 + }
  198 + if (!StringUtils.isEmpty(dbCred.getUserName()) && !dbCred.getUserName().equals(clientCred.getUserName())) {
  199 + return false;
  200 + }
  201 + if (!StringUtils.isEmpty(dbCred.getPassword())) {
  202 + if (StringUtils.isEmpty(clientCred.getPassword())) {
  203 + return false;
  204 + } else {
  205 + if (!dbCred.getPassword().equals(clientCred.getPassword())) {
  206 + return false;
  207 + }
  208 + }
  209 + }
  210 + return true;
  211 + }
  212 +
149 213 private ListenableFuture<TransportApiResponseMsg> handle(GetOrCreateDeviceFromGatewayRequestMsg requestMsg) {
150 214 DeviceId gatewayId = new DeviceId(new UUID(requestMsg.getGatewayIdMSB(), requestMsg.getGatewayIdLSB()));
151 215 ListenableFuture<Device> gatewayFuture = deviceService.findDeviceByIdAsync(TenantId.SYS_TENANT_ID, gatewayId);
... ... @@ -237,7 +301,7 @@ public class DefaultTransportApiService implements TransportApiService {
237 301 builder.setCredentialsBody(credentials.getCredentialsValue());
238 302 }
239 303 return TransportApiResponseMsg.newBuilder()
240   - .setValidateTokenResponseMsg(builder.build()).build();
  304 + .setValidateCredResponseMsg(builder.build()).build();
241 305 } catch (JsonProcessingException e) {
242 306 log.warn("[{}] Failed to lookup device by id", deviceId, e);
243 307 return getEmptyTransportApiResponse();
... ... @@ -265,6 +329,6 @@ public class DefaultTransportApiService implements TransportApiService {
265 329
266 330 private TransportApiResponseMsg getEmptyTransportApiResponse() {
267 331 return TransportApiResponseMsg.newBuilder()
268   - .setValidateTokenResponseMsg(ValidateDeviceCredentialsResponseMsg.getDefaultInstance()).build();
  332 + .setValidateCredResponseMsg(ValidateDeviceCredentialsResponseMsg.getDefaultInstance()).build();
269 333 }
270 334 }
... ...
  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.credentials;
  17 +
  18 +import lombok.Data;
  19 +
  20 +@Data
  21 +public class BasicMqttCredentials {
  22 +
  23 + private String clientId;
  24 + private String userName;
  25 + private String password;
  26 +
  27 +}
... ...
... ... @@ -18,6 +18,7 @@ package org.thingsboard.server.common.msg;
18 18 import lombok.extern.slf4j.Slf4j;
19 19 import org.bouncycastle.crypto.digests.SHA3Digest;
20 20 import org.bouncycastle.pqc.math.linearalgebra.ByteUtils;
  21 +
21 22 /**
22 23 * @author Valerii Sosliuk
23 24 */
... ... @@ -30,8 +31,8 @@ public class EncryptionUtil {
30 31 public static String trimNewLines(String input) {
31 32 return input.replaceAll("-----BEGIN CERTIFICATE-----", "")
32 33 .replaceAll("-----END CERTIFICATE-----", "")
33   - .replaceAll("\n","")
34   - .replaceAll("\r","");
  34 + .replaceAll("\n", "")
  35 + .replaceAll("\r", "");
35 36 }
36 37
37 38 public static String getSha3Hash(String data) {
... ... @@ -45,4 +46,20 @@ public class EncryptionUtil {
45 46 String sha3Hash = ByteUtils.toHexString(hashedBytes);
46 47 return sha3Hash;
47 48 }
  49 +
  50 + public static String getSha3Hash(String delim, String... tokens) {
  51 + StringBuilder sb = new StringBuilder();
  52 + boolean first = true;
  53 + for (String token : tokens) {
  54 + if (token != null && !token.isEmpty()) {
  55 + if (first) {
  56 + first = false;
  57 + } else {
  58 + sb.append(delim);
  59 + }
  60 + sb.append(token);
  61 + }
  62 + }
  63 + return getSha3Hash(sb.toString());
  64 + }
48 65 }
... ...
... ... @@ -147,6 +147,12 @@ message ValidateDeviceX509CertRequestMsg {
147 147 string hash = 1;
148 148 }
149 149
  150 +message ValidateBasicMqttCredRequestMsg {
  151 + string clientId = 1;
  152 + string userName = 2;
  153 + string password = 3;
  154 +}
  155 +
150 156 message ValidateDeviceCredentialsResponseMsg {
151 157 DeviceInfoProto deviceInfo = 1;
152 158 string credentialsBody = 2;
... ... @@ -429,11 +435,12 @@ message TransportApiRequestMsg {
429 435 GetOrCreateDeviceFromGatewayRequestMsg getOrCreateDeviceRequestMsg = 3;
430 436 GetTenantRoutingInfoRequestMsg getTenantRoutingInfoRequestMsg = 4;
431 437 GetDeviceProfileRequestMsg getDeviceProfileRequestMsg = 5;
  438 + ValidateBasicMqttCredRequestMsg validateBasicMqttCredRequestMsg = 6;
432 439 }
433 440
434 441 /* Response from ThingsBoard Core Service to Transport Service */
435 442 message TransportApiResponseMsg {
436   - ValidateDeviceCredentialsResponseMsg validateTokenResponseMsg = 1;
  443 + ValidateDeviceCredentialsResponseMsg validateCredResponseMsg = 1;
437 444 GetOrCreateDeviceFromGatewayResponseMsg getOrCreateDeviceResponseMsg = 2;
438 445 GetTenantRoutingInfoResponseMsg getTenantRoutingInfoResponseMsg = 4;
439 446 GetDeviceProfileResponseMsg getDeviceProfileResponseMsg = 5;
... ...
... ... @@ -66,6 +66,7 @@ import javax.net.ssl.SSLPeerUnverifiedException;
66 66 import javax.security.cert.X509Certificate;
67 67 import java.io.IOException;
68 68 import java.net.InetSocketAddress;
  69 +import java.nio.charset.StandardCharsets;
69 70 import java.util.ArrayList;
70 71 import java.util.List;
71 72 import java.util.UUID;
... ... @@ -365,25 +366,27 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
365 366 private void processAuthTokenConnect(ChannelHandlerContext ctx, MqttConnectMessage msg) {
366 367 String userName = msg.payload().userName();
367 368 log.info("[{}] Processing connect msg for client with user name: {}!", sessionId, userName);
368   - if (StringUtils.isEmpty(userName)) {
369   - ctx.writeAndFlush(createMqttConnAckMsg(CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD));
370   - ctx.close();
371   - } else {
372   - transportService.process(DeviceTransportType.MQTT, ValidateDeviceTokenRequestMsg.newBuilder().setToken(userName).build(),
373   - new TransportServiceCallback<ValidateDeviceCredentialsResponse>() {
374   - @Override
375   - public void onSuccess(ValidateDeviceCredentialsResponse msg) {
376   - onValidateDeviceResponse(msg, ctx);
377   - }
378   -
379   - @Override
380   - public void onError(Throwable e) {
381   - log.trace("[{}] Failed to process credentials: {}", address, userName, e);
382   - ctx.writeAndFlush(createMqttConnAckMsg(MqttConnectReturnCode.CONNECTION_REFUSED_SERVER_UNAVAILABLE));
383   - ctx.close();
384   - }
385   - });
  369 + TransportProtos.ValidateBasicMqttCredRequestMsg.Builder request = TransportProtos.ValidateBasicMqttCredRequestMsg.newBuilder()
  370 + .setClientId(msg.payload().clientIdentifier())
  371 + .setUserName(userName);
  372 + String password = msg.payload().password();
  373 + if (password != null) {
  374 + request.setPassword(password);
386 375 }
  376 + transportService.process(DeviceTransportType.MQTT, request.build(),
  377 + new TransportServiceCallback<ValidateDeviceCredentialsResponse>() {
  378 + @Override
  379 + public void onSuccess(ValidateDeviceCredentialsResponse msg) {
  380 + onValidateDeviceResponse(msg, ctx);
  381 + }
  382 +
  383 + @Override
  384 + public void onError(Throwable e) {
  385 + log.trace("[{}] Failed to process credentials: {}", address, userName, e);
  386 + ctx.writeAndFlush(createMqttConnAckMsg(MqttConnectReturnCode.CONNECTION_REFUSED_SERVER_UNAVAILABLE));
  387 + ctx.close();
  388 + }
  389 + });
387 390 }
388 391
389 392 private void processX509CertConnect(ChannelHandlerContext ctx, X509Certificate cert) {
... ...
... ... @@ -23,7 +23,6 @@ import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsRes
23 23 import org.thingsboard.server.gen.transport.TransportProtos.ClaimDeviceMsg;
24 24 import org.thingsboard.server.gen.transport.TransportProtos.GetAttributeRequestMsg;
25 25 import org.thingsboard.server.gen.transport.TransportProtos.GetOrCreateDeviceFromGatewayRequestMsg;
26   -import org.thingsboard.server.gen.transport.TransportProtos.GetOrCreateDeviceFromGatewayResponseMsg;
27 26 import org.thingsboard.server.gen.transport.TransportProtos.GetTenantRoutingInfoRequestMsg;
28 27 import org.thingsboard.server.gen.transport.TransportProtos.GetTenantRoutingInfoResponseMsg;
29 28 import org.thingsboard.server.gen.transport.TransportProtos.PostAttributeMsg;
... ... @@ -35,7 +34,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.SubscribeToRPCMsg;
35 34 import org.thingsboard.server.gen.transport.TransportProtos.SubscriptionInfoProto;
36 35 import org.thingsboard.server.gen.transport.TransportProtos.ToDeviceRpcResponseMsg;
37 36 import org.thingsboard.server.gen.transport.TransportProtos.ToServerRpcRequestMsg;
38   -import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceCredentialsResponseMsg;
  37 +import org.thingsboard.server.gen.transport.TransportProtos.ValidateBasicMqttCredRequestMsg;
39 38 import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceTokenRequestMsg;
40 39 import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceX509CertRequestMsg;
41 40
... ... @@ -49,6 +48,9 @@ public interface TransportService {
49 48 void process(DeviceTransportType transportType, ValidateDeviceTokenRequestMsg msg,
50 49 TransportServiceCallback<ValidateDeviceCredentialsResponse> callback);
51 50
  51 + void process(DeviceTransportType transportType, ValidateBasicMqttCredRequestMsg msg,
  52 + TransportServiceCallback<ValidateDeviceCredentialsResponse> callback);
  53 +
52 54 void process(DeviceTransportType transportType, ValidateDeviceX509CertRequestMsg msg,
53 55 TransportServiceCallback<ValidateDeviceCredentialsResponse> callback);
54 56
... ...
... ... @@ -252,9 +252,20 @@ public class DefaultTransportService implements TransportService {
252 252 }
253 253
254 254 @Override
255   - public void process(DeviceTransportType transportType, TransportProtos.ValidateDeviceTokenRequestMsg msg, TransportServiceCallback<ValidateDeviceCredentialsResponse> callback) {
  255 + public void process(DeviceTransportType transportType, TransportProtos.ValidateDeviceTokenRequestMsg msg,
  256 + TransportServiceCallback<ValidateDeviceCredentialsResponse> callback) {
256 257 log.trace("Processing msg: {}", msg);
257   - TbProtoQueueMsg<TransportApiRequestMsg> protoMsg = new TbProtoQueueMsg<>(UUID.randomUUID(), TransportApiRequestMsg.newBuilder().setValidateTokenRequestMsg(msg).build());
  258 + TbProtoQueueMsg<TransportApiRequestMsg> protoMsg = new TbProtoQueueMsg<>(UUID.randomUUID(),
  259 + TransportApiRequestMsg.newBuilder().setValidateTokenRequestMsg(msg).build());
  260 + doProcess(transportType, protoMsg, callback);
  261 + }
  262 +
  263 + @Override
  264 + public void process(DeviceTransportType transportType, TransportProtos.ValidateBasicMqttCredRequestMsg msg,
  265 + TransportServiceCallback<ValidateDeviceCredentialsResponse> callback) {
  266 + log.trace("Processing msg: {}", msg);
  267 + TbProtoQueueMsg<TransportApiRequestMsg> protoMsg = new TbProtoQueueMsg<>(UUID.randomUUID(),
  268 + TransportApiRequestMsg.newBuilder().setValidateBasicMqttCredRequestMsg(msg).build());
258 269 doProcess(transportType, protoMsg, callback);
259 270 }
260 271
... ... @@ -265,9 +276,10 @@ public class DefaultTransportService implements TransportService {
265 276 doProcess(transportType, protoMsg, callback);
266 277 }
267 278
268   - private void doProcess(DeviceTransportType transportType, TbProtoQueueMsg<TransportApiRequestMsg> protoMsg, TransportServiceCallback<ValidateDeviceCredentialsResponse> callback) {
  279 + private void doProcess(DeviceTransportType transportType, TbProtoQueueMsg<TransportApiRequestMsg> protoMsg,
  280 + TransportServiceCallback<ValidateDeviceCredentialsResponse> callback) {
269 281 ListenableFuture<ValidateDeviceCredentialsResponse> response = Futures.transform(transportApiRequestTemplate.send(protoMsg), tmp -> {
270   - TransportProtos.ValidateDeviceCredentialsResponseMsg msg = tmp.getValue().getValidateTokenResponseMsg();
  282 + TransportProtos.ValidateDeviceCredentialsResponseMsg msg = tmp.getValue().getValidateCredResponseMsg();
271 283 ValidateDeviceCredentialsResponse.ValidateDeviceCredentialsResponseBuilder result = ValidateDeviceCredentialsResponse.builder();
272 284 if (msg.hasDeviceInfo()) {
273 285 result.credentials(msg.getCredentialsBody());
... ...
... ... @@ -21,18 +21,20 @@ import org.hibernate.exception.ConstraintViolationException;
21 21 import org.springframework.beans.factory.annotation.Autowired;
22 22 import org.springframework.cache.annotation.CacheEvict;
23 23 import org.springframework.cache.annotation.Cacheable;
  24 +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
24 25 import org.springframework.stereotype.Service;
25 26 import org.springframework.util.StringUtils;
26 27 import org.thingsboard.server.common.data.Device;
  28 +import org.thingsboard.server.common.data.device.credentials.BasicMqttCredentials;
27 29 import org.thingsboard.server.common.data.id.DeviceId;
28 30 import org.thingsboard.server.common.data.id.EntityId;
29 31 import org.thingsboard.server.common.data.id.TenantId;
30 32 import org.thingsboard.server.common.data.security.DeviceCredentials;
31   -import org.thingsboard.server.common.data.security.DeviceCredentialsType;
32 33 import org.thingsboard.server.common.msg.EncryptionUtil;
33 34 import org.thingsboard.server.dao.entity.AbstractEntityService;
34 35 import org.thingsboard.server.dao.exception.DataValidationException;
35 36 import org.thingsboard.server.dao.service.DataValidator;
  37 +import org.thingsboard.server.dao.util.mapping.JacksonUtil;
36 38
37 39 import static org.thingsboard.server.common.data.CacheConstants.DEVICE_CREDENTIALS_CACHE;
38 40 import static org.thingsboard.server.dao.service.Validator.validateId;
... ... @@ -75,8 +77,16 @@ public class DeviceCredentialsServiceImpl extends AbstractEntityService implemen
75 77 }
76 78
77 79 private DeviceCredentials saveOrUpdate(TenantId tenantId, DeviceCredentials deviceCredentials) {
78   - if (deviceCredentials.getCredentialsType() == DeviceCredentialsType.X509_CERTIFICATE) {
79   - formatCertData(deviceCredentials);
  80 + if(deviceCredentials.getCredentialsType() == null){
  81 + throw new DataValidationException("Device credentials type should be specified");
  82 + }
  83 + switch (deviceCredentials.getCredentialsType()) {
  84 + case X509_CERTIFICATE:
  85 + formatCertData(deviceCredentials);
  86 + break;
  87 + case MQTT_BASIC:
  88 + formatSimpleMqttCredentials(deviceCredentials);
  89 + break;
80 90 }
81 91 log.trace("Executing updateDeviceCredentials [{}]", deviceCredentials);
82 92 credentialsValidator.validate(deviceCredentials, id -> tenantId);
... ... @@ -93,6 +103,32 @@ public class DeviceCredentialsServiceImpl extends AbstractEntityService implemen
93 103 }
94 104 }
95 105
  106 + private void formatSimpleMqttCredentials(DeviceCredentials deviceCredentials) {
  107 + BasicMqttCredentials mqttCredentials;
  108 + try {
  109 + mqttCredentials = JacksonUtil.fromString(deviceCredentials.getCredentialsValue(), BasicMqttCredentials.class);
  110 + if (mqttCredentials == null) {
  111 + throw new IllegalArgumentException();
  112 + }
  113 + } catch (IllegalArgumentException e) {
  114 + throw new DataValidationException("Invalid credentials body for simple mqtt credentials!");
  115 + }
  116 + if (StringUtils.isEmpty(mqttCredentials.getClientId()) && StringUtils.isEmpty(mqttCredentials.getUserName())) {
  117 + throw new DataValidationException("Both mqtt client id and user name are empty!");
  118 + }
  119 + if (StringUtils.isEmpty(mqttCredentials.getClientId())) {
  120 + deviceCredentials.setCredentialsId(mqttCredentials.getUserName());
  121 + } else if (StringUtils.isEmpty(mqttCredentials.getUserName())) {
  122 + deviceCredentials.setCredentialsId(EncryptionUtil.getSha3Hash(mqttCredentials.getClientId()));
  123 + } else {
  124 + deviceCredentials.setCredentialsId(EncryptionUtil.getSha3Hash("|", mqttCredentials.getClientId(), mqttCredentials.getUserName()));
  125 + }
  126 + if (!StringUtils.isEmpty(mqttCredentials.getPassword())) {
  127 + mqttCredentials.setPassword(mqttCredentials.getPassword());
  128 + }
  129 + deviceCredentials.setCredentialsValue(JacksonUtil.toString(mqttCredentials));
  130 + }
  131 +
96 132 private void formatCertData(DeviceCredentials deviceCredentials) {
97 133 String cert = EncryptionUtil.trimNewLines(deviceCredentials.getCredentialsValue());
98 134 String sha3Hash = EncryptionUtil.getSha3Hash(cert);
... ...
... ... @@ -58,6 +58,37 @@
58 58 {{ 'device.rsa-key-required' | translate }}
59 59 </mat-error>
60 60 </mat-form-field>
  61 + <section *ngIf="deviceCredentialsFormGroup.get('credentialsType').value === deviceCredentialsType.MQTT_BASIC" formGroupName="credentialsBasic">
  62 + <mat-form-field class="mat-block">
  63 + <mat-label translate>device.client-id</mat-label>
  64 + <input matInput formControlName="clientId">
  65 + <mat-error *ngIf="deviceCredentialsFormGroup.get('credentialsBasic.clientId').hasError('pattern')">
  66 + {{ 'device.client-id-pattern' | translate }}
  67 + </mat-error>
  68 + </mat-form-field>
  69 + <mat-form-field class="mat-block">
  70 + <mat-label translate>device.user-name</mat-label>
  71 + <input matInput formControlName="userName" [required]="!!deviceCredentialsFormGroup.get('credentialsBasic.password').value">
  72 + <mat-error *ngIf="deviceCredentialsFormGroup.get('credentialsBasic.userName').hasError('required')">
  73 + {{ 'device.user-name-required' | translate }}
  74 + </mat-error>
  75 + </mat-form-field>
  76 + <mat-form-field class="mat-block">
  77 + <mat-label translate>device.password</mat-label>
  78 + <input matInput formControlName="password"
  79 + autocomplete="new-password"
  80 + (ngModelChange)="passwordChanged()"
  81 + [type]="hidePassword ? 'password' : 'text'">
  82 + <button mat-icon-button matSuffix type="button"
  83 + (click)="hidePassword = !hidePassword"
  84 + [attr.aria-pressed]="hidePassword">
  85 + <mat-icon>{{hidePassword ? 'visibility_off' : 'visibility'}}</mat-icon>
  86 + </button>
  87 + </mat-form-field>
  88 + <mat-error *ngIf="deviceCredentialsFormGroup.get('credentialsBasic').hasError('atLeastOne')">
  89 + {{ 'device.client-id-or-user-name-necessary' | translate }}
  90 + </mat-error>
  91 + </section>
61 92 </fieldset>
62 93 </div>
63 94 <div mat-dialog-actions fxLayoutAlign="end center">
... ...
... ... @@ -19,9 +19,23 @@ import { ErrorStateMatcher } from '@angular/material/core';
19 19 import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
20 20 import { Store } from '@ngrx/store';
21 21 import { AppState } from '@core/core.state';
22   -import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms';
  22 +import {
  23 + FormBuilder,
  24 + FormControl,
  25 + FormGroup,
  26 + FormGroupDirective,
  27 + NgForm,
  28 + ValidationErrors,
  29 + ValidatorFn,
  30 + Validators
  31 +} from '@angular/forms';
23 32 import { DeviceService } from '@core/http/device.service';
24   -import { credentialTypeNames, DeviceCredentials, DeviceCredentialsType } from '@shared/models/device.models';
  33 +import {
  34 + credentialTypeNames,
  35 + DeviceCredentialMQTTBasic,
  36 + DeviceCredentials,
  37 + DeviceCredentialsType
  38 +} from '@shared/models/device.models';
25 39 import { DialogComponent } from '@shared/components/dialog.component';
26 40 import { Router } from '@angular/router';
27 41
... ... @@ -53,6 +67,8 @@ export class DeviceCredentialsDialogComponent extends
53 67
54 68 credentialTypeNamesMap = credentialTypeNames;
55 69
  70 + hidePassword = true;
  71 +
56 72 constructor(protected store: Store<AppState>,
57 73 protected router: Router,
58 74 @Inject(MAT_DIALOG_DATA) public data: DeviceCredentialsDialogData,
... ... @@ -69,7 +85,12 @@ export class DeviceCredentialsDialogComponent extends
69 85 this.deviceCredentialsFormGroup = this.fb.group({
70 86 credentialsType: [DeviceCredentialsType.ACCESS_TOKEN],
71 87 credentialsId: [''],
72   - credentialsValue: ['']
  88 + credentialsValue: [''],
  89 + credentialsBasic: this.fb.group({
  90 + clientId: ['', [Validators.pattern(/^[A-Za-z0-9]+$/)]],
  91 + userName: [''],
  92 + password: ['']
  93 + }, {validators: this.atLeastOne(Validators.required, ['clientId', 'userName'])})
73 94 });
74 95 if (this.isReadOnly) {
75 96 this.deviceCredentialsFormGroup.disable({emitEvent: false});
... ... @@ -89,10 +110,17 @@ export class DeviceCredentialsDialogComponent extends
89 110 this.deviceService.getDeviceCredentials(this.data.deviceId).subscribe(
90 111 (deviceCredentials) => {
91 112 this.deviceCredentials = deviceCredentials;
  113 + let credentialsValue = deviceCredentials.credentialsValue;
  114 + let credentialsBasic = {clientId: null, userName: null, password: null};
  115 + if (deviceCredentials.credentialsType === DeviceCredentialsType.MQTT_BASIC) {
  116 + credentialsValue = null;
  117 + credentialsBasic = JSON.parse(deviceCredentials.credentialsValue) as DeviceCredentialMQTTBasic;
  118 + }
92 119 this.deviceCredentialsFormGroup.patchValue({
93 120 credentialsType: deviceCredentials.credentialsType,
94 121 credentialsId: deviceCredentials.credentialsId,
95   - credentialsValue: deviceCredentials.credentialsValue
  122 + credentialsValue,
  123 + credentialsBasic
96 124 });
97 125 this.updateValidators();
98 126 }
... ... @@ -100,12 +128,16 @@ export class DeviceCredentialsDialogComponent extends
100 128 }
101 129
102 130 credentialsTypeChanged(): void {
103   - this.deviceCredentialsFormGroup.patchValue(
104   - {credentialsId: null, credentialsValue: null}, {emitEvent: true});
  131 + this.deviceCredentialsFormGroup.patchValue({
  132 + credentialsId: null,
  133 + credentialsValue: null,
  134 + credentialsBasic: {clientId: '', userName: '', password: ''}
  135 + }, {emitEvent: true});
105 136 this.updateValidators();
106 137 }
107 138
108 139 updateValidators(): void {
  140 + this.hidePassword = true;
109 141 const crendetialsType = this.deviceCredentialsFormGroup.get('credentialsType').value as DeviceCredentialsType;
110 142 switch (crendetialsType) {
111 143 case DeviceCredentialsType.ACCESS_TOKEN:
... ... @@ -113,27 +145,66 @@ export class DeviceCredentialsDialogComponent extends
113 145 this.deviceCredentialsFormGroup.get('credentialsId').updateValueAndValidity();
114 146 this.deviceCredentialsFormGroup.get('credentialsValue').setValidators([]);
115 147 this.deviceCredentialsFormGroup.get('credentialsValue').updateValueAndValidity();
  148 + this.deviceCredentialsFormGroup.get('credentialsBasic').disable();
116 149 break;
117 150 case DeviceCredentialsType.X509_CERTIFICATE:
118 151 this.deviceCredentialsFormGroup.get('credentialsValue').setValidators([Validators.required]);
119 152 this.deviceCredentialsFormGroup.get('credentialsValue').updateValueAndValidity();
120 153 this.deviceCredentialsFormGroup.get('credentialsId').setValidators([]);
121 154 this.deviceCredentialsFormGroup.get('credentialsId').updateValueAndValidity();
  155 + this.deviceCredentialsFormGroup.get('credentialsBasic').disable();
122 156 break;
  157 + case DeviceCredentialsType.MQTT_BASIC:
  158 + this.deviceCredentialsFormGroup.get('credentialsBasic').enable();
  159 + this.deviceCredentialsFormGroup.get('credentialsBasic').updateValueAndValidity();
  160 + this.deviceCredentialsFormGroup.get('credentialsId').setValidators([]);
  161 + this.deviceCredentialsFormGroup.get('credentialsId').updateValueAndValidity();
  162 + this.deviceCredentialsFormGroup.get('credentialsValue').setValidators([]);
  163 + this.deviceCredentialsFormGroup.get('credentialsValue').updateValueAndValidity();
123 164 }
124 165 }
125 166
  167 + private atLeastOne(validator: ValidatorFn, controls: string[] = null) {
  168 + return (group: FormGroup): ValidationErrors | null => {
  169 + if (!controls) {
  170 + controls = Object.keys(group.controls);
  171 + }
  172 + const hasAtLeastOne = group?.controls && controls.some(k => !validator(group.controls[k]));
  173 +
  174 + return hasAtLeastOne ? null : {atLeastOne: true};
  175 + };
  176 + }
  177 +
126 178 cancel(): void {
127 179 this.dialogRef.close(null);
128 180 }
129 181
130 182 save(): void {
131 183 this.submitted = true;
132   - this.deviceCredentials = {...this.deviceCredentials, ...this.deviceCredentialsFormGroup.value};
  184 + const deviceCredentialsValue = this.deviceCredentialsFormGroup.value;
  185 + if (deviceCredentialsValue.credentialsType === DeviceCredentialsType.MQTT_BASIC) {
  186 + deviceCredentialsValue.credentialsValue = JSON.stringify(deviceCredentialsValue.credentialsBasic);
  187 + }
  188 + delete deviceCredentialsValue.credentialsBasic;
  189 + this.deviceCredentials = {...this.deviceCredentials, ...deviceCredentialsValue};
133 190 this.deviceService.saveDeviceCredentials(this.deviceCredentials).subscribe(
134 191 (deviceCredentials) => {
135 192 this.dialogRef.close(deviceCredentials);
136 193 }
137 194 );
138 195 }
  196 +
  197 + passwordChanged() {
  198 + const value = this.deviceCredentialsFormGroup.get('credentialsBasic.password').value;
  199 + if (value !== '') {
  200 + this.deviceCredentialsFormGroup.get('credentialsBasic.userName').setValidators([Validators.required]);
  201 + if (this.deviceCredentialsFormGroup.get('credentialsBasic.userName').untouched) {
  202 + this.deviceCredentialsFormGroup.get('credentialsBasic.userName').markAsTouched({onlySelf: true});
  203 + }
  204 + this.deviceCredentialsFormGroup.get('credentialsBasic.userName').updateValueAndValidity();
  205 + } else {
  206 + this.deviceCredentialsFormGroup.get('credentialsBasic.userName').setValidators([]);
  207 + this.deviceCredentialsFormGroup.get('credentialsBasic.userName').updateValueAndValidity();
  208 + }
  209 + }
139 210 }
... ...
... ... @@ -292,13 +292,15 @@ export interface DeviceInfo extends Device {
292 292
293 293 export enum DeviceCredentialsType {
294 294 ACCESS_TOKEN = 'ACCESS_TOKEN',
295   - X509_CERTIFICATE = 'X509_CERTIFICATE'
  295 + X509_CERTIFICATE = 'X509_CERTIFICATE',
  296 + MQTT_BASIC = 'MQTT_BASIC'
296 297 }
297 298
298 299 export const credentialTypeNames = new Map<DeviceCredentialsType, string>(
299 300 [
300 301 [DeviceCredentialsType.ACCESS_TOKEN, 'Access token'],
301   - [DeviceCredentialsType.X509_CERTIFICATE, 'X.509 Certificate'],
  302 + [DeviceCredentialsType.X509_CERTIFICATE, 'MQTT X.509'],
  303 + [DeviceCredentialsType.MQTT_BASIC, 'MQTT Basic']
302 304 ]
303 305 );
304 306
... ... @@ -309,6 +311,12 @@ export interface DeviceCredentials extends BaseData<DeviceCredentialsId> {
309 311 credentialsValue: string;
310 312 }
311 313
  314 +export interface DeviceCredentialMQTTBasic {
  315 + clientId: string;
  316 + userName: string;
  317 + password: string;
  318 +}
  319 +
312 320 export interface DeviceSearchQuery extends EntitySearchQuery {
313 321 deviceTypes: Array<string>;
314 322 }
... ...
... ... @@ -718,6 +718,12 @@
718 718 "access-token-invalid": "Access token length must be from 1 to 20 characters.",
719 719 "rsa-key": "RSA public key",
720 720 "rsa-key-required": "RSA public key is required.",
  721 + "client-id": "Client ID",
  722 + "client-id-pattern": "Contains invalid character.",
  723 + "user-name": "User Name",
  724 + "user-name-required": "User Name is required.",
  725 + "client-id-or-user-name-necessary": "Client ID and/or User Name are necessary",
  726 + "password": "Password",
721 727 "secret": "Secret",
722 728 "secret-required": "Secret is required.",
723 729 "device-type": "Device type",
... ...