Commit 969a686c74fd33269500227685dfb8834f3bcf59
Merge branch 'develop/3.2' of github.com:thingsboard/thingsboard into develop/3.2
Showing
12 changed files
with
326 additions
and
42 deletions
... | ... | @@ -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", | ... | ... |