Merge branch 'feature/x509-device-cert-impr' of github.com:AndriiLandiak/thingsboard into feature/x509-device-provisioning
This commit is contained in:
commit
4cea4362dc
@ -20,15 +20,16 @@ import com.fasterxml.jackson.databind.JsonNode;
|
|||||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
import com.google.common.util.concurrent.ListenableFuture;
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.thingsboard.common.util.JacksonUtil;
|
import org.thingsboard.common.util.JacksonUtil;
|
||||||
import org.thingsboard.server.cluster.TbClusterService;
|
import org.thingsboard.server.cluster.TbClusterService;
|
||||||
import org.thingsboard.server.common.data.DataConstants;
|
import org.thingsboard.server.common.data.DataConstants;
|
||||||
import org.thingsboard.server.common.data.Device;
|
import org.thingsboard.server.common.data.Device;
|
||||||
import org.thingsboard.server.common.data.DeviceProfile;
|
import org.thingsboard.server.common.data.DeviceProfile;
|
||||||
|
import org.thingsboard.server.common.data.DeviceProfileProvisionType;
|
||||||
import org.thingsboard.server.common.data.StringUtils;
|
import org.thingsboard.server.common.data.StringUtils;
|
||||||
import org.thingsboard.server.common.data.audit.ActionType;
|
import org.thingsboard.server.common.data.audit.ActionType;
|
||||||
|
import org.thingsboard.server.common.data.device.profile.X509CertificateChainProvisionConfiguration;
|
||||||
import org.thingsboard.server.common.data.id.CustomerId;
|
import org.thingsboard.server.common.data.id.CustomerId;
|
||||||
import org.thingsboard.server.common.data.id.TenantId;
|
import org.thingsboard.server.common.data.id.TenantId;
|
||||||
import org.thingsboard.server.common.data.id.UserId;
|
import org.thingsboard.server.common.data.id.UserId;
|
||||||
@ -36,15 +37,16 @@ import org.thingsboard.server.common.data.kv.AttributeKvEntry;
|
|||||||
import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
|
import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
|
||||||
import org.thingsboard.server.common.data.kv.StringDataEntry;
|
import org.thingsboard.server.common.data.kv.StringDataEntry;
|
||||||
import org.thingsboard.server.common.data.security.DeviceCredentials;
|
import org.thingsboard.server.common.data.security.DeviceCredentials;
|
||||||
|
import org.thingsboard.server.common.data.security.DeviceCredentialsType;
|
||||||
import org.thingsboard.server.common.msg.TbMsg;
|
import org.thingsboard.server.common.msg.TbMsg;
|
||||||
import org.thingsboard.server.common.msg.TbMsgMetaData;
|
import org.thingsboard.server.common.msg.TbMsgMetaData;
|
||||||
import org.thingsboard.server.common.msg.queue.ServiceType;
|
import org.thingsboard.server.common.msg.queue.ServiceType;
|
||||||
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
|
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
|
||||||
|
import org.thingsboard.server.common.transport.util.SslUtil;
|
||||||
import org.thingsboard.server.dao.attributes.AttributesService;
|
import org.thingsboard.server.dao.attributes.AttributesService;
|
||||||
import org.thingsboard.server.dao.audit.AuditLogService;
|
import org.thingsboard.server.dao.audit.AuditLogService;
|
||||||
import org.thingsboard.server.dao.device.DeviceCredentialsService;
|
import org.thingsboard.server.dao.device.DeviceCredentialsService;
|
||||||
import org.thingsboard.server.dao.device.DeviceDao;
|
import org.thingsboard.server.dao.device.DeviceProfileService;
|
||||||
import org.thingsboard.server.dao.device.DeviceProfileDao;
|
|
||||||
import org.thingsboard.server.dao.device.DeviceProvisionService;
|
import org.thingsboard.server.dao.device.DeviceProvisionService;
|
||||||
import org.thingsboard.server.dao.device.DeviceService;
|
import org.thingsboard.server.dao.device.DeviceService;
|
||||||
import org.thingsboard.server.dao.device.provision.ProvisionFailedException;
|
import org.thingsboard.server.dao.device.provision.ProvisionFailedException;
|
||||||
@ -59,12 +61,13 @@ import org.thingsboard.server.queue.common.TbProtoQueueMsg;
|
|||||||
import org.thingsboard.server.queue.discovery.PartitionService;
|
import org.thingsboard.server.queue.discovery.PartitionService;
|
||||||
import org.thingsboard.server.queue.provider.TbQueueProducerProvider;
|
import org.thingsboard.server.queue.provider.TbQueueProducerProvider;
|
||||||
import org.thingsboard.server.queue.util.TbCoreComponent;
|
import org.thingsboard.server.queue.util.TbCoreComponent;
|
||||||
import org.thingsboard.server.service.state.DeviceStateService;
|
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@ -77,39 +80,28 @@ public class DeviceProvisionServiceImpl implements DeviceProvisionService {
|
|||||||
private static final String DEVICE_PROVISION_STATE = "provisionState";
|
private static final String DEVICE_PROVISION_STATE = "provisionState";
|
||||||
private static final String PROVISIONED_STATE = "provisioned";
|
private static final String PROVISIONED_STATE = "provisioned";
|
||||||
|
|
||||||
@Autowired
|
private final TbClusterService clusterService;
|
||||||
TbClusterService clusterService;
|
private final DeviceProfileService deviceProfileService;
|
||||||
|
private final DeviceService deviceService;
|
||||||
|
private final DeviceCredentialsService deviceCredentialsService;
|
||||||
|
private final AttributesService attributesService;
|
||||||
|
private final AuditLogService auditLogService;
|
||||||
|
private final PartitionService partitionService;
|
||||||
|
|
||||||
@Autowired
|
public DeviceProvisionServiceImpl(TbQueueProducerProvider producerProvider, TbClusterService clusterService, DeviceProfileService deviceProfileService, DeviceService deviceService, DeviceCredentialsService deviceCredentialsService, AttributesService attributesService, AuditLogService auditLogService, PartitionService partitionService) {
|
||||||
DeviceDao deviceDao;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
DeviceProfileDao deviceProfileDao;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
DeviceService deviceService;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
DeviceCredentialsService deviceCredentialsService;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
AttributesService attributesService;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
DeviceStateService deviceStateService;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
AuditLogService auditLogService;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
PartitionService partitionService;
|
|
||||||
|
|
||||||
public DeviceProvisionServiceImpl(TbQueueProducerProvider producerProvider) {
|
|
||||||
ruleEngineMsgProducer = producerProvider.getRuleEngineMsgProducer();
|
ruleEngineMsgProducer = producerProvider.getRuleEngineMsgProducer();
|
||||||
|
this.clusterService = clusterService;
|
||||||
|
this.deviceProfileService = deviceProfileService;
|
||||||
|
this.deviceService = deviceService;
|
||||||
|
this.deviceCredentialsService = deviceCredentialsService;
|
||||||
|
this.attributesService = attributesService;
|
||||||
|
this.auditLogService = auditLogService;
|
||||||
|
this.partitionService = partitionService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ProvisionResponse provisionDevice(ProvisionRequest provisionRequest) {
|
public ProvisionResponse provisionDevice(ProvisionRequest provisionRequest) {
|
||||||
|
fetchAndApplyDeviceNameForX509ProvisionRequestWithRegEx(provisionRequest);
|
||||||
String provisionRequestKey = provisionRequest.getCredentials().getProvisionDeviceKey();
|
String provisionRequestKey = provisionRequest.getCredentials().getProvisionDeviceKey();
|
||||||
String provisionRequestSecret = provisionRequest.getCredentials().getProvisionDeviceSecret();
|
String provisionRequestSecret = provisionRequest.getCredentials().getProvisionDeviceSecret();
|
||||||
if (!StringUtils.isEmpty(provisionRequest.getDeviceName())) {
|
if (!StringUtils.isEmpty(provisionRequest.getDeviceName())) {
|
||||||
@ -124,14 +116,14 @@ public class DeviceProvisionServiceImpl implements DeviceProvisionService {
|
|||||||
throw new ProvisionFailedException(ProvisionResponseStatus.NOT_FOUND.name());
|
throw new ProvisionFailedException(ProvisionResponseStatus.NOT_FOUND.name());
|
||||||
}
|
}
|
||||||
|
|
||||||
DeviceProfile targetProfile = deviceProfileDao.findByProvisionDeviceKey(provisionRequestKey);
|
DeviceProfile targetProfile = deviceProfileService.findDeviceProfileByProvisionDeviceKey(provisionRequestKey);
|
||||||
|
|
||||||
if (targetProfile == null || targetProfile.getProfileData().getProvisionConfiguration() == null ||
|
if (targetProfile == null || targetProfile.getProfileData().getProvisionConfiguration() == null ||
|
||||||
targetProfile.getProfileData().getProvisionConfiguration().getProvisionDeviceSecret() == null) {
|
targetProfile.getProfileData().getProvisionConfiguration().getProvisionDeviceSecret() == null) {
|
||||||
throw new ProvisionFailedException(ProvisionResponseStatus.NOT_FOUND.name());
|
throw new ProvisionFailedException(ProvisionResponseStatus.NOT_FOUND.name());
|
||||||
}
|
}
|
||||||
|
|
||||||
Device targetDevice = deviceDao.findDeviceByTenantIdAndName(targetProfile.getTenantId().getId(), provisionRequest.getDeviceName()).orElse(null);
|
Device targetDevice = deviceService.findDeviceByTenantIdAndName(targetProfile.getTenantId(), provisionRequest.getDeviceName());
|
||||||
|
|
||||||
switch (targetProfile.getProvisionType()) {
|
switch (targetProfile.getProvisionType()) {
|
||||||
case ALLOW_CREATE_NEW_DEVICES:
|
case ALLOW_CREATE_NEW_DEVICES:
|
||||||
@ -155,6 +147,25 @@ public class DeviceProvisionServiceImpl implements DeviceProvisionService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case X509_CERTIFICATE_CHAIN:
|
||||||
|
if (targetProfile.getProfileData().getProvisionConfiguration().getProvisionDeviceSecret().equals(provisionRequestSecret)) {
|
||||||
|
X509CertificateChainProvisionConfiguration x509Configuration = (X509CertificateChainProvisionConfiguration) targetProfile.getProfileData().getProvisionConfiguration();
|
||||||
|
if (targetDevice != null && targetDevice.getDeviceProfileId().equals(targetProfile.getId())) {
|
||||||
|
DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(targetDevice.getTenantId(), targetDevice.getId());
|
||||||
|
if (deviceCredentials.getCredentialsType() == DeviceCredentialsType.X509_CERTIFICATE) {
|
||||||
|
String updatedDeviceCertificateValue = provisionRequest.getCredentialsData().getX509CertHash();
|
||||||
|
deviceCredentials = updateDeviceCredentials(targetDevice.getTenantId(), deviceCredentials,
|
||||||
|
updatedDeviceCertificateValue, DeviceCredentialsType.X509_CERTIFICATE);
|
||||||
|
}
|
||||||
|
return new ProvisionResponse(deviceCredentials, ProvisionResponseStatus.SUCCESS);
|
||||||
|
} else if (x509Configuration.isAllowCreateNewDevicesByX509Certificate()) {
|
||||||
|
return createDevice(provisionRequest, targetProfile);
|
||||||
|
} else {
|
||||||
|
log.warn("Device with name {} doesn't exist and cannot be created due incorrect configuration for X509CertificateChainProvisionConfiguration", provisionRequest.getDeviceName());
|
||||||
|
throw new ProvisionFailedException(ProvisionResponseStatus.FAILURE.name());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
throw new ProvisionFailedException(ProvisionResponseStatus.NOT_FOUND.name());
|
throw new ProvisionFailedException(ProvisionResponseStatus.NOT_FOUND.name());
|
||||||
}
|
}
|
||||||
@ -209,6 +220,14 @@ public class DeviceProvisionServiceImpl implements DeviceProvisionService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private DeviceCredentials updateDeviceCredentials(TenantId tenantId, DeviceCredentials deviceCredentials, String certificateValue,
|
||||||
|
DeviceCredentialsType credentialsType) {
|
||||||
|
log.trace("Updating device credentials [{}] with certificate value [{}]", deviceCredentials, certificateValue);
|
||||||
|
deviceCredentials.setCredentialsValue(certificateValue);
|
||||||
|
deviceCredentials.setCredentialsType(credentialsType);
|
||||||
|
return deviceCredentialsService.updateDeviceCredentials(tenantId, deviceCredentials);
|
||||||
|
}
|
||||||
|
|
||||||
private ListenableFuture<List<String>> saveProvisionStateAttribute(Device device) {
|
private ListenableFuture<List<String>> saveProvisionStateAttribute(Device device) {
|
||||||
return attributesService.save(device.getTenantId(), device.getId(), DataConstants.SERVER_SCOPE,
|
return attributesService.save(device.getTenantId(), device.getId(), DataConstants.SERVER_SCOPE,
|
||||||
Collections.singletonList(new BaseAttributeKvEntry(new StringDataEntry(DEVICE_PROVISION_STATE, PROVISIONED_STATE),
|
Collections.singletonList(new BaseAttributeKvEntry(new StringDataEntry(DEVICE_PROVISION_STATE, PROVISIONED_STATE),
|
||||||
@ -257,4 +276,32 @@ public class DeviceProvisionServiceImpl implements DeviceProvisionService {
|
|||||||
ActionType actionType = success ? ActionType.PROVISION_SUCCESS : ActionType.PROVISION_FAILURE;
|
ActionType actionType = success ? ActionType.PROVISION_SUCCESS : ActionType.PROVISION_FAILURE;
|
||||||
auditLogService.logEntityAction(tenantId, customerId, new UserId(UserId.NULL_UUID), device.getName(), device.getId(), device, actionType, null, provisionRequest);
|
auditLogService.logEntityAction(tenantId, customerId, new UserId(UserId.NULL_UUID), device.getName(), device.getId(), device, actionType, null, provisionRequest);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void fetchAndApplyDeviceNameForX509ProvisionRequestWithRegEx(ProvisionRequest provisionRequest) {
|
||||||
|
DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileByProvisionDeviceKey(provisionRequest.getCredentials().getProvisionDeviceKey());
|
||||||
|
if (deviceProfile != null && DeviceProfileProvisionType.X509_CERTIFICATE_CHAIN.equals(deviceProfile.getProfileData().getProvisionConfiguration().getType())) {
|
||||||
|
X509CertificateChainProvisionConfiguration configuration = (X509CertificateChainProvisionConfiguration) deviceProfile.getProfileData().getProvisionConfiguration();
|
||||||
|
String certificateValue = provisionRequest.getCredentialsData().getX509CertHash();
|
||||||
|
String certificateRegEx = configuration.getCertificateRegExPattern();
|
||||||
|
String deviceName = extractDeviceNameFromCertificateCNByRegEx(certificateValue, certificateRegEx);
|
||||||
|
if (deviceName == null) {
|
||||||
|
log.warn("Device name cannot be extracted using regex [{}] for certificate [{}]",certificateRegEx, certificateValue);
|
||||||
|
throw new ProvisionFailedException(ProvisionResponseStatus.FAILURE.name());
|
||||||
|
}
|
||||||
|
provisionRequest.setDeviceName(deviceName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String extractDeviceNameFromCertificateCNByRegEx(String x509Value, String regex) {
|
||||||
|
try {
|
||||||
|
String commonName = SslUtil.parseCommonName(SslUtil.readCertFile(x509Value));
|
||||||
|
log.trace("Extract CN [{}] by regex pattern [{}]", commonName, regex);
|
||||||
|
Pattern pattern = Pattern.compile(regex);
|
||||||
|
Matcher matcher = pattern.matcher(commonName);
|
||||||
|
if (matcher.find()) {
|
||||||
|
return matcher.group(0);
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -82,6 +82,9 @@ public class DefaultCacheCleanupService implements CacheCleanupService {
|
|||||||
log.info("Clearing cache to upgrade from version 3.4.2 to 3.4.3 ...");
|
log.info("Clearing cache to upgrade from version 3.4.2 to 3.4.3 ...");
|
||||||
clearCacheByName("repositorySettings");
|
clearCacheByName("repositorySettings");
|
||||||
break;
|
break;
|
||||||
|
case "3.4.4":
|
||||||
|
log.info("Clearing cache to upgrade from version 3.4.4 to 3.5.0");
|
||||||
|
clearCacheByName("deviceProfiles");
|
||||||
default:
|
default:
|
||||||
//Do nothing, since cache cleanup is optional.
|
//Do nothing, since cache cleanup is optional.
|
||||||
}
|
}
|
||||||
|
|||||||
@ -66,11 +66,13 @@ import org.thingsboard.server.common.msg.TbMsg;
|
|||||||
import org.thingsboard.server.common.msg.TbMsgDataType;
|
import org.thingsboard.server.common.msg.TbMsgDataType;
|
||||||
import org.thingsboard.server.common.msg.TbMsgMetaData;
|
import org.thingsboard.server.common.msg.TbMsgMetaData;
|
||||||
import org.thingsboard.server.dao.device.DeviceCredentialsService;
|
import org.thingsboard.server.dao.device.DeviceCredentialsService;
|
||||||
|
import org.thingsboard.server.dao.device.DeviceProfileService;
|
||||||
import org.thingsboard.server.dao.device.DeviceProvisionService;
|
import org.thingsboard.server.dao.device.DeviceProvisionService;
|
||||||
import org.thingsboard.server.dao.device.DeviceService;
|
import org.thingsboard.server.dao.device.DeviceService;
|
||||||
import org.thingsboard.server.dao.device.provision.ProvisionFailedException;
|
import org.thingsboard.server.dao.device.provision.ProvisionFailedException;
|
||||||
import org.thingsboard.server.dao.device.provision.ProvisionRequest;
|
import org.thingsboard.server.dao.device.provision.ProvisionRequest;
|
||||||
import org.thingsboard.server.dao.device.provision.ProvisionResponse;
|
import org.thingsboard.server.dao.device.provision.ProvisionResponse;
|
||||||
|
import org.thingsboard.server.dao.device.provision.ProvisionResponseStatus;
|
||||||
import org.thingsboard.server.dao.ota.OtaPackageService;
|
import org.thingsboard.server.dao.ota.OtaPackageService;
|
||||||
import org.thingsboard.server.dao.queue.QueueService;
|
import org.thingsboard.server.dao.queue.QueueService;
|
||||||
import org.thingsboard.server.dao.relation.RelationService;
|
import org.thingsboard.server.dao.relation.RelationService;
|
||||||
@ -108,8 +110,10 @@ import java.util.concurrent.ConcurrentHashMap;
|
|||||||
import java.util.concurrent.ConcurrentMap;
|
import java.util.concurrent.ConcurrentMap;
|
||||||
import java.util.concurrent.locks.Lock;
|
import java.util.concurrent.locks.Lock;
|
||||||
import java.util.concurrent.locks.ReentrantLock;
|
import java.util.concurrent.locks.ReentrantLock;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import static org.thingsboard.server.common.data.DeviceProfileProvisionType.X509_CERTIFICATE_CHAIN;
|
||||||
import static org.thingsboard.server.service.transport.BasicCredentialsValidationResult.PASSWORD_MISMATCH;
|
import static org.thingsboard.server.service.transport.BasicCredentialsValidationResult.PASSWORD_MISMATCH;
|
||||||
import static org.thingsboard.server.service.transport.BasicCredentialsValidationResult.VALID;
|
import static org.thingsboard.server.service.transport.BasicCredentialsValidationResult.VALID;
|
||||||
|
|
||||||
@ -124,10 +128,13 @@ public class DefaultTransportApiService implements TransportApiService {
|
|||||||
|
|
||||||
private static final ObjectMapper mapper = new ObjectMapper();
|
private static final ObjectMapper mapper = new ObjectMapper();
|
||||||
|
|
||||||
|
private static final Pattern X509_CERTIFICATE_TRIM_CHAIN_PATTERN = Pattern.compile("-----BEGIN CERTIFICATE-----\\s*.*?\\s*-----END CERTIFICATE-----");
|
||||||
|
|
||||||
private final TbDeviceProfileCache deviceProfileCache;
|
private final TbDeviceProfileCache deviceProfileCache;
|
||||||
private final TbTenantProfileCache tenantProfileCache;
|
private final TbTenantProfileCache tenantProfileCache;
|
||||||
private final TbApiUsageStateService apiUsageStateService;
|
private final TbApiUsageStateService apiUsageStateService;
|
||||||
private final DeviceService deviceService;
|
private final DeviceService deviceService;
|
||||||
|
private final DeviceProfileService deviceProfileService;
|
||||||
private final RelationService relationService;
|
private final RelationService relationService;
|
||||||
private final DeviceCredentialsService deviceCredentialsService;
|
private final DeviceCredentialsService deviceCredentialsService;
|
||||||
private final DbCallbackExecutorService dbCallbackExecutorService;
|
private final DbCallbackExecutorService dbCallbackExecutorService;
|
||||||
@ -159,6 +166,9 @@ public class DefaultTransportApiService implements TransportApiService {
|
|||||||
} else if (transportApiRequestMsg.hasValidateX509CertRequestMsg()) {
|
} else if (transportApiRequestMsg.hasValidateX509CertRequestMsg()) {
|
||||||
ValidateDeviceX509CertRequestMsg msg = transportApiRequestMsg.getValidateX509CertRequestMsg();
|
ValidateDeviceX509CertRequestMsg msg = transportApiRequestMsg.getValidateX509CertRequestMsg();
|
||||||
result = validateCredentials(msg.getHash(), DeviceCredentialsType.X509_CERTIFICATE);
|
result = validateCredentials(msg.getHash(), DeviceCredentialsType.X509_CERTIFICATE);
|
||||||
|
} else if (transportApiRequestMsg.hasValidateOrCreateX509CertRequestMsg()) {
|
||||||
|
TransportProtos.ValidateOrCreateDeviceX509CertRequestMsg msg = transportApiRequestMsg.getValidateOrCreateX509CertRequestMsg();
|
||||||
|
result = validateOrCreateDeviceX509Certificate(msg.getCertificateChain());
|
||||||
} else if (transportApiRequestMsg.hasGetOrCreateDeviceRequestMsg()) {
|
} else if (transportApiRequestMsg.hasGetOrCreateDeviceRequestMsg()) {
|
||||||
result = handle(transportApiRequestMsg.getGetOrCreateDeviceRequestMsg());
|
result = handle(transportApiRequestMsg.getGetOrCreateDeviceRequestMsg());
|
||||||
} else if (transportApiRequestMsg.hasEntityProfileRequestMsg()) {
|
} else if (transportApiRequestMsg.hasEntityProfileRequestMsg()) {
|
||||||
@ -226,6 +236,32 @@ public class DefaultTransportApiService implements TransportApiService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected ListenableFuture<TransportApiResponseMsg> validateOrCreateDeviceX509Certificate(String certificateChain) {
|
||||||
|
List<String> chain = X509_CERTIFICATE_TRIM_CHAIN_PATTERN.matcher(certificateChain).results().map(match ->
|
||||||
|
EncryptionUtil.certTrimNewLines(match.group())).collect(Collectors.toList());
|
||||||
|
for (String certificateValue : chain) {
|
||||||
|
String certificateHash = EncryptionUtil.getSha3Hash(certificateValue);
|
||||||
|
DeviceCredentials credentials = deviceCredentialsService.findDeviceCredentialsByCredentialsId(certificateHash);
|
||||||
|
if (credentials != null && credentials.getCredentialsType() == DeviceCredentialsType.X509_CERTIFICATE) {
|
||||||
|
return getDeviceInfo(credentials);
|
||||||
|
}
|
||||||
|
DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileByProvisionDeviceKey(certificateHash);
|
||||||
|
if (deviceProfile != null && deviceProfile.getProvisionType() == X509_CERTIFICATE_CHAIN) {
|
||||||
|
String updatedDeviceProvisionSecret = chain.get(0);
|
||||||
|
ProvisionRequest provisionRequest = createProvisionRequest(deviceProfile, updatedDeviceProvisionSecret);
|
||||||
|
ProvisionResponse provisionResponse = provisionDeviceRequestAndGetResponse(provisionRequest);
|
||||||
|
if (provisionResponse != null && ProvisionResponseStatus.SUCCESS.equals(provisionResponse.getResponseStatus())) {
|
||||||
|
return getDeviceInfo(provisionResponse.getDeviceCredentials());
|
||||||
|
} else {
|
||||||
|
return getEmptyTransportApiResponseFuture();
|
||||||
|
}
|
||||||
|
} else if (deviceProfile != null) {
|
||||||
|
log.warn("[{}] Device Profile provision configuration mismatched: expected {}, actual {}", deviceProfile.getId(), X509_CERTIFICATE_CHAIN, deviceProfile.getProvisionType());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return getEmptyTransportApiResponseFuture();
|
||||||
|
}
|
||||||
|
|
||||||
private ListenableFuture<TransportApiResponseMsg> validateUserNameCredentials(TransportProtos.ValidateBasicMqttCredRequestMsg mqtt) {
|
private ListenableFuture<TransportApiResponseMsg> validateUserNameCredentials(TransportProtos.ValidateBasicMqttCredRequestMsg mqtt) {
|
||||||
DeviceCredentials credentials = deviceCredentialsService.findDeviceCredentialsByCredentialsId(mqtt.getUserName());
|
DeviceCredentials credentials = deviceCredentialsService.findDeviceCredentialsByCredentialsId(mqtt.getUserName());
|
||||||
if (credentials != null) {
|
if (credentials != null) {
|
||||||
@ -665,4 +701,24 @@ public class DefaultTransportApiService implements TransportApiService {
|
|||||||
private Long checkLong(Long l) {
|
private Long checkLong(Long l) {
|
||||||
return l != null ? l : 0;
|
return l != null ? l : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ProvisionRequest createProvisionRequest(DeviceProfile deviceProfile, String certificateValue) {
|
||||||
|
ProvisionDeviceProfileCredentials provisionDeviceProfileCredentials = new ProvisionDeviceProfileCredentials(
|
||||||
|
deviceProfile.getProvisionDeviceKey(),
|
||||||
|
deviceProfile.getProfileData().getProvisionConfiguration().getProvisionDeviceSecret()
|
||||||
|
);
|
||||||
|
ProvisionDeviceCredentialsData provisionDeviceCredentialsData = new ProvisionDeviceCredentialsData(null, null, null, null, certificateValue);
|
||||||
|
|
||||||
|
return new ProvisionRequest(null, DeviceCredentialsType.X509_CERTIFICATE, provisionDeviceCredentialsData, provisionDeviceProfileCredentials);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ProvisionResponse provisionDeviceRequestAndGetResponse(ProvisionRequest provisionRequest) {
|
||||||
|
try {
|
||||||
|
return deviceProvisionService.provisionDevice(provisionRequest);
|
||||||
|
} catch (ProvisionFailedException e) {
|
||||||
|
log.error(e.getMessage());
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -130,6 +130,9 @@ security:
|
|||||||
loginProcessingUrl: "${SECURITY_OAUTH2_LOGIN_PROCESSING_URL:/login/oauth2/code/}"
|
loginProcessingUrl: "${SECURITY_OAUTH2_LOGIN_PROCESSING_URL:/login/oauth2/code/}"
|
||||||
githubMapper:
|
githubMapper:
|
||||||
emailUrl: "${SECURITY_OAUTH2_GITHUB_MAPPER_EMAIL_URL_KEY:https://api.github.com/user/emails}"
|
emailUrl: "${SECURITY_OAUTH2_GITHUB_MAPPER_EMAIL_URL_KEY:https://api.github.com/user/emails}"
|
||||||
|
java_cacerts:
|
||||||
|
path: "${SECURITY_JAVA_CACERTS_PATH:${java.home}${file.separator}lib${file.separator}security${file.separator}cacerts}"
|
||||||
|
password: "${SECURITY_JAVA_CACERTS_PASSWORD:changeit}"
|
||||||
|
|
||||||
# Usage statistics parameters
|
# Usage statistics parameters
|
||||||
usage:
|
usage:
|
||||||
|
|||||||
@ -299,6 +299,28 @@ public abstract class BaseDeviceProfileControllerTest extends AbstractController
|
|||||||
tenantAdmin.getId(), tenantAdmin.getEmail(), ActionType.ADDED, new DataValidationException(msgError));
|
tenantAdmin.getId(), tenantAdmin.getEmail(), ActionType.ADDED, new DataValidationException(msgError));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSaveDeviceProfileWithSameCertificateHash() throws Exception {
|
||||||
|
DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile");
|
||||||
|
deviceProfile.setProvisionDeviceKey("Certificate hash");
|
||||||
|
|
||||||
|
doPost("/api/deviceProfile", deviceProfile)
|
||||||
|
.andExpect(status().isOk());
|
||||||
|
|
||||||
|
DeviceProfile deviceProfile2 = this.createDeviceProfile("Device Profile 2");
|
||||||
|
deviceProfile2.setProvisionDeviceKey("Certificate hash");
|
||||||
|
|
||||||
|
Mockito.reset(tbClusterService, auditLogService);
|
||||||
|
|
||||||
|
String msgError = "Device profile with such provision device key already exists!";
|
||||||
|
doPost("/api/deviceProfile", deviceProfile2)
|
||||||
|
.andExpect(status().isBadRequest())
|
||||||
|
.andExpect(statusReason(containsString(msgError)));
|
||||||
|
|
||||||
|
testNotifyEntityEqualsOneTimeServiceNeverError(deviceProfile, savedTenant.getId(),
|
||||||
|
tenantAdmin.getId(), tenantAdmin.getEmail(), ActionType.ADDED, new DataValidationException(msgError));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testChangeDeviceProfileTypeNull() throws Exception {
|
public void testChangeDeviceProfileTypeNull() throws Exception {
|
||||||
DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile");
|
DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile");
|
||||||
|
|||||||
@ -0,0 +1,210 @@
|
|||||||
|
/**
|
||||||
|
* Copyright © 2016-2023 The Thingsboard Authors
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.thingsboard.server.service.transport;
|
||||||
|
|
||||||
|
|
||||||
|
import com.google.common.util.concurrent.Futures;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||||
|
import org.springframework.boot.test.mock.mockito.SpyBean;
|
||||||
|
import org.springframework.test.context.ContextConfiguration;
|
||||||
|
import org.springframework.test.context.junit4.SpringRunner;
|
||||||
|
import org.thingsboard.server.cache.ota.OtaPackageDataCache;
|
||||||
|
import org.thingsboard.server.cluster.TbClusterService;
|
||||||
|
import org.thingsboard.server.common.data.Device;
|
||||||
|
import org.thingsboard.server.common.data.DeviceProfile;
|
||||||
|
import org.thingsboard.server.common.data.DeviceProfileProvisionType;
|
||||||
|
import org.thingsboard.server.common.data.device.profile.DeviceProfileData;
|
||||||
|
import org.thingsboard.server.common.data.device.profile.X509CertificateChainProvisionConfiguration;
|
||||||
|
import org.thingsboard.server.common.data.id.DeviceId;
|
||||||
|
import org.thingsboard.server.common.data.security.DeviceCredentials;
|
||||||
|
import org.thingsboard.server.common.data.security.DeviceCredentialsType;
|
||||||
|
import org.thingsboard.server.common.msg.EncryptionUtil;
|
||||||
|
import org.thingsboard.server.dao.device.DeviceCredentialsService;
|
||||||
|
import org.thingsboard.server.dao.device.DeviceProfileService;
|
||||||
|
import org.thingsboard.server.dao.device.DeviceProvisionService;
|
||||||
|
import org.thingsboard.server.dao.device.DeviceService;
|
||||||
|
import org.thingsboard.server.dao.device.provision.ProvisionResponse;
|
||||||
|
import org.thingsboard.server.dao.device.provision.ProvisionResponseStatus;
|
||||||
|
import org.thingsboard.server.dao.ota.OtaPackageService;
|
||||||
|
import org.thingsboard.server.dao.queue.QueueService;
|
||||||
|
import org.thingsboard.server.dao.relation.RelationService;
|
||||||
|
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
|
||||||
|
import org.thingsboard.server.queue.util.DataDecodingEncodingService;
|
||||||
|
import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
|
||||||
|
import org.thingsboard.server.service.executors.DbCallbackExecutorService;
|
||||||
|
import org.thingsboard.server.service.profile.TbDeviceProfileCache;
|
||||||
|
import org.thingsboard.server.service.resource.TbResourceService;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@RunWith(SpringRunner.class)
|
||||||
|
@ContextConfiguration(classes = DefaultTransportApiService.class)
|
||||||
|
public class DefaultTransportApiServiceTest {
|
||||||
|
|
||||||
|
@MockBean
|
||||||
|
protected TbDeviceProfileCache deviceProfileCache;
|
||||||
|
@MockBean
|
||||||
|
protected TbTenantProfileCache tenantProfileCache;
|
||||||
|
@MockBean
|
||||||
|
protected TbApiUsageStateService apiUsageStateService;
|
||||||
|
@MockBean
|
||||||
|
protected DeviceService deviceService;
|
||||||
|
@MockBean
|
||||||
|
protected DeviceProfileService deviceProfileService;
|
||||||
|
@MockBean
|
||||||
|
protected RelationService relationService;
|
||||||
|
@MockBean
|
||||||
|
protected DeviceCredentialsService deviceCredentialsService;
|
||||||
|
@MockBean
|
||||||
|
protected DbCallbackExecutorService dbCallbackExecutorService;
|
||||||
|
@MockBean
|
||||||
|
protected TbClusterService tbClusterService;
|
||||||
|
@MockBean
|
||||||
|
protected DataDecodingEncodingService dataDecodingEncodingService;
|
||||||
|
@MockBean
|
||||||
|
protected DeviceProvisionService deviceProvisionService;
|
||||||
|
@MockBean
|
||||||
|
protected TbResourceService resourceService;
|
||||||
|
@MockBean
|
||||||
|
protected OtaPackageService otaPackageService;
|
||||||
|
@MockBean
|
||||||
|
protected OtaPackageDataCache otaPackageDataCache;
|
||||||
|
@MockBean
|
||||||
|
protected QueueService queueService;
|
||||||
|
@SpyBean
|
||||||
|
DefaultTransportApiService service;
|
||||||
|
|
||||||
|
private String certificateChain;
|
||||||
|
private String[] chain;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setUp() {
|
||||||
|
String filePath = "src/test/resources/mqtt/x509ChainProvisionTest.pem";
|
||||||
|
try {
|
||||||
|
certificateChain = Files.readString(Paths.get(filePath));
|
||||||
|
certificateChain = certTrimNewLinesForChainInDeviceProfile(certificateChain);
|
||||||
|
chain = fetchLeafCertificateFromChain(certificateChain);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void validateExistingDeviceX509Certificate() {
|
||||||
|
var device = createDevice();
|
||||||
|
when(deviceService.findDeviceByIdAsync(any(), any())).thenReturn(Futures.immediateFuture(device));
|
||||||
|
|
||||||
|
var deviceCredentials = createDeviceCredentials(chain[0], device.getId());
|
||||||
|
when(deviceCredentialsService.findDeviceCredentialsByCredentialsId(any())).thenReturn(deviceCredentials);
|
||||||
|
|
||||||
|
service.validateOrCreateDeviceX509Certificate(certificateChain);
|
||||||
|
verify(deviceCredentialsService, times(1)).findDeviceCredentialsByCredentialsId(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void provisionDeviceX509Certificate() {
|
||||||
|
var deviceProfile = createDeviceProfile(chain[1]);
|
||||||
|
when(deviceProfileService.findDeviceProfileByProvisionDeviceKey(any())).thenReturn(deviceProfile);
|
||||||
|
|
||||||
|
var device = createDevice();
|
||||||
|
when(deviceService.findDeviceByTenantIdAndName(any(), any())).thenReturn(device);
|
||||||
|
when(deviceService.findDeviceByIdAsync(any(), any())).thenReturn(Futures.immediateFuture(device));
|
||||||
|
|
||||||
|
var deviceCredentials = createDeviceCredentials(chain[0], device.getId());
|
||||||
|
when(deviceCredentialsService.findDeviceCredentialsByCredentialsId(any())).thenReturn(null);
|
||||||
|
when(deviceCredentialsService.updateDeviceCredentials(any(), any())).thenReturn(deviceCredentials);
|
||||||
|
|
||||||
|
var provisionResponse = createProvisionResponse(deviceCredentials);
|
||||||
|
when(deviceProvisionService.provisionDevice(any())).thenReturn(provisionResponse);
|
||||||
|
|
||||||
|
service.validateOrCreateDeviceX509Certificate(certificateChain);
|
||||||
|
verify(deviceProfileService, times(1)).findDeviceProfileByProvisionDeviceKey(any());
|
||||||
|
verify(deviceService, times(1)).findDeviceByIdAsync(any(), any());
|
||||||
|
verify(deviceCredentialsService, times(1)).findDeviceCredentialsByCredentialsId(any());
|
||||||
|
verify(deviceProvisionService, times(1)).provisionDevice(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
private DeviceProfile createDeviceProfile(String certificateValue) {
|
||||||
|
X509CertificateChainProvisionConfiguration provision = new X509CertificateChainProvisionConfiguration();
|
||||||
|
provision.setProvisionDeviceSecret(certificateValue);
|
||||||
|
provision.setCertificateRegExPattern("([^@]+)");
|
||||||
|
provision.setAllowCreateNewDevicesByX509Certificate(true);
|
||||||
|
|
||||||
|
DeviceProfileData deviceProfileData = new DeviceProfileData();
|
||||||
|
deviceProfileData.setProvisionConfiguration(provision);
|
||||||
|
|
||||||
|
DeviceProfile deviceProfile = new DeviceProfile();
|
||||||
|
deviceProfile.setProfileData(deviceProfileData);
|
||||||
|
deviceProfile.setProvisionDeviceKey(EncryptionUtil.getSha3Hash(certificateValue));
|
||||||
|
deviceProfile.setProvisionType(DeviceProfileProvisionType.X509_CERTIFICATE_CHAIN);
|
||||||
|
return deviceProfile;
|
||||||
|
}
|
||||||
|
|
||||||
|
private DeviceCredentials createDeviceCredentials(String certificateValue, DeviceId deviceId) {
|
||||||
|
DeviceCredentials deviceCredentials = new DeviceCredentials();
|
||||||
|
deviceCredentials.setDeviceId(deviceId);
|
||||||
|
deviceCredentials.setCredentialsValue(certificateValue);
|
||||||
|
deviceCredentials.setCredentialsId(EncryptionUtil.getSha3Hash(certificateValue));
|
||||||
|
deviceCredentials.setCredentialsType(DeviceCredentialsType.X509_CERTIFICATE);
|
||||||
|
return deviceCredentials;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Device createDevice() {
|
||||||
|
Device device = new Device();
|
||||||
|
device.setId(new DeviceId(UUID.randomUUID()));
|
||||||
|
return device;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ProvisionResponse createProvisionResponse(DeviceCredentials deviceCredentials) {
|
||||||
|
return new ProvisionResponse(deviceCredentials, ProvisionResponseStatus.SUCCESS);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String certTrimNewLinesForChainInDeviceProfile(String input) {
|
||||||
|
return input.replaceAll("\n", "")
|
||||||
|
.replaceAll("\r", "")
|
||||||
|
.replaceAll("-----BEGIN CERTIFICATE-----", "-----BEGIN CERTIFICATE-----\n")
|
||||||
|
.replaceAll("-----END CERTIFICATE-----", "\n-----END CERTIFICATE-----\n")
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String[] fetchLeafCertificateFromChain(String value) {
|
||||||
|
List<String> chain = new ArrayList<>();
|
||||||
|
String regex = "-----BEGIN CERTIFICATE-----\\s*.*?\\s*-----END CERTIFICATE-----";
|
||||||
|
Pattern pattern = Pattern.compile(regex);
|
||||||
|
Matcher matcher = pattern.matcher(value);
|
||||||
|
while (matcher.find()) {
|
||||||
|
chain.add(matcher.group(0));
|
||||||
|
}
|
||||||
|
return chain.toArray(new String[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIICMTCCAdegAwIBAgIUI9dBuwN6pTtK6uZ03rkiCwV4wEYwCgYIKoZIzj0EAwIw
|
||||||
|
bjELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE5ldyBZb3JrMRowGAYDVQQKDBFUaGlu
|
||||||
|
Z3NCb2FyZCwgSW5jLjEwMC4GA1UEAwwnZGV2aWNlQ2VydGlmaWNhdGVAWDUwOVBy
|
||||||
|
b3Zpc2lvblN0cmF0ZWd5MB4XDTIzMDMyOTE0NTYxN1oXDTI0MDMyODE0NTYxN1ow
|
||||||
|
bjELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE5ldyBZb3JrMRowGAYDVQQKDBFUaGlu
|
||||||
|
Z3NCb2FyZCwgSW5jLjEwMC4GA1UEAwwnZGV2aWNlQ2VydGlmaWNhdGVAWDUwOVBy
|
||||||
|
b3Zpc2lvblN0cmF0ZWd5MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE9Zo791qK
|
||||||
|
QiGNBm11r4ZGxh+w+ossZL3xc46ufq5QckQHP7zkD2XDAcmP5GvdkM1sBFN9AWaC
|
||||||
|
kQfNnWmfERsOOKNTMFEwHQYDVR0OBBYEFFFc5uyCyglQoZiKhzXzMcQ3BKORMB8G
|
||||||
|
A1UdIwQYMBaAFFFc5uyCyglQoZiKhzXzMcQ3BKORMA8GA1UdEwEB/wQFMAMBAf8w
|
||||||
|
CgYIKoZIzj0EAwIDSAAwRQIhANbA9CuhoOifZMMmqkpuld+65CR+ItKdXeRAhLMZ
|
||||||
|
uccuAiB0FSQB34zMutXrZj1g8Gl5OkE7YryFHbei1z0SveHR8g==
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIICMTCCAdegAwIBAgIUUEKxS9hTz4l+oLUMF0LV6TC/gCIwCgYIKoZIzj0EAwIw
|
||||||
|
bjELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE5ldyBZb3JrMRowGAYDVQQKDBFUaGlu
|
||||||
|
Z3NCb2FyZCwgSW5jLjEwMC4GA1UEAwwnZGV2aWNlUHJvZmlsZUNlcnRAWDUwOVBy
|
||||||
|
b3Zpc2lvblN0cmF0ZWd5MB4XDTIzMDMyOTE0NTczNloXDTI0MDMyODE0NTczNlow
|
||||||
|
bjELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE5ldyBZb3JrMRowGAYDVQQKDBFUaGlu
|
||||||
|
Z3NCb2FyZCwgSW5jLjEwMC4GA1UEAwwnZGV2aWNlUHJvZmlsZUNlcnRAWDUwOVBy
|
||||||
|
b3Zpc2lvblN0cmF0ZWd5MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAECMlWO72k
|
||||||
|
rDoUL9FQjUmSCetkhaEGJUfQkdSfkLSNa0GyAEIMbfmzI4zITeapunu4rGet3EMy
|
||||||
|
LydQzuQanBicp6NTMFEwHQYDVR0OBBYEFHpZ78tPnztNii4Da/yCw6mhEIL3MB8G
|
||||||
|
A1UdIwQYMBaAFHpZ78tPnztNii4Da/yCw6mhEIL3MA8GA1UdEwEB/wQFMAMBAf8w
|
||||||
|
CgYIKoZIzj0EAwIDSAAwRQIgJ7qyMFqNcwSYkH6o+UlQXzLWfwZbNjVk+aR7foAZ
|
||||||
|
NGsCIQDsd7v3WQIGHiArfZeDs1DLEDuV/2h6L+ZNoGNhEKL+1A==
|
||||||
|
-----END CERTIFICATE-----
|
||||||
@ -179,6 +179,10 @@ message ValidateDeviceX509CertRequestMsg {
|
|||||||
string hash = 1;
|
string hash = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message ValidateOrCreateDeviceX509CertRequestMsg {
|
||||||
|
string certificateChain = 1;
|
||||||
|
}
|
||||||
|
|
||||||
message ValidateBasicMqttCredRequestMsg {
|
message ValidateBasicMqttCredRequestMsg {
|
||||||
string clientId = 1;
|
string clientId = 1;
|
||||||
string userName = 2;
|
string userName = 2;
|
||||||
@ -942,6 +946,7 @@ message TransportApiRequestMsg {
|
|||||||
GetDeviceRequestMsg deviceRequestMsg = 12;
|
GetDeviceRequestMsg deviceRequestMsg = 12;
|
||||||
GetDeviceCredentialsRequestMsg deviceCredentialsRequestMsg = 13;
|
GetDeviceCredentialsRequestMsg deviceCredentialsRequestMsg = 13;
|
||||||
GetAllQueueRoutingInfoRequestMsg getAllQueueRoutingInfoRequestMsg = 14;
|
GetAllQueueRoutingInfoRequestMsg getAllQueueRoutingInfoRequestMsg = 14;
|
||||||
|
ValidateOrCreateDeviceX509CertRequestMsg validateOrCreateX509CertRequestMsg = 15;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Response from ThingsBoard Core Service to Transport Service */
|
/* Response from ThingsBoard Core Service to Transport Service */
|
||||||
|
|||||||
@ -15,10 +15,10 @@
|
|||||||
*/
|
*/
|
||||||
package org.thingsboard.server.dao.device;
|
package org.thingsboard.server.dao.device;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import org.thingsboard.server.common.data.id.DeviceId;
|
import org.thingsboard.server.common.data.id.DeviceId;
|
||||||
import org.thingsboard.server.common.data.id.TenantId;
|
import org.thingsboard.server.common.data.id.TenantId;
|
||||||
import org.thingsboard.server.common.data.security.DeviceCredentials;
|
import org.thingsboard.server.common.data.security.DeviceCredentials;
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
|
||||||
|
|
||||||
public interface DeviceCredentialsService {
|
public interface DeviceCredentialsService {
|
||||||
|
|
||||||
|
|||||||
@ -39,6 +39,8 @@ public interface DeviceProfileService extends EntityDaoService {
|
|||||||
|
|
||||||
PageData<DeviceProfileInfo> findDeviceProfileInfos(TenantId tenantId, PageLink pageLink, String transportType);
|
PageData<DeviceProfileInfo> findDeviceProfileInfos(TenantId tenantId, PageLink pageLink, String transportType);
|
||||||
|
|
||||||
|
DeviceProfile findDeviceProfileByProvisionDeviceKey(String provisionDeviceKey);
|
||||||
|
|
||||||
DeviceProfile findOrCreateDeviceProfile(TenantId tenantId, String profileName);
|
DeviceProfile findOrCreateDeviceProfile(TenantId tenantId, String profileName);
|
||||||
|
|
||||||
DeviceProfile createDefaultDeviceProfile(TenantId tenantId);
|
DeviceProfile createDefaultDeviceProfile(TenantId tenantId);
|
||||||
|
|||||||
@ -18,5 +18,6 @@ package org.thingsboard.server.common.data;
|
|||||||
public enum DeviceProfileProvisionType {
|
public enum DeviceProfileProvisionType {
|
||||||
DISABLED,
|
DISABLED,
|
||||||
ALLOW_CREATE_NEW_DEVICES,
|
ALLOW_CREATE_NEW_DEVICES,
|
||||||
CHECK_PRE_PROVISIONED_DEVICES
|
CHECK_PRE_PROVISIONED_DEVICES,
|
||||||
|
X509_CERTIFICATE_CHAIN
|
||||||
}
|
}
|
||||||
|
|||||||
@ -31,7 +31,8 @@ import java.io.Serializable;
|
|||||||
@JsonSubTypes({
|
@JsonSubTypes({
|
||||||
@JsonSubTypes.Type(value = DisabledDeviceProfileProvisionConfiguration.class, name = "DISABLED"),
|
@JsonSubTypes.Type(value = DisabledDeviceProfileProvisionConfiguration.class, name = "DISABLED"),
|
||||||
@JsonSubTypes.Type(value = AllowCreateNewDevicesDeviceProfileProvisionConfiguration.class, name = "ALLOW_CREATE_NEW_DEVICES"),
|
@JsonSubTypes.Type(value = AllowCreateNewDevicesDeviceProfileProvisionConfiguration.class, name = "ALLOW_CREATE_NEW_DEVICES"),
|
||||||
@JsonSubTypes.Type(value = CheckPreProvisionedDevicesDeviceProfileProvisionConfiguration.class, name = "CHECK_PRE_PROVISIONED_DEVICES")})
|
@JsonSubTypes.Type(value = CheckPreProvisionedDevicesDeviceProfileProvisionConfiguration.class, name = "CHECK_PRE_PROVISIONED_DEVICES"),
|
||||||
|
@JsonSubTypes.Type(value = X509CertificateChainProvisionConfiguration.class, name = "X509_CERTIFICATE_CHAIN")})
|
||||||
public interface DeviceProfileProvisionConfiguration extends Serializable {
|
public interface DeviceProfileProvisionConfiguration extends Serializable {
|
||||||
|
|
||||||
String getProvisionDeviceSecret();
|
String getProvisionDeviceSecret();
|
||||||
|
|||||||
@ -0,0 +1,36 @@
|
|||||||
|
/**
|
||||||
|
* Copyright © 2016-2023 The Thingsboard Authors
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.thingsboard.server.common.data.device.profile;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import org.thingsboard.server.common.data.DeviceProfileProvisionType;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class X509CertificateChainProvisionConfiguration implements DeviceProfileProvisionConfiguration {
|
||||||
|
|
||||||
|
private String provisionDeviceSecret;
|
||||||
|
private String certificateRegExPattern;
|
||||||
|
private boolean allowCreateNewDevicesByX509Certificate;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DeviceProfileProvisionType getType() {
|
||||||
|
return DeviceProfileProvisionType.X509_CERTIFICATE_CHAIN;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -35,6 +35,14 @@ public class EncryptionUtil {
|
|||||||
.replaceAll("-----END CERTIFICATE-----", "");
|
.replaceAll("-----END CERTIFICATE-----", "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static String certTrimNewLinesForChainInDeviceProfile(String input) {
|
||||||
|
return input.replaceAll("\n", "")
|
||||||
|
.replaceAll("\r", "")
|
||||||
|
.replaceAll("-----BEGIN CERTIFICATE-----", "-----BEGIN CERTIFICATE-----\n")
|
||||||
|
.replaceAll("-----END CERTIFICATE-----", "\n-----END CERTIFICATE-----\n")
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
public static String pubkTrimNewLines(String input) {
|
public static String pubkTrimNewLines(String input) {
|
||||||
return input.replaceAll("-----BEGIN PUBLIC KEY-----", "")
|
return input.replaceAll("-----BEGIN PUBLIC KEY-----", "")
|
||||||
.replaceAll("\n", "")
|
.replaceAll("\n", "")
|
||||||
|
|||||||
@ -24,9 +24,8 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
|||||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.thingsboard.server.common.data.StringUtils;
|
|
||||||
import org.thingsboard.server.common.data.DeviceTransportType;
|
import org.thingsboard.server.common.data.DeviceTransportType;
|
||||||
import org.thingsboard.server.common.msg.EncryptionUtil;
|
import org.thingsboard.server.common.data.StringUtils;
|
||||||
import org.thingsboard.server.common.transport.TransportService;
|
import org.thingsboard.server.common.transport.TransportService;
|
||||||
import org.thingsboard.server.common.transport.TransportServiceCallback;
|
import org.thingsboard.server.common.transport.TransportServiceCallback;
|
||||||
import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse;
|
import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse;
|
||||||
@ -42,7 +41,6 @@ import javax.net.ssl.SSLEngine;
|
|||||||
import javax.net.ssl.TrustManager;
|
import javax.net.ssl.TrustManager;
|
||||||
import javax.net.ssl.TrustManagerFactory;
|
import javax.net.ssl.TrustManagerFactory;
|
||||||
import javax.net.ssl.X509TrustManager;
|
import javax.net.ssl.X509TrustManager;
|
||||||
import java.security.cert.CertificateEncodingException;
|
|
||||||
import java.security.cert.CertificateException;
|
import java.security.cert.CertificateException;
|
||||||
import java.security.cert.X509Certificate;
|
import java.security.cert.X509Certificate;
|
||||||
import java.util.concurrent.CountDownLatch;
|
import java.util.concurrent.CountDownLatch;
|
||||||
@ -123,7 +121,7 @@ public class MqttSslHandlerProvider {
|
|||||||
static class ThingsboardMqttX509TrustManager implements X509TrustManager {
|
static class ThingsboardMqttX509TrustManager implements X509TrustManager {
|
||||||
|
|
||||||
private final X509TrustManager trustManager;
|
private final X509TrustManager trustManager;
|
||||||
private TransportService transportService;
|
private final TransportService transportService;
|
||||||
|
|
||||||
ThingsboardMqttX509TrustManager(X509TrustManager trustManager, TransportService transportService) {
|
ThingsboardMqttX509TrustManager(X509TrustManager trustManager, TransportService transportService) {
|
||||||
this.trustManager = trustManager;
|
this.trustManager = trustManager;
|
||||||
@ -142,17 +140,15 @@ public class MqttSslHandlerProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void checkClientTrusted(X509Certificate[] chain,
|
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
|
||||||
String authType) throws CertificateException {
|
String clientDeviceCertValue = SslUtil.getCertificateString(chain[0]);
|
||||||
String credentialsBody = null;
|
|
||||||
for (X509Certificate cert : chain) {
|
|
||||||
try {
|
|
||||||
String strCert = SslUtil.getCertificateString(cert);
|
|
||||||
String sha3Hash = EncryptionUtil.getSha3Hash(strCert);
|
|
||||||
final String[] credentialsBodyHolder = new String[1];
|
final String[] credentialsBodyHolder = new String[1];
|
||||||
CountDownLatch latch = new CountDownLatch(1);
|
CountDownLatch latch = new CountDownLatch(1);
|
||||||
transportService.process(DeviceTransportType.MQTT, TransportProtos.ValidateDeviceX509CertRequestMsg.newBuilder().setHash(sha3Hash).build(),
|
try {
|
||||||
new TransportServiceCallback<ValidateDeviceCredentialsResponse>() {
|
String certificateChain = SslUtil.getCertificateChainString(chain);
|
||||||
|
transportService.process(DeviceTransportType.MQTT, TransportProtos.ValidateOrCreateDeviceX509CertRequestMsg
|
||||||
|
.newBuilder().setCertificateChain(certificateChain).build(),
|
||||||
|
new TransportServiceCallback<>() {
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(ValidateDeviceCredentialsResponse msg) {
|
public void onSuccess(ValidateDeviceCredentialsResponse msg) {
|
||||||
if (!StringUtils.isEmpty(msg.getCredentials())) {
|
if (!StringUtils.isEmpty(msg.getCredentials())) {
|
||||||
@ -163,23 +159,22 @@ public class MqttSslHandlerProvider {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onError(Throwable e) {
|
public void onError(Throwable e) {
|
||||||
log.error(e.getMessage(), e);
|
log.trace("Failed to process certificate chain: {}", certificateChain, e);
|
||||||
latch.countDown();
|
latch.countDown();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
latch.await(10, TimeUnit.SECONDS);
|
latch.await(10, TimeUnit.SECONDS);
|
||||||
if (strCert.equals(credentialsBodyHolder[0])) {
|
if (!clientDeviceCertValue.equals(credentialsBodyHolder[0])) {
|
||||||
credentialsBody = credentialsBodyHolder[0];
|
log.debug("Failed to find credentials for device certificate chain: {}", chain);
|
||||||
break;
|
if (chain.length == 1) {
|
||||||
|
throw new CertificateException("Invalid Device Certificate");
|
||||||
|
} else {
|
||||||
|
throw new CertificateException("Invalid Chain of X509 Certificates");
|
||||||
}
|
}
|
||||||
} catch (InterruptedException | CertificateEncodingException e) {
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
log.error(e.getMessage(), e);
|
log.error(e.getMessage(), e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (credentialsBody == null) {
|
|
||||||
log.debug("Failed to find credentials for device certificate chain: {}", chain);
|
|
||||||
throw new CertificateException("Invalid Device Certificate");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -54,8 +54,9 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToServerRpcRequestMs
|
|||||||
import org.thingsboard.server.gen.transport.TransportProtos.TransportToDeviceActorMsg;
|
import org.thingsboard.server.gen.transport.TransportProtos.TransportToDeviceActorMsg;
|
||||||
import org.thingsboard.server.gen.transport.TransportProtos.ValidateBasicMqttCredRequestMsg;
|
import org.thingsboard.server.gen.transport.TransportProtos.ValidateBasicMqttCredRequestMsg;
|
||||||
import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceLwM2MCredentialsRequestMsg;
|
import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceLwM2MCredentialsRequestMsg;
|
||||||
import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceTokenRequestMsg;
|
|
||||||
import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceX509CertRequestMsg;
|
import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceX509CertRequestMsg;
|
||||||
|
import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceTokenRequestMsg;
|
||||||
|
import org.thingsboard.server.gen.transport.TransportProtos.ValidateOrCreateDeviceX509CertRequestMsg;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
@ -87,6 +88,9 @@ public interface TransportService {
|
|||||||
void process(DeviceTransportType transportType, ValidateDeviceX509CertRequestMsg msg,
|
void process(DeviceTransportType transportType, ValidateDeviceX509CertRequestMsg msg,
|
||||||
TransportServiceCallback<ValidateDeviceCredentialsResponse> callback);
|
TransportServiceCallback<ValidateDeviceCredentialsResponse> callback);
|
||||||
|
|
||||||
|
void process(DeviceTransportType transportType, ValidateOrCreateDeviceX509CertRequestMsg msg,
|
||||||
|
TransportServiceCallback<ValidateDeviceCredentialsResponse> callback);
|
||||||
|
|
||||||
void process(ValidateDeviceLwM2MCredentialsRequestMsg msg,
|
void process(ValidateDeviceLwM2MCredentialsRequestMsg msg,
|
||||||
TransportServiceCallback<ValidateDeviceCredentialsResponse> callback);
|
TransportServiceCallback<ValidateDeviceCredentialsResponse> callback);
|
||||||
|
|
||||||
|
|||||||
@ -72,7 +72,6 @@ import org.thingsboard.server.common.transport.auth.GetOrCreateDeviceFromGateway
|
|||||||
import org.thingsboard.server.common.transport.auth.TransportDeviceInfo;
|
import org.thingsboard.server.common.transport.auth.TransportDeviceInfo;
|
||||||
import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse;
|
import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse;
|
||||||
import org.thingsboard.server.common.transport.limits.TransportRateLimitService;
|
import org.thingsboard.server.common.transport.limits.TransportRateLimitService;
|
||||||
import org.thingsboard.server.queue.util.DataDecodingEncodingService;
|
|
||||||
import org.thingsboard.server.common.transport.util.JsonUtils;
|
import org.thingsboard.server.common.transport.util.JsonUtils;
|
||||||
import org.thingsboard.server.gen.transport.TransportProtos;
|
import org.thingsboard.server.gen.transport.TransportProtos;
|
||||||
import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceRequestMsg;
|
import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceRequestMsg;
|
||||||
@ -97,6 +96,7 @@ import org.thingsboard.server.queue.provider.TbQueueProducerProvider;
|
|||||||
import org.thingsboard.server.queue.provider.TbTransportQueueFactory;
|
import org.thingsboard.server.queue.provider.TbTransportQueueFactory;
|
||||||
import org.thingsboard.server.queue.scheduler.SchedulerComponent;
|
import org.thingsboard.server.queue.scheduler.SchedulerComponent;
|
||||||
import org.thingsboard.server.queue.util.AfterStartUp;
|
import org.thingsboard.server.queue.util.AfterStartUp;
|
||||||
|
import org.thingsboard.server.queue.util.DataDecodingEncodingService;
|
||||||
import org.thingsboard.server.queue.util.TbTransportComponent;
|
import org.thingsboard.server.queue.util.TbTransportComponent;
|
||||||
|
|
||||||
import javax.annotation.PostConstruct;
|
import javax.annotation.PostConstruct;
|
||||||
@ -434,6 +434,13 @@ public class DefaultTransportService implements TransportService {
|
|||||||
doProcess(transportType, protoMsg, callback);
|
doProcess(transportType, protoMsg, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void process(DeviceTransportType transportType, TransportProtos.ValidateOrCreateDeviceX509CertRequestMsg msg, TransportServiceCallback<ValidateDeviceCredentialsResponse> callback) {
|
||||||
|
log.trace("Processing msg: {}", msg);
|
||||||
|
TbProtoQueueMsg<TransportApiRequestMsg> protoMsg = new TbProtoQueueMsg<>(UUID.randomUUID(), TransportApiRequestMsg.newBuilder().setValidateOrCreateX509CertRequestMsg(msg).build());
|
||||||
|
doProcess(transportType, protoMsg, callback);
|
||||||
|
}
|
||||||
|
|
||||||
private void doProcess(DeviceTransportType transportType, TbProtoQueueMsg<TransportApiRequestMsg> protoMsg,
|
private void doProcess(DeviceTransportType transportType, TbProtoQueueMsg<TransportApiRequestMsg> protoMsg,
|
||||||
TransportServiceCallback<ValidateDeviceCredentialsResponse> callback) {
|
TransportServiceCallback<ValidateDeviceCredentialsResponse> callback) {
|
||||||
ListenableFuture<ValidateDeviceCredentialsResponse> response = Futures.transform(transportApiRequestTemplate.send(protoMsg), tmp -> {
|
ListenableFuture<ValidateDeviceCredentialsResponse> response = Futures.transform(transportApiRequestTemplate.send(protoMsg), tmp -> {
|
||||||
|
|||||||
@ -16,11 +16,21 @@
|
|||||||
package org.thingsboard.server.common.transport.util;
|
package org.thingsboard.server.common.transport.util;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.codec.binary.Base64;
|
||||||
|
import org.bouncycastle.asn1.x500.RDN;
|
||||||
|
import org.bouncycastle.asn1.x500.X500Name;
|
||||||
|
import org.bouncycastle.asn1.x500.style.BCStyle;
|
||||||
|
import org.bouncycastle.asn1.x500.style.IETFUtils;
|
||||||
|
import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder;
|
||||||
import org.springframework.util.Base64Utils;
|
import org.springframework.util.Base64Utils;
|
||||||
import org.thingsboard.server.common.msg.EncryptionUtil;
|
import org.thingsboard.server.common.msg.EncryptionUtil;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.InputStream;
|
||||||
import java.security.cert.Certificate;
|
import java.security.cert.Certificate;
|
||||||
import java.security.cert.CertificateEncodingException;
|
import java.security.cert.CertificateEncodingException;
|
||||||
|
import java.security.cert.CertificateFactory;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Valerii Sosliuk
|
* @author Valerii Sosliuk
|
||||||
@ -35,4 +45,44 @@ public class SslUtil {
|
|||||||
throws CertificateEncodingException {
|
throws CertificateEncodingException {
|
||||||
return EncryptionUtil.certTrimNewLines(Base64Utils.encodeToString(cert.getEncoded()));
|
return EncryptionUtil.certTrimNewLines(Base64Utils.encodeToString(cert.getEncoded()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static String getCertificateChainString(Certificate[] chain)
|
||||||
|
throws CertificateEncodingException {
|
||||||
|
String begin = "-----BEGIN CERTIFICATE-----";
|
||||||
|
String end = "-----END CERTIFICATE-----";
|
||||||
|
StringBuilder stringBuilder = new StringBuilder();
|
||||||
|
for (Certificate cert: chain) {
|
||||||
|
stringBuilder.append(begin).append(EncryptionUtil.certTrimNewLines(Base64Utils.encodeToString(cert.getEncoded()))).append(end).append("\n");
|
||||||
|
}
|
||||||
|
return stringBuilder.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static X509Certificate readCertFile(String fileContent) {
|
||||||
|
X509Certificate certificate = null;
|
||||||
|
try {
|
||||||
|
if (fileContent != null && !fileContent.trim().isEmpty()) {
|
||||||
|
fileContent = fileContent.replace("-----BEGIN CERTIFICATE-----", "")
|
||||||
|
.replace("-----END CERTIFICATE-----", "")
|
||||||
|
.replaceAll("\\s", "");
|
||||||
|
byte[] decoded = Base64.decodeBase64(fileContent);
|
||||||
|
CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
|
||||||
|
try (InputStream inStream = new ByteArrayInputStream(decoded)) {
|
||||||
|
certificate = (X509Certificate) certFactory.generateCertificate(inStream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
return certificate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String parseCommonName(X509Certificate certificate) {
|
||||||
|
X500Name x500name;
|
||||||
|
try {
|
||||||
|
x500name = new JcaX509CertificateHolder(certificate).getSubject();
|
||||||
|
} catch (CertificateEncodingException e) {
|
||||||
|
log.warn("Cannot parse CN from device certificate");
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
RDN cn = x500name.getRDNs(BCStyle.CN)[0];
|
||||||
|
return IETFUtils.valueToString(cn.getFirst().getValue());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,15 +16,12 @@
|
|||||||
package org.thingsboard.server.dao.device;
|
package org.thingsboard.server.dao.device;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.eclipse.leshan.core.SecurityMode;
|
import org.eclipse.leshan.core.SecurityMode;
|
||||||
import org.eclipse.leshan.core.util.SecurityUtil;
|
import org.eclipse.leshan.core.util.SecurityUtil;
|
||||||
import org.hibernate.exception.ConstraintViolationException;
|
import org.hibernate.exception.ConstraintViolationException;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Propagation;
|
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
|
||||||
import org.springframework.transaction.event.TransactionalEventListener;
|
import org.springframework.transaction.event.TransactionalEventListener;
|
||||||
import org.thingsboard.common.util.JacksonUtil;
|
import org.thingsboard.common.util.JacksonUtil;
|
||||||
import org.thingsboard.server.common.data.StringUtils;
|
import org.thingsboard.server.common.data.StringUtils;
|
||||||
|
|||||||
@ -16,6 +16,7 @@
|
|||||||
package org.thingsboard.server.dao.device;
|
package org.thingsboard.server.dao.device;
|
||||||
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
import org.thingsboard.server.common.data.StringUtils;
|
||||||
import org.thingsboard.server.common.data.id.DeviceProfileId;
|
import org.thingsboard.server.common.data.id.DeviceProfileId;
|
||||||
import org.thingsboard.server.common.data.id.TenantId;
|
import org.thingsboard.server.common.data.id.TenantId;
|
||||||
|
|
||||||
@ -30,34 +31,44 @@ public class DeviceProfileCacheKey implements Serializable {
|
|||||||
private final String name;
|
private final String name;
|
||||||
private final DeviceProfileId deviceProfileId;
|
private final DeviceProfileId deviceProfileId;
|
||||||
private final boolean defaultProfile;
|
private final boolean defaultProfile;
|
||||||
|
private final String provisionDeviceKey;
|
||||||
|
|
||||||
private DeviceProfileCacheKey(TenantId tenantId, String name, DeviceProfileId deviceProfileId, boolean defaultProfile) {
|
private DeviceProfileCacheKey(TenantId tenantId, String name, DeviceProfileId deviceProfileId, boolean defaultProfile, String provisionDeviceKey) {
|
||||||
this.tenantId = tenantId;
|
this.tenantId = tenantId;
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.deviceProfileId = deviceProfileId;
|
this.deviceProfileId = deviceProfileId;
|
||||||
this.defaultProfile = defaultProfile;
|
this.defaultProfile = defaultProfile;
|
||||||
|
this.provisionDeviceKey = provisionDeviceKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static DeviceProfileCacheKey fromName(TenantId tenantId, String name) {
|
public static DeviceProfileCacheKey fromName(TenantId tenantId, String name) {
|
||||||
return new DeviceProfileCacheKey(tenantId, name, null, false);
|
return new DeviceProfileCacheKey(tenantId, name, null, false, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static DeviceProfileCacheKey fromId(DeviceProfileId id) {
|
public static DeviceProfileCacheKey fromId(DeviceProfileId id) {
|
||||||
return new DeviceProfileCacheKey(null, null, id, false);
|
return new DeviceProfileCacheKey(null, null, id, false, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static DeviceProfileCacheKey defaultProfile(TenantId tenantId) {
|
public static DeviceProfileCacheKey defaultProfile(TenantId tenantId) {
|
||||||
return new DeviceProfileCacheKey(tenantId, null, null, true);
|
return new DeviceProfileCacheKey(tenantId, null, null, true, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static DeviceProfileCacheKey fromProvisionDeviceKey(String provisionDeviceKey) {
|
||||||
|
return new DeviceProfileCacheKey(null, null, null, false, provisionDeviceKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* IMPORTANT: Method toString() has to return unique value, if you add additional field to this class, please also refactor toString().
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
if (deviceProfileId != null) {
|
if (deviceProfileId != null) {
|
||||||
return deviceProfileId.toString();
|
return deviceProfileId.toString();
|
||||||
} else if (defaultProfile) {
|
} else if (defaultProfile) {
|
||||||
return tenantId.toString();
|
return tenantId.toString();
|
||||||
} else {
|
} else if (StringUtils.isNotEmpty(provisionDeviceKey)) {
|
||||||
|
return provisionDeviceKey;
|
||||||
|
}
|
||||||
return tenantId + "_" + name;
|
return tenantId + "_" + name;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
@ -27,5 +27,6 @@ public class DeviceProfileEvictEvent {
|
|||||||
private final String oldName;
|
private final String oldName;
|
||||||
private final DeviceProfileId deviceProfileId;
|
private final DeviceProfileId deviceProfileId;
|
||||||
private final boolean defaultProfile;
|
private final boolean defaultProfile;
|
||||||
|
private final String provisionDeviceKey;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -34,12 +34,14 @@ import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileCon
|
|||||||
import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileTransportConfiguration;
|
import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileTransportConfiguration;
|
||||||
import org.thingsboard.server.common.data.device.profile.DeviceProfileData;
|
import org.thingsboard.server.common.data.device.profile.DeviceProfileData;
|
||||||
import org.thingsboard.server.common.data.device.profile.DisabledDeviceProfileProvisionConfiguration;
|
import org.thingsboard.server.common.data.device.profile.DisabledDeviceProfileProvisionConfiguration;
|
||||||
|
import org.thingsboard.server.common.data.device.profile.X509CertificateChainProvisionConfiguration;
|
||||||
import org.thingsboard.server.common.data.id.DeviceProfileId;
|
import org.thingsboard.server.common.data.id.DeviceProfileId;
|
||||||
import org.thingsboard.server.common.data.id.EntityId;
|
import org.thingsboard.server.common.data.id.EntityId;
|
||||||
import org.thingsboard.server.common.data.id.HasId;
|
import org.thingsboard.server.common.data.id.HasId;
|
||||||
import org.thingsboard.server.common.data.id.TenantId;
|
import org.thingsboard.server.common.data.id.TenantId;
|
||||||
import org.thingsboard.server.common.data.page.PageData;
|
import org.thingsboard.server.common.data.page.PageData;
|
||||||
import org.thingsboard.server.common.data.page.PageLink;
|
import org.thingsboard.server.common.data.page.PageLink;
|
||||||
|
import org.thingsboard.server.common.msg.EncryptionUtil;
|
||||||
import org.thingsboard.server.dao.entity.AbstractCachedEntityService;
|
import org.thingsboard.server.dao.entity.AbstractCachedEntityService;
|
||||||
import org.thingsboard.server.dao.exception.DataValidationException;
|
import org.thingsboard.server.dao.exception.DataValidationException;
|
||||||
import org.thingsboard.server.dao.queue.QueueService;
|
import org.thingsboard.server.dao.queue.QueueService;
|
||||||
@ -47,12 +49,19 @@ import org.thingsboard.server.dao.service.DataValidator;
|
|||||||
import org.thingsboard.server.dao.service.PaginatedRemover;
|
import org.thingsboard.server.dao.service.PaginatedRemover;
|
||||||
import org.thingsboard.server.dao.service.Validator;
|
import org.thingsboard.server.dao.service.Validator;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.security.cert.Certificate;
|
||||||
|
import java.security.cert.CertificateException;
|
||||||
|
import java.security.cert.CertificateFactory;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
import static org.thingsboard.server.dao.service.Validator.validateId;
|
import static org.thingsboard.server.dao.service.Validator.validateId;
|
||||||
|
import static org.thingsboard.server.dao.service.Validator.validateString;
|
||||||
|
|
||||||
@Service("DeviceProfileDaoService")
|
@Service("DeviceProfileDaoService")
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@ -61,6 +70,7 @@ public class DeviceProfileServiceImpl extends AbstractCachedEntityService<Device
|
|||||||
private static final String INCORRECT_TENANT_ID = "Incorrect tenantId ";
|
private static final String INCORRECT_TENANT_ID = "Incorrect tenantId ";
|
||||||
private static final String INCORRECT_DEVICE_PROFILE_ID = "Incorrect deviceProfileId ";
|
private static final String INCORRECT_DEVICE_PROFILE_ID = "Incorrect deviceProfileId ";
|
||||||
private static final String INCORRECT_DEVICE_PROFILE_NAME = "Incorrect deviceProfileName ";
|
private static final String INCORRECT_DEVICE_PROFILE_NAME = "Incorrect deviceProfileName ";
|
||||||
|
private static final String INCORRECT_PROVISION_DEVICE_KEY = "Incorrect provisionDeviceKey ";
|
||||||
private static final String DEVICE_PROFILE_WITH_SUCH_NAME_ALREADY_EXISTS = "Device profile with such name already exists!";
|
private static final String DEVICE_PROFILE_WITH_SUCH_NAME_ALREADY_EXISTS = "Device profile with such name already exists!";
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
@ -93,13 +103,16 @@ public class DeviceProfileServiceImpl extends AbstractCachedEntityService<Device
|
|||||||
if (StringUtils.isNotEmpty(event.getOldName()) && !event.getOldName().equals(event.getNewName())) {
|
if (StringUtils.isNotEmpty(event.getOldName()) && !event.getOldName().equals(event.getNewName())) {
|
||||||
keys.add(DeviceProfileCacheKey.fromName(event.getTenantId(), event.getOldName()));
|
keys.add(DeviceProfileCacheKey.fromName(event.getTenantId(), event.getOldName()));
|
||||||
}
|
}
|
||||||
|
if (StringUtils.isNotEmpty(event.getProvisionDeviceKey())) {
|
||||||
|
keys.add(DeviceProfileCacheKey.fromProvisionDeviceKey(event.getProvisionDeviceKey()));
|
||||||
|
}
|
||||||
cache.evict(keys);
|
cache.evict(keys);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public DeviceProfile findDeviceProfileById(TenantId tenantId, DeviceProfileId deviceProfileId) {
|
public DeviceProfile findDeviceProfileById(TenantId tenantId, DeviceProfileId deviceProfileId) {
|
||||||
log.trace("Executing findDeviceProfileById [{}]", deviceProfileId);
|
log.trace("Executing findDeviceProfileById [{}]", deviceProfileId);
|
||||||
Validator.validateId(deviceProfileId, INCORRECT_DEVICE_PROFILE_ID + deviceProfileId);
|
validateId(deviceProfileId, INCORRECT_DEVICE_PROFILE_ID + deviceProfileId);
|
||||||
return cache.getAndPutInTransaction(DeviceProfileCacheKey.fromId(deviceProfileId),
|
return cache.getAndPutInTransaction(DeviceProfileCacheKey.fromId(deviceProfileId),
|
||||||
() -> deviceProfileDao.findById(tenantId, deviceProfileId.getId()), true);
|
() -> deviceProfileDao.findById(tenantId, deviceProfileId.getId()), true);
|
||||||
}
|
}
|
||||||
@ -107,30 +120,46 @@ public class DeviceProfileServiceImpl extends AbstractCachedEntityService<Device
|
|||||||
@Override
|
@Override
|
||||||
public DeviceProfile findDeviceProfileByName(TenantId tenantId, String profileName) {
|
public DeviceProfile findDeviceProfileByName(TenantId tenantId, String profileName) {
|
||||||
log.trace("Executing findDeviceProfileByName [{}][{}]", tenantId, profileName);
|
log.trace("Executing findDeviceProfileByName [{}][{}]", tenantId, profileName);
|
||||||
Validator.validateString(profileName, INCORRECT_DEVICE_PROFILE_NAME + profileName);
|
validateString(profileName, INCORRECT_DEVICE_PROFILE_NAME + profileName);
|
||||||
return cache.getAndPutInTransaction(DeviceProfileCacheKey.fromName(tenantId, profileName),
|
return cache.getAndPutInTransaction(DeviceProfileCacheKey.fromName(tenantId, profileName),
|
||||||
() -> deviceProfileDao.findByName(tenantId, profileName), true);
|
() -> deviceProfileDao.findByName(tenantId, profileName), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DeviceProfile findDeviceProfileByProvisionDeviceKey(String provisionDeviceKey) {
|
||||||
|
log.trace("Executing findDeviceProfileByProvisionDeviceKey provisionKey [{}]", provisionDeviceKey);
|
||||||
|
validateString(provisionDeviceKey, INCORRECT_PROVISION_DEVICE_KEY + provisionDeviceKey);
|
||||||
|
return cache.getAndPutInTransaction(DeviceProfileCacheKey.fromProvisionDeviceKey(provisionDeviceKey),
|
||||||
|
() -> deviceProfileDao.findByProvisionDeviceKey(provisionDeviceKey), false);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public DeviceProfileInfo findDeviceProfileInfoById(TenantId tenantId, DeviceProfileId deviceProfileId) {
|
public DeviceProfileInfo findDeviceProfileInfoById(TenantId tenantId, DeviceProfileId deviceProfileId) {
|
||||||
log.trace("Executing findDeviceProfileById [{}]", deviceProfileId);
|
log.trace("Executing findDeviceProfileById [{}]", deviceProfileId);
|
||||||
Validator.validateId(deviceProfileId, INCORRECT_DEVICE_PROFILE_ID + deviceProfileId);
|
validateId(deviceProfileId, INCORRECT_DEVICE_PROFILE_ID + deviceProfileId);
|
||||||
return toDeviceProfileInfo(findDeviceProfileById(tenantId, deviceProfileId));
|
return toDeviceProfileInfo(findDeviceProfileById(tenantId, deviceProfileId));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public DeviceProfile saveDeviceProfile(DeviceProfile deviceProfile) {
|
public DeviceProfile saveDeviceProfile(DeviceProfile deviceProfile) {
|
||||||
log.trace("Executing saveDeviceProfile [{}]", deviceProfile);
|
log.trace("Executing saveDeviceProfile [{}]", deviceProfile);
|
||||||
|
if (deviceProfile.getProfileData() != null && deviceProfile.getProfileData().getProvisionConfiguration() instanceof X509CertificateChainProvisionConfiguration) {
|
||||||
|
X509CertificateChainProvisionConfiguration x509Configuration = (X509CertificateChainProvisionConfiguration) deviceProfile.getProfileData().getProvisionConfiguration();
|
||||||
|
if (x509Configuration.getProvisionDeviceSecret() != null) {
|
||||||
|
formatDeviceProfileCertificate(deviceProfile, x509Configuration);
|
||||||
|
}
|
||||||
|
}
|
||||||
DeviceProfile oldDeviceProfile = deviceProfileValidator.validate(deviceProfile, DeviceProfile::getTenantId);
|
DeviceProfile oldDeviceProfile = deviceProfileValidator.validate(deviceProfile, DeviceProfile::getTenantId);
|
||||||
DeviceProfile savedDeviceProfile;
|
DeviceProfile savedDeviceProfile;
|
||||||
try {
|
try {
|
||||||
savedDeviceProfile = deviceProfileDao.saveAndFlush(deviceProfile.getTenantId(), deviceProfile);
|
savedDeviceProfile = deviceProfileDao.saveAndFlush(deviceProfile.getTenantId(), deviceProfile);
|
||||||
publishEvictEvent(new DeviceProfileEvictEvent(savedDeviceProfile.getTenantId(), savedDeviceProfile.getName(),
|
publishEvictEvent(new DeviceProfileEvictEvent(savedDeviceProfile.getTenantId(), savedDeviceProfile.getName(),
|
||||||
oldDeviceProfile != null ? oldDeviceProfile.getName() : null, savedDeviceProfile.getId(), savedDeviceProfile.isDefault()));
|
oldDeviceProfile != null ? oldDeviceProfile.getName() : null, savedDeviceProfile.getId(), savedDeviceProfile.isDefault(),
|
||||||
|
oldDeviceProfile != null ? oldDeviceProfile.getProvisionDeviceKey() : null));
|
||||||
} catch (Exception t) {
|
} catch (Exception t) {
|
||||||
handleEvictEvent(new DeviceProfileEvictEvent(deviceProfile.getTenantId(), deviceProfile.getName(),
|
handleEvictEvent(new DeviceProfileEvictEvent(deviceProfile.getTenantId(), deviceProfile.getName(),
|
||||||
oldDeviceProfile != null ? oldDeviceProfile.getName() : null, null, deviceProfile.isDefault()));
|
oldDeviceProfile != null ? oldDeviceProfile.getName() : null, null, deviceProfile.isDefault(),
|
||||||
|
oldDeviceProfile != null ? oldDeviceProfile.getProvisionDeviceKey() : null));
|
||||||
checkConstraintViolation(t,
|
checkConstraintViolation(t,
|
||||||
Map.of("device_profile_name_unq_key", DEVICE_PROFILE_WITH_SUCH_NAME_ALREADY_EXISTS,
|
Map.of("device_profile_name_unq_key", DEVICE_PROFILE_WITH_SUCH_NAME_ALREADY_EXISTS,
|
||||||
"device_provision_key_unq_key", "Device profile with such provision device key already exists!",
|
"device_provision_key_unq_key", "Device profile with such provision device key already exists!",
|
||||||
@ -156,7 +185,7 @@ public class DeviceProfileServiceImpl extends AbstractCachedEntityService<Device
|
|||||||
@Transactional
|
@Transactional
|
||||||
public void deleteDeviceProfile(TenantId tenantId, DeviceProfileId deviceProfileId) {
|
public void deleteDeviceProfile(TenantId tenantId, DeviceProfileId deviceProfileId) {
|
||||||
log.trace("Executing deleteDeviceProfile [{}]", deviceProfileId);
|
log.trace("Executing deleteDeviceProfile [{}]", deviceProfileId);
|
||||||
Validator.validateId(deviceProfileId, INCORRECT_DEVICE_PROFILE_ID + deviceProfileId);
|
validateId(deviceProfileId, INCORRECT_DEVICE_PROFILE_ID + deviceProfileId);
|
||||||
DeviceProfile deviceProfile = deviceProfileDao.findById(tenantId, deviceProfileId.getId());
|
DeviceProfile deviceProfile = deviceProfileDao.findById(tenantId, deviceProfileId.getId());
|
||||||
if (deviceProfile != null && deviceProfile.isDefault()) {
|
if (deviceProfile != null && deviceProfile.isDefault()) {
|
||||||
throw new DataValidationException("Deletion of Default Device Profile is prohibited!");
|
throw new DataValidationException("Deletion of Default Device Profile is prohibited!");
|
||||||
@ -170,7 +199,8 @@ public class DeviceProfileServiceImpl extends AbstractCachedEntityService<Device
|
|||||||
deleteEntityRelations(tenantId, deviceProfileId);
|
deleteEntityRelations(tenantId, deviceProfileId);
|
||||||
deviceProfileDao.removeById(tenantId, deviceProfileId.getId());
|
deviceProfileDao.removeById(tenantId, deviceProfileId.getId());
|
||||||
publishEvictEvent(new DeviceProfileEvictEvent(deviceProfile.getTenantId(), deviceProfile.getName(),
|
publishEvictEvent(new DeviceProfileEvictEvent(deviceProfile.getTenantId(), deviceProfile.getName(),
|
||||||
null, deviceProfile.getId(), deviceProfile.isDefault()));
|
null, deviceProfile.getId(), deviceProfile.isDefault(),
|
||||||
|
deviceProfile.getProvisionDeviceKey()));
|
||||||
} catch (Exception t) {
|
} catch (Exception t) {
|
||||||
ConstraintViolationException e = extractConstraintViolationException(t).orElse(null);
|
ConstraintViolationException e = extractConstraintViolationException(t).orElse(null);
|
||||||
if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("fk_device_profile")) {
|
if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("fk_device_profile")) {
|
||||||
@ -260,7 +290,7 @@ public class DeviceProfileServiceImpl extends AbstractCachedEntityService<Device
|
|||||||
@Override
|
@Override
|
||||||
public boolean setDefaultDeviceProfile(TenantId tenantId, DeviceProfileId deviceProfileId) {
|
public boolean setDefaultDeviceProfile(TenantId tenantId, DeviceProfileId deviceProfileId) {
|
||||||
log.trace("Executing setDefaultDeviceProfile [{}]", deviceProfileId);
|
log.trace("Executing setDefaultDeviceProfile [{}]", deviceProfileId);
|
||||||
Validator.validateId(deviceProfileId, INCORRECT_DEVICE_PROFILE_ID + deviceProfileId);
|
validateId(deviceProfileId, INCORRECT_DEVICE_PROFILE_ID + deviceProfileId);
|
||||||
DeviceProfile deviceProfile = deviceProfileDao.findById(tenantId, deviceProfileId.getId());
|
DeviceProfile deviceProfile = deviceProfileDao.findById(tenantId, deviceProfileId.getId());
|
||||||
if (!deviceProfile.isDefault()) {
|
if (!deviceProfile.isDefault()) {
|
||||||
deviceProfile.setDefault(true);
|
deviceProfile.setDefault(true);
|
||||||
@ -268,14 +298,14 @@ public class DeviceProfileServiceImpl extends AbstractCachedEntityService<Device
|
|||||||
boolean changed = false;
|
boolean changed = false;
|
||||||
if (previousDefaultDeviceProfile == null) {
|
if (previousDefaultDeviceProfile == null) {
|
||||||
deviceProfileDao.save(tenantId, deviceProfile);
|
deviceProfileDao.save(tenantId, deviceProfile);
|
||||||
publishEvictEvent(new DeviceProfileEvictEvent(deviceProfile.getTenantId(), deviceProfile.getName(), null, deviceProfile.getId(), true));
|
publishEvictEvent(new DeviceProfileEvictEvent(deviceProfile.getTenantId(), deviceProfile.getName(), null, deviceProfile.getId(), true, deviceProfile.getProvisionDeviceKey()));
|
||||||
changed = true;
|
changed = true;
|
||||||
} else if (!previousDefaultDeviceProfile.getId().equals(deviceProfile.getId())) {
|
} else if (!previousDefaultDeviceProfile.getId().equals(deviceProfile.getId())) {
|
||||||
previousDefaultDeviceProfile.setDefault(false);
|
previousDefaultDeviceProfile.setDefault(false);
|
||||||
deviceProfileDao.save(tenantId, previousDefaultDeviceProfile);
|
deviceProfileDao.save(tenantId, previousDefaultDeviceProfile);
|
||||||
deviceProfileDao.save(tenantId, deviceProfile);
|
deviceProfileDao.save(tenantId, deviceProfile);
|
||||||
publishEvictEvent(new DeviceProfileEvictEvent(previousDefaultDeviceProfile.getTenantId(), previousDefaultDeviceProfile.getName(), null, previousDefaultDeviceProfile.getId(), false));
|
publishEvictEvent(new DeviceProfileEvictEvent(previousDefaultDeviceProfile.getTenantId(), previousDefaultDeviceProfile.getName(), null, previousDefaultDeviceProfile.getId(), false, deviceProfile.getProvisionDeviceKey()));
|
||||||
publishEvictEvent(new DeviceProfileEvictEvent(deviceProfile.getTenantId(), deviceProfile.getName(), null, deviceProfile.getId(), true));
|
publishEvictEvent(new DeviceProfileEvictEvent(deviceProfile.getTenantId(), deviceProfile.getName(), null, deviceProfile.getId(), true, deviceProfile.getProvisionDeviceKey()));
|
||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
return changed;
|
return changed;
|
||||||
@ -319,4 +349,38 @@ public class DeviceProfileServiceImpl extends AbstractCachedEntityService<Device
|
|||||||
profile.getDefaultDashboardId(), profile.getType(), profile.getTransportType());
|
profile.getDefaultDashboardId(), profile.getType(), profile.getTransportType());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void formatDeviceProfileCertificate(DeviceProfile deviceProfile, X509CertificateChainProvisionConfiguration x509Configuration) {
|
||||||
|
String formattedCertificateValue = formatCertificateValue(x509Configuration.getProvisionDeviceSecret());
|
||||||
|
String cert = fetchLeafCertificateFromChain(formattedCertificateValue);
|
||||||
|
String sha3Hash = EncryptionUtil.getSha3Hash(cert);
|
||||||
|
DeviceProfileData deviceProfileData = deviceProfile.getProfileData();
|
||||||
|
x509Configuration.setProvisionDeviceSecret(formattedCertificateValue);
|
||||||
|
deviceProfileData.setProvisionConfiguration(x509Configuration);
|
||||||
|
deviceProfile.setProfileData(deviceProfileData);
|
||||||
|
deviceProfile.setProvisionDeviceKey(sha3Hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String fetchLeafCertificateFromChain(String value) {
|
||||||
|
String regex = "-----BEGIN CERTIFICATE-----\\s*.*?\\s*-----END CERTIFICATE-----";
|
||||||
|
Pattern pattern = Pattern.compile(regex);
|
||||||
|
Matcher matcher = pattern.matcher(value);
|
||||||
|
if (matcher.find()) {
|
||||||
|
// if the method receives a chain it fetches the leaf (end-entity) certificate, else if it gets a single certificate, it returns the single certificate
|
||||||
|
return matcher.group(0);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String formatCertificateValue(String certificateValue) {
|
||||||
|
try {
|
||||||
|
CertificateFactory cf = CertificateFactory.getInstance("X.509");
|
||||||
|
ByteArrayInputStream inputStream = new ByteArrayInputStream(certificateValue.getBytes());
|
||||||
|
Certificate[] certificates = cf.generateCertificates(inputStream).toArray(new Certificate[0]);
|
||||||
|
if (certificates.length > 1) {
|
||||||
|
return EncryptionUtil.certTrimNewLinesForChainInDeviceProfile(certificateValue);
|
||||||
|
}
|
||||||
|
} catch (CertificateException ignored) {}
|
||||||
|
return EncryptionUtil.certTrimNewLines(certificateValue);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,8 +19,10 @@ import com.google.protobuf.Descriptors;
|
|||||||
import com.google.protobuf.DynamicMessage;
|
import com.google.protobuf.DynamicMessage;
|
||||||
import org.eclipse.leshan.core.util.SecurityUtil;
|
import org.eclipse.leshan.core.util.SecurityUtil;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.context.annotation.Lazy;
|
import org.springframework.context.annotation.Lazy;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.util.Base64Utils;
|
||||||
import org.springframework.util.CollectionUtils;
|
import org.springframework.util.CollectionUtils;
|
||||||
import org.thingsboard.server.common.data.DashboardInfo;
|
import org.thingsboard.server.common.data.DashboardInfo;
|
||||||
import org.thingsboard.server.common.data.DeviceProfile;
|
import org.thingsboard.server.common.data.DeviceProfile;
|
||||||
@ -55,6 +57,17 @@ import org.thingsboard.server.dao.queue.QueueService;
|
|||||||
import org.thingsboard.server.dao.rule.RuleChainService;
|
import org.thingsboard.server.dao.rule.RuleChainService;
|
||||||
import org.thingsboard.server.dao.tenant.TenantService;
|
import org.thingsboard.server.dao.tenant.TenantService;
|
||||||
|
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.security.InvalidAlgorithmParameterException;
|
||||||
|
import java.security.KeyStore;
|
||||||
|
import java.security.KeyStoreException;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.cert.CertificateEncodingException;
|
||||||
|
import java.security.cert.CertificateException;
|
||||||
|
import java.security.cert.PKIXParameters;
|
||||||
|
import java.security.cert.TrustAnchor;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
@ -85,6 +98,12 @@ public class DeviceProfileDataValidator extends AbstractHasOtaPackageValidator<D
|
|||||||
@Autowired
|
@Autowired
|
||||||
private DashboardService dashboardService;
|
private DashboardService dashboardService;
|
||||||
|
|
||||||
|
@Value("${security.java_cacerts.path}")
|
||||||
|
private String javaCacertsPath;
|
||||||
|
|
||||||
|
@Value("${security.java_cacerts.password}")
|
||||||
|
private String javaCacertsPassword;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void validateDataImpl(TenantId tenantId, DeviceProfile deviceProfile) {
|
protected void validateDataImpl(TenantId tenantId, DeviceProfile deviceProfile) {
|
||||||
if (StringUtils.isEmpty(deviceProfile.getName())) {
|
if (StringUtils.isEmpty(deviceProfile.getName())) {
|
||||||
@ -118,6 +137,11 @@ public class DeviceProfileDataValidator extends AbstractHasOtaPackageValidator<D
|
|||||||
if (deviceProfile.getProvisionType() == null) {
|
if (deviceProfile.getProvisionType() == null) {
|
||||||
deviceProfile.setProvisionType(DeviceProfileProvisionType.DISABLED);
|
deviceProfile.setProvisionType(DeviceProfileProvisionType.DISABLED);
|
||||||
}
|
}
|
||||||
|
if (deviceProfile.getProvisionDeviceKey() != null && DeviceProfileProvisionType.X509_CERTIFICATE_CHAIN.equals(deviceProfile.getProvisionType())) {
|
||||||
|
if (isDeviceProfileCertificateInJavaCacerts(deviceProfile.getProfileData().getProvisionConfiguration().getProvisionDeviceSecret())) {
|
||||||
|
throw new DataValidationException("Device profile certificate cannot be well known root CA!");
|
||||||
|
}
|
||||||
|
}
|
||||||
DeviceProfileTransportConfiguration transportConfiguration = deviceProfile.getProfileData().getTransportConfiguration();
|
DeviceProfileTransportConfiguration transportConfiguration = deviceProfile.getProfileData().getTransportConfiguration();
|
||||||
transportConfiguration.validate();
|
transportConfiguration.validate();
|
||||||
if (transportConfiguration instanceof MqttDeviceProfileTransportConfiguration) {
|
if (transportConfiguration instanceof MqttDeviceProfileTransportConfiguration) {
|
||||||
@ -211,6 +235,11 @@ public class DeviceProfileDataValidator extends AbstractHasOtaPackageValidator<D
|
|||||||
throw new DataValidationException(message);
|
throw new DataValidationException(message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (deviceProfile.getProvisionDeviceKey() != null && DeviceProfileProvisionType.X509_CERTIFICATE_CHAIN.equals(deviceProfile.getProvisionType())) {
|
||||||
|
if (isDeviceProfileCertificateInJavaCacerts(deviceProfile.getProvisionDeviceKey())) {
|
||||||
|
throw new DataValidationException("Device profile certificate cannot be well known root CA!");
|
||||||
|
}
|
||||||
|
}
|
||||||
return old;
|
return old;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -363,4 +392,27 @@ public class DeviceProfileDataValidator extends AbstractHasOtaPackageValidator<D
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isDeviceProfileCertificateInJavaCacerts(String deviceProfileX509Secret) {
|
||||||
|
try {
|
||||||
|
FileInputStream is = new FileInputStream(javaCacertsPath);
|
||||||
|
KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType());
|
||||||
|
keystore.load(is, javaCacertsPassword.toCharArray());
|
||||||
|
|
||||||
|
PKIXParameters params = new PKIXParameters(keystore);
|
||||||
|
for (TrustAnchor ta : params.getTrustAnchors()) {
|
||||||
|
X509Certificate cert = ta.getTrustedCert();
|
||||||
|
if (getCertificateString(cert).equals(deviceProfileX509Secret)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (CertificateException | KeyStoreException | NoSuchAlgorithmException |
|
||||||
|
InvalidAlgorithmParameterException | IOException ignored) {
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getCertificateString(X509Certificate cert) throws CertificateEncodingException {
|
||||||
|
return EncryptionUtil.certTrimNewLines(Base64Utils.encodeToString(cert.getEncoded()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -82,6 +82,8 @@ redis.connection.password=
|
|||||||
security.user_login_case_sensitive=true
|
security.user_login_case_sensitive=true
|
||||||
security.claim.allowClaimingByDefault=true
|
security.claim.allowClaimingByDefault=true
|
||||||
security.claim.duration=60000
|
security.claim.duration=60000
|
||||||
|
security.java_cacerts.path=/path/to/cacerts/file
|
||||||
|
security.java_cacerts.password=myPassword
|
||||||
|
|
||||||
database.ts_max_intervals=700
|
database.ts_max_intervals=700
|
||||||
|
|
||||||
|
|||||||
@ -27,7 +27,49 @@
|
|||||||
{{ 'device-profile.provision-strategy-required' | translate }}
|
{{ 'device-profile.provision-strategy-required' | translate }}
|
||||||
</mat-error>
|
</mat-error>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<section *ngIf="provisionConfigurationFormGroup.get('type').value !== deviceProvisionType.DISABLED" fxLayoutGap.gt-xs="8px" fxLayout="row" fxLayout.xs="column">
|
<div [ngSwitch]="provisionConfigurationFormGroup.get('type').value">
|
||||||
|
<ng-template [ngSwitchCase]="deviceProvisionType.ALLOW_CREATE_NEW_DEVICES">
|
||||||
|
<ng-container *ngTemplateOutlet="default"></ng-container>
|
||||||
|
</ng-template>
|
||||||
|
<ng-template [ngSwitchCase]="deviceProvisionType.CHECK_PRE_PROVISIONED_DEVICES">
|
||||||
|
<ng-container *ngTemplateOutlet="default"></ng-container>
|
||||||
|
</ng-template>
|
||||||
|
<ng-template [ngSwitchCase]="deviceProvisionType.X509_CERTIFICATE_CHAIN">
|
||||||
|
<div fxFlex fxLayoutAlign="start center" class="tb-hint">
|
||||||
|
<span [innerHTML]="'device-profile.provision-strategy-x509.certificate-chain-hint' | translate"></span>
|
||||||
|
<span tb-help-popup="device-profile/x509-chain-hint"
|
||||||
|
tb-help-popup-placement="top"
|
||||||
|
trigger-style="letter-spacing:0.25px; font-size: 12px"
|
||||||
|
[tb-help-popup-style]="{maxWidth: '820px'}"
|
||||||
|
trigger-text="{{ 'action.more' | translate }}"></span>
|
||||||
|
</div>
|
||||||
|
<mat-slide-toggle formControlName="allowCreateNewDevicesByX509Certificate">
|
||||||
|
{{ 'device-profile.provision-strategy-x509.allow-create-new-devices' | translate }}
|
||||||
|
</mat-slide-toggle>
|
||||||
|
<div class="tb-hint" style="padding:0 40px 16px" [innerHTML]="'device-profile.provision-strategy-x509.allow-create-new-devices-hint' | translate"></div>
|
||||||
|
<mat-form-field class="mat-block">
|
||||||
|
<mat-label translate>device-profile.provision-strategy-x509.certificate-value</mat-label>
|
||||||
|
<textarea matInput formControlName="certificateValue" cols="15" rows="5" required></textarea>
|
||||||
|
<mat-error *ngIf="provisionConfigurationFormGroup.get('certificateValue').hasError('required')">
|
||||||
|
{{ 'device-profile.provision-strategy-x509.certificate-value-required' | translate }}
|
||||||
|
</mat-error>
|
||||||
|
</mat-form-field>
|
||||||
|
<mat-form-field class="mat-block">
|
||||||
|
<mat-label translate>device-profile.provision-strategy-x509.cn-regex-variable</mat-label>
|
||||||
|
<input matInput type="text" formControlName="certificateRegExPattern" required>
|
||||||
|
<span matSuffix
|
||||||
|
tb-help-popup="device-profile/x509-chain-regex-examples"
|
||||||
|
tb-help-popup-placement="top"
|
||||||
|
trigger-style="letter-spacing:0.25px"
|
||||||
|
[tb-help-popup-style]="{maxWidth: '820px'}"></span>
|
||||||
|
<mat-error *ngIf="provisionConfigurationFormGroup.get('certificateRegExPattern').hasError('required')">
|
||||||
|
{{ 'device-profile.provision-strategy-x509.cn-regex-variable-required' | translate }}
|
||||||
|
</mat-error>
|
||||||
|
<mat-hint translate>device-profile.provision-strategy-x509.cn-regex-variable-hint</mat-hint>
|
||||||
|
</mat-form-field>
|
||||||
|
</ng-template>
|
||||||
|
<ng-template #default>
|
||||||
|
<section fxLayoutGap.gt-xs="8px" fxLayout="row" fxLayout.xs="column">
|
||||||
<mat-form-field fxFlex class="mat-block">
|
<mat-form-field fxFlex class="mat-block">
|
||||||
<mat-label translate>device-profile.provision-device-key</mat-label>
|
<mat-label translate>device-profile.provision-device-key</mat-label>
|
||||||
<input matInput formControlName="provisionDeviceKey" required/>
|
<input matInput formControlName="provisionDeviceKey" required/>
|
||||||
@ -59,4 +101,6 @@
|
|||||||
</mat-error>
|
</mat-error>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</section>
|
</section>
|
||||||
|
</ng-template>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -32,7 +32,7 @@ import {
|
|||||||
DeviceProvisionType,
|
DeviceProvisionType,
|
||||||
deviceProvisionTypeTranslationMap
|
deviceProvisionTypeTranslationMap
|
||||||
} from '@shared/models/device.models';
|
} from '@shared/models/device.models';
|
||||||
import { generateSecret, isDefinedAndNotNull } from '@core/utils';
|
import { generateSecret, isBoolean, isDefinedAndNotNull } from '@core/utils';
|
||||||
import { ActionNotificationShow } from '@core/notification/notification.actions';
|
import { ActionNotificationShow } from '@core/notification/notification.actions';
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
import { AppState } from '@core/core.state';
|
import { AppState } from '@core/core.state';
|
||||||
@ -86,14 +86,36 @@ export class DeviceProfileProvisionConfigurationComponent implements ControlValu
|
|||||||
this.provisionConfigurationFormGroup = this.fb.group({
|
this.provisionConfigurationFormGroup = this.fb.group({
|
||||||
type: [DeviceProvisionType.DISABLED, Validators.required],
|
type: [DeviceProvisionType.DISABLED, Validators.required],
|
||||||
provisionDeviceSecret: [{value: null, disabled: true}, Validators.required],
|
provisionDeviceSecret: [{value: null, disabled: true}, Validators.required],
|
||||||
provisionDeviceKey: [{value: null, disabled: true}, Validators.required]
|
provisionDeviceKey: [{value: null, disabled: true}, Validators.required],
|
||||||
|
certificateValue: [{value: null, disabled: true}, Validators.required],
|
||||||
|
certificateRegExPattern: [{value: null, disabled: true}, Validators.required],
|
||||||
|
allowCreateNewDevicesByX509Certificate: [{value: null, disabled: true}, Validators.required]
|
||||||
});
|
});
|
||||||
this.provisionConfigurationFormGroup.get('type').valueChanges.subscribe((type) => {
|
this.provisionConfigurationFormGroup.get('type').valueChanges.subscribe((type) => {
|
||||||
if (type === DeviceProvisionType.DISABLED) {
|
if (type === DeviceProvisionType.DISABLED) {
|
||||||
this.provisionConfigurationFormGroup.get('provisionDeviceSecret').disable({emitEvent: false});
|
for (const field in this.provisionConfigurationFormGroup.controls) {
|
||||||
this.provisionConfigurationFormGroup.get('provisionDeviceSecret').patchValue(null, {emitEvent: false});
|
if (field !== 'type') {
|
||||||
this.provisionConfigurationFormGroup.get('provisionDeviceKey').disable({emitEvent: false});
|
const control = this.provisionConfigurationFormGroup.get(field);
|
||||||
this.provisionConfigurationFormGroup.get('provisionDeviceKey').patchValue(null);
|
control.disable({emitEvent: false});
|
||||||
|
control.patchValue(null, {emitEvent: false});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (type === DeviceProvisionType.X509_CERTIFICATE_CHAIN) {
|
||||||
|
const certificateValue: string = this.provisionConfigurationFormGroup.get('certificateValue').value;
|
||||||
|
if (!certificateValue || !certificateValue.length) {
|
||||||
|
this.provisionConfigurationFormGroup.get('certificateValue').patchValue(null, {emitEvent: false});
|
||||||
|
}
|
||||||
|
const certificateRegExPattern: string = this.provisionConfigurationFormGroup.get('certificateRegExPattern').value;
|
||||||
|
if (!certificateRegExPattern || !certificateRegExPattern.length) {
|
||||||
|
this.provisionConfigurationFormGroup.get('certificateRegExPattern').patchValue('[\\w]*', {emitEvent: false});
|
||||||
|
}
|
||||||
|
const allowCreateNewDevicesByX509Certificate: boolean | null = this.provisionConfigurationFormGroup.get('allowCreateNewDevicesByX509Certificate').value;
|
||||||
|
if (!isBoolean(allowCreateNewDevicesByX509Certificate)) {
|
||||||
|
this.provisionConfigurationFormGroup.get('allowCreateNewDevicesByX509Certificate').patchValue(true, {emitEvent: false});
|
||||||
|
}
|
||||||
|
this.provisionConfigurationFormGroup.get('certificateValue').enable({emitEvent: false});
|
||||||
|
this.provisionConfigurationFormGroup.get('certificateRegExPattern').enable({emitEvent: false});
|
||||||
|
this.provisionConfigurationFormGroup.get('allowCreateNewDevicesByX509Certificate').enable({emitEvent: false});
|
||||||
} else {
|
} else {
|
||||||
const provisionDeviceSecret: string = this.provisionConfigurationFormGroup.get('provisionDeviceSecret').value;
|
const provisionDeviceSecret: string = this.provisionConfigurationFormGroup.get('provisionDeviceSecret').value;
|
||||||
if (!provisionDeviceSecret || !provisionDeviceSecret.length) {
|
if (!provisionDeviceSecret || !provisionDeviceSecret.length) {
|
||||||
@ -121,6 +143,9 @@ export class DeviceProfileProvisionConfigurationComponent implements ControlValu
|
|||||||
|
|
||||||
writeValue(value: DeviceProvisionConfiguration | null): void {
|
writeValue(value: DeviceProvisionConfiguration | null): void {
|
||||||
if (isDefinedAndNotNull(value)) {
|
if (isDefinedAndNotNull(value)) {
|
||||||
|
if (value.type === DeviceProvisionType.X509_CERTIFICATE_CHAIN) {
|
||||||
|
value.certificateValue = value.provisionDeviceSecret;
|
||||||
|
}
|
||||||
this.provisionConfigurationFormGroup.patchValue(value, {emitEvent: false});
|
this.provisionConfigurationFormGroup.patchValue(value, {emitEvent: false});
|
||||||
} else {
|
} else {
|
||||||
this.provisionConfigurationFormGroup.patchValue({type: DeviceProvisionType.DISABLED});
|
this.provisionConfigurationFormGroup.patchValue({type: DeviceProvisionType.DISABLED});
|
||||||
@ -150,8 +175,12 @@ export class DeviceProfileProvisionConfigurationComponent implements ControlValu
|
|||||||
|
|
||||||
private updateModel(): void {
|
private updateModel(): void {
|
||||||
let deviceProvisionConfiguration: DeviceProvisionConfiguration = null;
|
let deviceProvisionConfiguration: DeviceProvisionConfiguration = null;
|
||||||
|
this.resetFormControls(this.provisionConfigurationFormGroup.value);
|
||||||
if (this.provisionConfigurationFormGroup.valid) {
|
if (this.provisionConfigurationFormGroup.valid) {
|
||||||
deviceProvisionConfiguration = this.provisionConfigurationFormGroup.getRawValue();
|
deviceProvisionConfiguration = this.provisionConfigurationFormGroup.getRawValue();
|
||||||
|
if (deviceProvisionConfiguration.type === DeviceProvisionType.X509_CERTIFICATE_CHAIN) {
|
||||||
|
deviceProvisionConfiguration.provisionDeviceSecret = deviceProvisionConfiguration.certificateValue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.propagateChange(deviceProvisionConfiguration);
|
this.propagateChange(deviceProvisionConfiguration);
|
||||||
}
|
}
|
||||||
@ -166,4 +195,15 @@ export class DeviceProfileProvisionConfigurationComponent implements ControlValu
|
|||||||
horizontalPosition: 'right'
|
horizontalPosition: 'right'
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private resetFormControls(value: DeviceProvisionConfiguration) {
|
||||||
|
if (value.type === DeviceProvisionType.CHECK_PRE_PROVISIONED_DEVICES || value.type === DeviceProvisionType.ALLOW_CREATE_NEW_DEVICES) {
|
||||||
|
this.provisionConfigurationFormGroup.get('certificateValue').reset({value: null, disabled: true}, {emitEvent: false});
|
||||||
|
this.provisionConfigurationFormGroup.get('certificateRegExPattern').reset({value: null, disabled: true}, {emitEvent: false});
|
||||||
|
this.provisionConfigurationFormGroup.get('allowCreateNewDevicesByX509Certificate').reset({value: null, disabled: true}, {emitEvent: false});
|
||||||
|
} else if (value.type === DeviceProvisionType.X509_CERTIFICATE_CHAIN) {
|
||||||
|
this.provisionConfigurationFormGroup.get('provisionDeviceSecret').reset({value: null, disabled: true}, {emitEvent: false});
|
||||||
|
this.provisionConfigurationFormGroup.get('provisionDeviceKey').reset({value: null, disabled: true}, {emitEvent: false});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -104,7 +104,9 @@ export class DeviceProfileComponent extends EntityComponent<DeviceProfile> {
|
|||||||
const deviceProvisionConfiguration: DeviceProvisionConfiguration = {
|
const deviceProvisionConfiguration: DeviceProvisionConfiguration = {
|
||||||
type: entity?.provisionType ? entity.provisionType : DeviceProvisionType.DISABLED,
|
type: entity?.provisionType ? entity.provisionType : DeviceProvisionType.DISABLED,
|
||||||
provisionDeviceKey: entity?.provisionDeviceKey,
|
provisionDeviceKey: entity?.provisionDeviceKey,
|
||||||
provisionDeviceSecret: entity?.profileData?.provisionConfiguration?.provisionDeviceSecret
|
provisionDeviceSecret: entity?.profileData?.provisionConfiguration?.provisionDeviceSecret,
|
||||||
|
certificateRegExPattern: entity?.profileData?.provisionConfiguration?.certificateRegExPattern,
|
||||||
|
allowCreateNewDevicesByX509Certificate: entity?.profileData?.provisionConfiguration?.allowCreateNewDevicesByX509Certificate
|
||||||
};
|
};
|
||||||
const form = this.fb.group(
|
const form = this.fb.group(
|
||||||
{
|
{
|
||||||
@ -185,7 +187,9 @@ export class DeviceProfileComponent extends EntityComponent<DeviceProfile> {
|
|||||||
const deviceProvisionConfiguration: DeviceProvisionConfiguration = {
|
const deviceProvisionConfiguration: DeviceProvisionConfiguration = {
|
||||||
type: entity?.provisionType ? entity.provisionType : DeviceProvisionType.DISABLED,
|
type: entity?.provisionType ? entity.provisionType : DeviceProvisionType.DISABLED,
|
||||||
provisionDeviceKey: entity?.provisionDeviceKey,
|
provisionDeviceKey: entity?.provisionDeviceKey,
|
||||||
provisionDeviceSecret: entity?.profileData?.provisionConfiguration?.provisionDeviceSecret
|
provisionDeviceSecret: entity?.profileData?.provisionConfiguration?.provisionDeviceSecret,
|
||||||
|
certificateRegExPattern: entity?.profileData?.provisionConfiguration?.certificateRegExPattern,
|
||||||
|
allowCreateNewDevicesByX509Certificate: entity?.profileData?.provisionConfiguration?.allowCreateNewDevicesByX509Certificate
|
||||||
};
|
};
|
||||||
this.entityForm.patchValue({name: entity.name});
|
this.entityForm.patchValue({name: entity.name});
|
||||||
this.entityForm.patchValue({type: entity.type}, {emitEvent: false});
|
this.entityForm.patchValue({type: entity.type}, {emitEvent: false});
|
||||||
|
|||||||
@ -169,6 +169,6 @@
|
|||||||
<mat-checkbox formControlName="sendAckOnValidationException">
|
<mat-checkbox formControlName="sendAckOnValidationException">
|
||||||
{{ 'device-profile.mqtt-send-ack-on-validation-exception' | translate }}
|
{{ 'device-profile.mqtt-send-ack-on-validation-exception' | translate }}
|
||||||
</mat-checkbox>
|
</mat-checkbox>
|
||||||
<div class="tb-hint" innerHTML="{{ 'device-profile.mqtt-send-ack-on-validation-exception-hint' | translate }}"></div>
|
<div class="tb-hint" style="max-width: 800px" innerHTML="{{ 'device-profile.mqtt-send-ack-on-validation-exception-hint' | translate }}"></div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@ -62,7 +62,8 @@ export enum CoapTransportDeviceType {
|
|||||||
export enum DeviceProvisionType {
|
export enum DeviceProvisionType {
|
||||||
DISABLED = 'DISABLED',
|
DISABLED = 'DISABLED',
|
||||||
ALLOW_CREATE_NEW_DEVICES = 'ALLOW_CREATE_NEW_DEVICES',
|
ALLOW_CREATE_NEW_DEVICES = 'ALLOW_CREATE_NEW_DEVICES',
|
||||||
CHECK_PRE_PROVISIONED_DEVICES = 'CHECK_PRE_PROVISIONED_DEVICES'
|
CHECK_PRE_PROVISIONED_DEVICES = 'CHECK_PRE_PROVISIONED_DEVICES',
|
||||||
|
X509_CERTIFICATE_CHAIN = 'X509_CERTIFICATE_CHAIN'
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DeviceConfigurationFormInfo {
|
export interface DeviceConfigurationFormInfo {
|
||||||
@ -110,7 +111,8 @@ export const deviceProvisionTypeTranslationMap = new Map<DeviceProvisionType, st
|
|||||||
[
|
[
|
||||||
[DeviceProvisionType.DISABLED, 'device-profile.provision-strategy-disabled'],
|
[DeviceProvisionType.DISABLED, 'device-profile.provision-strategy-disabled'],
|
||||||
[DeviceProvisionType.ALLOW_CREATE_NEW_DEVICES, 'device-profile.provision-strategy-created-new'],
|
[DeviceProvisionType.ALLOW_CREATE_NEW_DEVICES, 'device-profile.provision-strategy-created-new'],
|
||||||
[DeviceProvisionType.CHECK_PRE_PROVISIONED_DEVICES, 'device-profile.provision-strategy-check-pre-provisioned']
|
[DeviceProvisionType.CHECK_PRE_PROVISIONED_DEVICES, 'device-profile.provision-strategy-check-pre-provisioned'],
|
||||||
|
[DeviceProvisionType.X509_CERTIFICATE_CHAIN, 'device-profile.provision-strategy-x509.certificate-chain']
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -320,6 +322,9 @@ export interface DeviceProvisionConfiguration {
|
|||||||
type: DeviceProvisionType;
|
type: DeviceProvisionType;
|
||||||
provisionDeviceSecret?: string;
|
provisionDeviceSecret?: string;
|
||||||
provisionDeviceKey?: string;
|
provisionDeviceKey?: string;
|
||||||
|
certificateValue?: string;
|
||||||
|
certificateRegExPattern?: string;
|
||||||
|
allowCreateNewDevicesByX509Certificate?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createDeviceProfileConfiguration(type: DeviceProfileType): DeviceProfileConfiguration {
|
export function createDeviceProfileConfiguration(type: DeviceProfileType): DeviceProfileConfiguration {
|
||||||
|
|||||||
@ -0,0 +1,18 @@
|
|||||||
|
##### X509 Certificate Chain info
|
||||||
|
|
||||||
|
X.509 certificates strategy is used to provision devices by client certificates in two-way TLS communication.
|
||||||
|
|
||||||
|
<b>This strategy can:</b>
|
||||||
|
* check for pre-provisioned devices
|
||||||
|
* update X.509 device credentials
|
||||||
|
* create new devices
|
||||||
|
|
||||||
|
<b>The user uploads</b> X.509 certificate to the device profile and sets a regular expression to fetch the device name from *Common Name (CN)*.
|
||||||
|
|
||||||
|
<b>Client certificates must</b> be signed by X.509 certificate, pre-uploaded for this device profile to provision devices by the strategy.
|
||||||
|
|
||||||
|
<b>The client must</b> establish a TLS connection using the entire chain of certificates (this chain must include device profile X.509 certificate on the last level).
|
||||||
|
|
||||||
|
If a device already exists with outdated X.509 credentials, this strategy automatically updates it with the device certificate's credentials from the chain.
|
||||||
|
|
||||||
|
<b>Important:</b> Uploaded certificates should be neither root nor intermediate certificates that are provided by a well-known *Certificate Authority (CA)*.
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
#### Examples of RegEx usage
|
||||||
|
|
||||||
|
* **Pattern:** <code>.*</code> - matches any character (until line terminators)
|
||||||
|
<br>**CN sample:** <code>DeviceName\nAdditionalInfo</code>
|
||||||
|
<br>**Pattern matches:** <code>DeviceName</code>
|
||||||
|
|
||||||
|
* **Pattern:** <code>^([^@]+)</code> - matches any string that starts with one or more characters that are not the <code>@</code> symbol (<code>@</code> could be replaced by any other symbol)
|
||||||
|
<br>**CN sample:** <code>DeviceName@AdditionalInfo</code>
|
||||||
|
<br>**Pattern matches:** <code>DeviceName</code>
|
||||||
|
|
||||||
|
* **Pattern:** <code>[\w]*$</code> (equivalent to <code>[a-zA-Z0-9_]\*$</code>) - matches zero or more occurences of any word character (letter, digit or underscore) at the end of the string
|
||||||
|
<br>**CN sample:** <code>AdditionalInfo2110#DeviceName_01</code>
|
||||||
|
<br>**Pattern matches:** <code>DeviceName_01</code>
|
||||||
|
|
||||||
|
**Note:** Client will get error response in case regex is failed to match.
|
||||||
@ -63,6 +63,8 @@
|
|||||||
"print": "Print",
|
"print": "Print",
|
||||||
"restore": "Restore",
|
"restore": "Restore",
|
||||||
"confirm": "Confirm",
|
"confirm": "Confirm",
|
||||||
|
"more": "More",
|
||||||
|
"less": "Less",
|
||||||
"skip": "Skip",
|
"skip": "Skip",
|
||||||
"send": "Send"
|
"send": "Send"
|
||||||
},
|
},
|
||||||
@ -1516,6 +1518,17 @@
|
|||||||
"provision-device-secret-required": "Provision device secret is required.",
|
"provision-device-secret-required": "Provision device secret is required.",
|
||||||
"copy-provision-secret": "Copy provision secret",
|
"copy-provision-secret": "Copy provision secret",
|
||||||
"provision-secret-copied-message": "Provision secret has been copied to clipboard",
|
"provision-secret-copied-message": "Provision secret has been copied to clipboard",
|
||||||
|
"provision-strategy-x509": {
|
||||||
|
"certificate-chain": "X509 Certificates Chain",
|
||||||
|
"certificate-chain-hint": "X.509 certificates strategy is used to provision devices by client certificates in two-way TLS communication.",
|
||||||
|
"allow-create-new-devices": "Create new devices",
|
||||||
|
"allow-create-new-devices-hint": "If selected new devices will be created and client certificate will be used as device credentials.",
|
||||||
|
"certificate-value": "Certificate in PEM format",
|
||||||
|
"certificate-value-required": "Certificate in PEM format is required",
|
||||||
|
"cn-regex-variable": "CN Regular Expression variable",
|
||||||
|
"cn-regex-variable-required": "CN Regular Expression variable is required",
|
||||||
|
"cn-regex-variable-hint": "Required to fetch device name from device's X509 certificate's common name."
|
||||||
|
},
|
||||||
"condition": "Condition",
|
"condition": "Condition",
|
||||||
"condition-type": "Condition type",
|
"condition-type": "Condition type",
|
||||||
"condition-type-simple": "Simple",
|
"condition-type-simple": "Simple",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user