Browse Source
# Conflicts: # application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java # application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.javapull/8373/head
226 changed files with 8305 additions and 764 deletions
@ -0,0 +1,50 @@ |
|||||
|
{ |
||||
|
"widgetsBundle": { |
||||
|
"alias": "home_page_widgets", |
||||
|
"title": "Home page widgets", |
||||
|
"image": null, |
||||
|
"description": null, |
||||
|
"externalId": null, |
||||
|
"name": "Home page widgets" |
||||
|
}, |
||||
|
"widgetTypes": [ |
||||
|
{ |
||||
|
"alias": "documentation_links", |
||||
|
"name": "Documentation links", |
||||
|
"image": null, |
||||
|
"description": null, |
||||
|
"descriptor": { |
||||
|
"type": "static", |
||||
|
"sizeX": 7.5, |
||||
|
"sizeY": 3, |
||||
|
"resources": [], |
||||
|
"templateHtml": "<tb-doc-links-widget\n [ctx]=\"ctx\">\n</tb-doc-links-widget>\n", |
||||
|
"templateCss": "", |
||||
|
"controllerScript": "self.onInit = function() {\n}", |
||||
|
"settingsSchema": "", |
||||
|
"dataKeySettingsSchema": "", |
||||
|
"settingsDirective": "tb-doc-links-widget-settings", |
||||
|
"defaultConfig": "{\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"columns\":3},\"title\":\"Documentation links\",\"dropShadow\":true}" |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
"alias": "getting_started", |
||||
|
"name": "Getting started", |
||||
|
"image": null, |
||||
|
"description": null, |
||||
|
"descriptor": { |
||||
|
"type": "static", |
||||
|
"sizeX": 7.5, |
||||
|
"sizeY": 6.5, |
||||
|
"resources": [], |
||||
|
"templateHtml": "<tb-getting-started-widget\n [ctx]=\"ctx\">\n</tb-getting-started-widget>\n", |
||||
|
"templateCss": "", |
||||
|
"controllerScript": "self.onInit = function() {\n}", |
||||
|
"settingsSchema": "", |
||||
|
"dataKeySettingsSchema": "", |
||||
|
"settingsDirective": "", |
||||
|
"defaultConfig": "{\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Getting started\",\"dropShadow\":true}" |
||||
|
} |
||||
|
} |
||||
|
] |
||||
|
} |
||||
@ -0,0 +1,58 @@ |
|||||
|
/** |
||||
|
* 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.controller; |
||||
|
|
||||
|
import org.junit.Test; |
||||
|
import org.thingsboard.server.common.data.Device; |
||||
|
import org.thingsboard.server.common.data.SaveDeviceWithCredentialsRequest; |
||||
|
import org.thingsboard.server.common.data.security.DeviceCredentials; |
||||
|
import org.thingsboard.server.common.data.security.DeviceCredentialsType; |
||||
|
|
||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; |
||||
|
|
||||
|
public abstract class BaseTelemetryControllerTest extends AbstractControllerTest { |
||||
|
|
||||
|
@Test |
||||
|
public void testConstraintValidator() throws Exception { |
||||
|
loginTenantAdmin(); |
||||
|
Device device = createDevice(); |
||||
|
String correctRequestBody = "{\"data\": \"value\"}"; |
||||
|
doPostAsync("/api/plugins/telemetry/" + device.getId() + "/SHARED_SCOPE", correctRequestBody, String.class, status().isOk()); |
||||
|
doPostAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/timeseries/smth", correctRequestBody, String.class, status().isOk()); |
||||
|
String invalidRequestBody = "{\"data\": \"<object data=\\\"data:text/html,<script>alert(document)</script>\\\"></object>\"}"; |
||||
|
doPostAsync("/api/plugins/telemetry/" + device.getId() + "/SHARED_SCOPE", invalidRequestBody, String.class, status().isBadRequest()); |
||||
|
doPostAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/timeseries/smth", invalidRequestBody, String.class, status().isBadRequest()); |
||||
|
invalidRequestBody = "{\"<object data=\\\"data:text/html,<script>alert(document)</script>\\\"></object>\": \"data\"}"; |
||||
|
doPostAsync("/api/plugins/telemetry/" + device.getId() + "/SHARED_SCOPE", invalidRequestBody, String.class, status().isBadRequest()); |
||||
|
doPostAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/timeseries/smth", invalidRequestBody, String.class, status().isBadRequest()); |
||||
|
} |
||||
|
|
||||
|
private Device createDevice() throws Exception { |
||||
|
String testToken = "TEST_TOKEN"; |
||||
|
|
||||
|
Device device = new Device(); |
||||
|
device.setName("My device"); |
||||
|
device.setType("default"); |
||||
|
|
||||
|
DeviceCredentials deviceCredentials = new DeviceCredentials(); |
||||
|
deviceCredentials.setCredentialsType(DeviceCredentialsType.ACCESS_TOKEN); |
||||
|
deviceCredentials.setCredentialsId(testToken); |
||||
|
|
||||
|
SaveDeviceWithCredentialsRequest saveRequest = new SaveDeviceWithCredentialsRequest(device, deviceCredentials); |
||||
|
|
||||
|
return readResponse(doPost("/api/device-with-credentials", saveRequest).andExpect(status().isOk()), Device.class); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,266 @@ |
|||||
|
/** |
||||
|
* 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.device.provision; |
||||
|
|
||||
|
|
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.assertj.core.api.Assertions; |
||||
|
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.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.Tenant; |
||||
|
import org.thingsboard.server.common.data.device.credentials.ProvisionDeviceCredentialsData; |
||||
|
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.CustomerId; |
||||
|
import org.thingsboard.server.common.data.id.DeviceId; |
||||
|
import org.thingsboard.server.common.data.id.DeviceProfileId; |
||||
|
import org.thingsboard.server.common.data.id.TenantId; |
||||
|
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.common.transport.util.SslUtil; |
||||
|
import org.thingsboard.server.dao.attributes.AttributesService; |
||||
|
import org.thingsboard.server.dao.audit.AuditLogService; |
||||
|
import org.thingsboard.server.dao.device.DeviceCredentialsService; |
||||
|
import org.thingsboard.server.dao.device.DeviceProfileService; |
||||
|
import org.thingsboard.server.dao.device.DeviceService; |
||||
|
import org.thingsboard.server.dao.device.provision.ProvisionFailedException; |
||||
|
import org.thingsboard.server.dao.device.provision.ProvisionRequest; |
||||
|
import org.thingsboard.server.dao.device.provision.ProvisionResponse; |
||||
|
import org.thingsboard.server.dao.device.provision.ProvisionResponseStatus; |
||||
|
import org.thingsboard.server.gen.transport.TransportProtos; |
||||
|
import org.thingsboard.server.queue.TbQueueProducer; |
||||
|
import org.thingsboard.server.queue.common.TbProtoQueueMsg; |
||||
|
import org.thingsboard.server.queue.discovery.PartitionService; |
||||
|
import org.thingsboard.server.queue.provider.TbQueueProducerProvider; |
||||
|
import org.thingsboard.server.service.device.DeviceProvisionServiceImpl;;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 = DeviceProvisionServiceImpl.class) |
||||
|
public class DeviceProvisionServiceTest { |
||||
|
|
||||
|
@MockBean |
||||
|
protected TbQueueProducerProvider producerProvider; |
||||
|
@MockBean |
||||
|
protected TbQueueProducer<TbProtoQueueMsg<TransportProtos.ToRuleEngineMsg>> ruleEngineMsgProducer; |
||||
|
@MockBean |
||||
|
protected TbClusterService clusterService; |
||||
|
@MockBean |
||||
|
protected DeviceProfileService deviceProfileService; |
||||
|
@MockBean |
||||
|
protected DeviceService deviceService; |
||||
|
@MockBean |
||||
|
protected DeviceCredentialsService deviceCredentialsService; |
||||
|
@MockBean |
||||
|
protected AttributesService attributesService; |
||||
|
@MockBean |
||||
|
protected AuditLogService auditLogService; |
||||
|
@MockBean |
||||
|
protected PartitionService partitionService; |
||||
|
@SpyBean |
||||
|
DeviceProvisionServiceImpl service; |
||||
|
|
||||
|
private String[] chain; |
||||
|
|
||||
|
@Before |
||||
|
public void setUp() { |
||||
|
String filePath = "src/test/resources/provision/x509ChainProvisionTest.pem"; |
||||
|
try { |
||||
|
String certificateChain = Files.readString(Paths.get(filePath)); |
||||
|
certificateChain = certTrimNewLinesForChainInDeviceProfile(certificateChain); |
||||
|
chain = fetchLeafCertificateFromChain(certificateChain); |
||||
|
} catch (IOException e) { |
||||
|
throw new RuntimeException(e); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
@Test |
||||
|
public void provisionDeviceViaX509Certificate() { |
||||
|
var tenant = createTenant(); |
||||
|
var deviceProfile = createDeviceProfile(tenant.getId(), chain[1], true); |
||||
|
|
||||
|
var device = createDevice(tenant.getId(), deviceProfile.getId()); |
||||
|
when(deviceService.findDeviceByTenantIdAndName(any(), any())).thenReturn(device); |
||||
|
|
||||
|
var deviceCredentials = createDeviceCredentials(chain[0], device.getId()); |
||||
|
when(deviceCredentialsService.findDeviceCredentialsByDeviceId(any(), any())).thenReturn(deviceCredentials); |
||||
|
when(deviceCredentialsService.updateDeviceCredentials(any(), any())).thenReturn(deviceCredentials); |
||||
|
|
||||
|
ProvisionResponse response = service.provisionDeviceViaX509Chain(deviceProfile, createProvisionRequest(chain[0])); |
||||
|
|
||||
|
verify(deviceService, times(1)).findDeviceByTenantIdAndName(any(), any()); |
||||
|
verify(deviceCredentialsService, times(1)).findDeviceCredentialsByDeviceId(any(), any()); |
||||
|
verify(deviceCredentialsService, times(1)).updateDeviceCredentials(any(), any()); |
||||
|
|
||||
|
Assertions.assertThat(response.getResponseStatus()).isEqualTo(ProvisionResponseStatus.SUCCESS); |
||||
|
Assertions.assertThat(response.getDeviceCredentials()).isEqualTo(deviceCredentials); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void provisionDeviceWithIncorrectConfiguration() { |
||||
|
var tenant = createTenant(); |
||||
|
var deviceProfile = createDeviceProfile(tenant.getId(), chain[1], false); |
||||
|
|
||||
|
Assertions.assertThatThrownBy(() -> |
||||
|
service.provisionDeviceViaX509Chain(deviceProfile, createProvisionRequest(chain[0]))) |
||||
|
.isInstanceOf(ProvisionFailedException.class); |
||||
|
|
||||
|
verify(deviceService, times(1)).findDeviceByTenantIdAndName(any(), any()); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void matchDeviceNameFromX509CNCertificateByRegex() { |
||||
|
var tenant = createTenant(); |
||||
|
var deviceProfile = createDeviceProfile(tenant.getId(), chain[1], true); |
||||
|
X509CertificateChainProvisionConfiguration configuration = (X509CertificateChainProvisionConfiguration) deviceProfile.getProfileData().getProvisionConfiguration(); |
||||
|
String CN = getCNFromX509Certificate(chain[0]); |
||||
|
String deviceName = service.extractDeviceNameFromCNByRegEx(deviceProfile, CN, configuration.getCertificateRegExPattern()); |
||||
|
|
||||
|
Assertions.assertThat(deviceName).isNotBlank(); |
||||
|
Assertions.assertThat(deviceName).isEqualTo("deviceCertificate"); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void matchDeviceNameFromCNByRegex() { |
||||
|
var CN = "DeviceA.company.com"; |
||||
|
var regex = "(.*)\\.company.com"; |
||||
|
var result = service.extractDeviceNameFromCNByRegEx(null, CN, regex); |
||||
|
Assertions.assertThat(result).isNotBlank(); |
||||
|
Assertions.assertThat(result).isEqualTo("DeviceA"); |
||||
|
|
||||
|
CN = "DeviceA@company.com"; |
||||
|
regex = "(.*)@company.com"; |
||||
|
result = service.extractDeviceNameFromCNByRegEx(null, CN, regex); |
||||
|
Assertions.assertThat(result).isNotBlank(); |
||||
|
Assertions.assertThat(result).isEqualTo("DeviceA"); |
||||
|
|
||||
|
CN = "prefixDeviceAsuffix@company.com"; |
||||
|
regex = "prefix(.*)suffix@company.com"; |
||||
|
result = service.extractDeviceNameFromCNByRegEx(null, CN, regex); |
||||
|
Assertions.assertThat(result).isNotBlank(); |
||||
|
Assertions.assertThat(result).isEqualTo("DeviceA"); |
||||
|
|
||||
|
CN = "prefixDeviceAsufix@company.com"; |
||||
|
regex = "prefix(.*)sufix@company.com"; |
||||
|
result = service.extractDeviceNameFromCNByRegEx(null, CN, regex); |
||||
|
Assertions.assertThat(result).isNotBlank(); |
||||
|
Assertions.assertThat(result).isEqualTo("DeviceA"); |
||||
|
|
||||
|
CN = "region.DeviceA.220423@company.com"; |
||||
|
regex = "\\D+\\.(.*)\\.\\d+@company.com"; |
||||
|
result = service.extractDeviceNameFromCNByRegEx(null, CN, regex); |
||||
|
Assertions.assertThat(result).isNotBlank(); |
||||
|
Assertions.assertThat(result).isEqualTo("DeviceA"); |
||||
|
} |
||||
|
|
||||
|
private DeviceProfile createDeviceProfile(TenantId tenantId, String certificateValue, boolean isAllowToCreateNewDevices) { |
||||
|
X509CertificateChainProvisionConfiguration provision = new X509CertificateChainProvisionConfiguration(); |
||||
|
provision.setProvisionDeviceSecret(certificateValue); |
||||
|
provision.setCertificateRegExPattern("([^@]+)"); |
||||
|
provision.setAllowCreateNewDevicesByX509Certificate(isAllowToCreateNewDevices); |
||||
|
|
||||
|
DeviceProfileData deviceProfileData = new DeviceProfileData(); |
||||
|
deviceProfileData.setProvisionConfiguration(provision); |
||||
|
|
||||
|
DeviceProfile deviceProfile = new DeviceProfile(); |
||||
|
deviceProfile.setId(new DeviceProfileId(UUID.randomUUID())); |
||||
|
deviceProfile.setProfileData(deviceProfileData); |
||||
|
deviceProfile.setProvisionDeviceKey(EncryptionUtil.getSha3Hash(certificateValue)); |
||||
|
deviceProfile.setProvisionType(DeviceProfileProvisionType.X509_CERTIFICATE_CHAIN); |
||||
|
deviceProfile.setTenantId(tenantId); |
||||
|
return deviceProfile; |
||||
|
} |
||||
|
|
||||
|
private Device createDevice(TenantId tenantId, DeviceProfileId deviceProfileId) { |
||||
|
Device device = new Device(); |
||||
|
device.setTenantId(tenantId); |
||||
|
device.setId(new DeviceId(UUID.randomUUID())); |
||||
|
device.setDeviceProfileId(deviceProfileId); |
||||
|
device.setCustomerId(new CustomerId(UUID.randomUUID())); |
||||
|
return device; |
||||
|
} |
||||
|
|
||||
|
private Tenant createTenant() { |
||||
|
Tenant tenant = new Tenant(); |
||||
|
tenant.setId(new TenantId(UUID.randomUUID())); |
||||
|
return tenant; |
||||
|
} |
||||
|
|
||||
|
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 ProvisionRequest createProvisionRequest(String certificateValue) { |
||||
|
return new ProvisionRequest(null, DeviceCredentialsType.X509_CERTIFICATE, |
||||
|
new ProvisionDeviceCredentialsData(null, null, null, null, certificateValue), |
||||
|
null); |
||||
|
} |
||||
|
|
||||
|
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]); |
||||
|
} |
||||
|
|
||||
|
private String getCNFromX509Certificate(String x509Value) { |
||||
|
try { |
||||
|
return SslUtil.parseCommonName(SslUtil.readCertFile(x509Value)); |
||||
|
} catch (Exception e) { |
||||
|
return null; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,211 @@ |
|||||
|
/** |
||||
|
* 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/provision/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 validateExistingDeviceByX509CertificateStrategy() { |
||||
|
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.provisionDeviceViaX509Chain(any(), 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)).provisionDeviceViaX509Chain(any(), 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----- |
||||
@ -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; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,37 @@ |
|||||
|
/** |
||||
|
* 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.dao.util; |
||||
|
|
||||
|
import org.thingsboard.server.common.data.kv.KvEntry; |
||||
|
import org.thingsboard.server.dao.exception.IncorrectParameterException; |
||||
|
import org.thingsboard.server.dao.service.ConstraintValidator; |
||||
|
|
||||
|
import java.util.List; |
||||
|
|
||||
|
public class KvUtils { |
||||
|
public static void validate(List<? extends KvEntry> tsKvEntries, boolean validateNoxss) { |
||||
|
tsKvEntries.forEach(kvEntry -> validate(kvEntry, validateNoxss)); |
||||
|
} |
||||
|
|
||||
|
public static void validate(KvEntry tsKvEntry, boolean validateNoxss) { |
||||
|
if (tsKvEntry == null) { |
||||
|
throw new IncorrectParameterException("Key value entry can't be null"); |
||||
|
} |
||||
|
if (validateNoxss) { |
||||
|
ConstraintValidator.validateFields(tsKvEntry); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,57 @@ |
|||||
|
/** |
||||
|
* 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.dao.service; |
||||
|
|
||||
|
import org.junit.Assert; |
||||
|
import org.junit.jupiter.api.Assertions; |
||||
|
import org.junit.jupiter.api.Test; |
||||
|
import org.thingsboard.server.common.data.kv.JsonDataEntry; |
||||
|
import org.thingsboard.server.common.data.kv.StringDataEntry; |
||||
|
import org.thingsboard.server.dao.exception.DataValidationException; |
||||
|
|
||||
|
class ConstraintValidatorTest { |
||||
|
|
||||
|
private static final int MIN_IN_MS = 60000; |
||||
|
private static final int _1M = 1_000_000; |
||||
|
|
||||
|
@Test |
||||
|
void validateFields() { |
||||
|
StringDataEntry stringDataEntryValid = new StringDataEntry("key", "value"); |
||||
|
|
||||
|
StringDataEntry stringDataEntryInvalid1 = new StringDataEntry("<object type=\"text/html\"><script>alert(document)</script></object>", "value"); |
||||
|
StringDataEntry stringDataEntryInvalid2 = new StringDataEntry("key", "<object type=\"text/html\"><script>alert(document)</script></object>"); |
||||
|
|
||||
|
JsonDataEntry jsonDataEntryInvalid = new JsonDataEntry("key", "{\"value\": <object type=\"text/html\"><script>alert(document)</script></object>}"); |
||||
|
|
||||
|
Assert.assertThrows(DataValidationException.class, () -> ConstraintValidator.validateFields(stringDataEntryInvalid1)); |
||||
|
Assert.assertThrows(DataValidationException.class, () -> ConstraintValidator.validateFields(stringDataEntryInvalid2)); |
||||
|
Assert.assertThrows(DataValidationException.class, () -> ConstraintValidator.validateFields(jsonDataEntryInvalid)); |
||||
|
ConstraintValidator.validateFields(stringDataEntryValid); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
void validatePerMinute() { |
||||
|
StringDataEntry stringDataEntryValid = new StringDataEntry("key", "value"); |
||||
|
|
||||
|
long start = System.currentTimeMillis(); |
||||
|
for (int i = 0; i < _1M; i++) { |
||||
|
ConstraintValidator.validateFields(stringDataEntryValid); |
||||
|
} |
||||
|
long end = System.currentTimeMillis(); |
||||
|
|
||||
|
Assertions.assertTrue(MIN_IN_MS > end - start); |
||||
|
} |
||||
|
} |
||||
@ -1,3 +0,0 @@ |
|||||
tb.baseUrl=http://localhost:8080 |
|
||||
tb.baseUiUrl=http://localhost:8080 |
|
||||
tb.wsUrl=ws://localhost:8080 |
|
||||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue