Browse Source
[3.3.3] Fix corner case when access token matches user name in credentialspull/5627/head
committed by
GitHub
5 changed files with 306 additions and 37 deletions
@ -0,0 +1,18 @@ |
|||
/** |
|||
* Copyright © 2016-2021 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; |
|||
|
|||
enum BasicCredentialsValidationResult {HASH_MISMATCH, PASSWORD_MISMATCH, VALID} |
|||
@ -0,0 +1,226 @@ |
|||
/** |
|||
* Copyright © 2016-2021 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.transport.mqtt.credentials.sql; |
|||
|
|||
import com.fasterxml.jackson.core.type.TypeReference; |
|||
import org.apache.commons.lang3.RandomStringUtils; |
|||
import org.eclipse.paho.client.mqttv3.MqttAsyncClient; |
|||
import org.eclipse.paho.client.mqttv3.MqttConnectOptions; |
|||
import org.eclipse.paho.client.mqttv3.MqttException; |
|||
import org.eclipse.paho.client.mqttv3.MqttSecurityException; |
|||
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence; |
|||
import org.junit.After; |
|||
import org.junit.Assert; |
|||
import org.junit.Before; |
|||
import org.junit.Test; |
|||
import org.thingsboard.common.util.JacksonUtil; |
|||
import org.thingsboard.server.common.data.Device; |
|||
import org.thingsboard.server.common.data.StringUtils; |
|||
import org.thingsboard.server.common.data.Tenant; |
|||
import org.thingsboard.server.common.data.User; |
|||
import org.thingsboard.server.common.data.device.credentials.BasicMqttCredentials; |
|||
import org.thingsboard.server.common.data.device.profile.MqttTopics; |
|||
import org.thingsboard.server.common.data.security.Authority; |
|||
import org.thingsboard.server.common.data.security.DeviceCredentials; |
|||
import org.thingsboard.server.common.data.security.DeviceCredentialsType; |
|||
import org.thingsboard.server.dao.service.DaoSqlTest; |
|||
import org.thingsboard.server.transport.mqtt.AbstractMqttIntegrationTest; |
|||
|
|||
import java.util.Arrays; |
|||
import java.util.HashSet; |
|||
import java.util.List; |
|||
import java.util.Set; |
|||
|
|||
import static org.junit.Assert.assertEquals; |
|||
import static org.junit.Assert.assertNotNull; |
|||
import static org.junit.Assert.assertNull; |
|||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; |
|||
|
|||
@DaoSqlTest |
|||
public class BasicMqttCredentialsTest extends AbstractMqttIntegrationTest { |
|||
|
|||
public static final String CLIENT_ID = "ClientId"; |
|||
public static final String USER_NAME1 = "UserName1"; |
|||
public static final String USER_NAME2 = "UserName2"; |
|||
public static final String USER_NAME3 = "UserName3"; |
|||
public static final String PASSWORD = "secret"; |
|||
|
|||
private Device clientIdDevice; |
|||
private Device clientIdAndUserNameDevice1; |
|||
private Device clientIdAndUserNameAndPasswordDevice2; |
|||
private Device clientIdAndUserNameAndPasswordDevice3; |
|||
private Device accessTokenDevice; |
|||
private Device accessToken2Device; |
|||
|
|||
|
|||
@Before |
|||
public void before() throws Exception { |
|||
loginSysAdmin(); |
|||
|
|||
Tenant tenant = new Tenant(); |
|||
tenant.setTitle("My tenant"); |
|||
savedTenant = doPost("/api/tenant", tenant, Tenant.class); |
|||
Assert.assertNotNull(savedTenant); |
|||
|
|||
tenantAdmin = new User(); |
|||
tenantAdmin.setAuthority(Authority.TENANT_ADMIN); |
|||
tenantAdmin.setTenantId(savedTenant.getId()); |
|||
tenantAdmin.setEmail("tenant" + atomicInteger.getAndIncrement() + "@thingsboard.org"); |
|||
tenantAdmin.setFirstName("Joe"); |
|||
tenantAdmin.setLastName("Downs"); |
|||
|
|||
tenantAdmin = createUserAndLogin(tenantAdmin, "testPassword1"); |
|||
|
|||
BasicMqttCredentials credValue = new BasicMqttCredentials(); |
|||
credValue.setClientId(CLIENT_ID); |
|||
clientIdDevice = createDevice("clientIdDevice", credValue); |
|||
|
|||
credValue = new BasicMqttCredentials(); |
|||
credValue.setClientId(CLIENT_ID); |
|||
credValue.setUserName(USER_NAME1); |
|||
clientIdAndUserNameDevice1 = createDevice("clientIdAndUserNameDevice", credValue); |
|||
|
|||
credValue = new BasicMqttCredentials(); |
|||
credValue.setClientId(CLIENT_ID); |
|||
credValue.setUserName(USER_NAME2); |
|||
credValue.setPassword(PASSWORD); |
|||
clientIdAndUserNameAndPasswordDevice2 = createDevice("clientIdAndUserNameAndPasswordDevice", credValue); |
|||
|
|||
credValue = new BasicMqttCredentials(); |
|||
credValue.setClientId(CLIENT_ID); |
|||
credValue.setUserName(USER_NAME3); |
|||
credValue.setPassword(PASSWORD); |
|||
clientIdAndUserNameAndPasswordDevice3 = createDevice("clientIdAndUserNameAndPasswordDevice2", credValue); |
|||
|
|||
accessTokenDevice = createDevice("accessTokenDevice", USER_NAME1); |
|||
accessToken2Device = createDevice("accessToken2Device", USER_NAME2); |
|||
} |
|||
|
|||
@Test |
|||
public void testCorrectCredentials() throws Exception { |
|||
// Check that correct devices receive telemetry
|
|||
testTelemetryIsDelivered(accessTokenDevice, getMqttAsyncClient(null, USER_NAME1, null)); |
|||
testTelemetryIsDelivered(clientIdDevice, getMqttAsyncClient(CLIENT_ID, null, null)); |
|||
testTelemetryIsDelivered(clientIdAndUserNameDevice1, getMqttAsyncClient(CLIENT_ID, USER_NAME1, null)); |
|||
testTelemetryIsDelivered(clientIdAndUserNameAndPasswordDevice2, getMqttAsyncClient(CLIENT_ID, USER_NAME2, PASSWORD)); |
|||
|
|||
// Also correct. Random clientId and password, but matches access token
|
|||
testTelemetryIsDelivered(accessToken2Device, getMqttAsyncClient(RandomStringUtils.randomAlphanumeric(10), USER_NAME2, RandomStringUtils.randomAlphanumeric(10))); |
|||
} |
|||
|
|||
@Test(expected = MqttSecurityException.class) |
|||
public void testCorrectClientIdAndUserNameButWrongPassword() throws Exception { |
|||
// Not correct. Correct clientId and username, but wrong password
|
|||
testTelemetryIsNotDelivered(clientIdAndUserNameAndPasswordDevice3, getMqttAsyncClient(CLIENT_ID, USER_NAME3, "WRONG PASSWORD")); |
|||
} |
|||
|
|||
private void testTelemetryIsDelivered(Device device, MqttAsyncClient client) throws Exception { |
|||
testTelemetryIsDelivered(device, client, true); |
|||
} |
|||
|
|||
private void testTelemetryIsNotDelivered(Device device, MqttAsyncClient client) throws Exception { |
|||
testTelemetryIsDelivered(device, client, false); |
|||
} |
|||
|
|||
private void testTelemetryIsDelivered(Device device, MqttAsyncClient client, boolean ok) throws Exception { |
|||
String randomKey = RandomStringUtils.randomAlphanumeric(10); |
|||
List<String> expectedKeys = Arrays.asList(randomKey); |
|||
publishMqttMsg(client, JacksonUtil.toString(JacksonUtil.newObjectNode().put(randomKey, true)).getBytes(), MqttTopics.DEVICE_TELEMETRY_TOPIC); |
|||
|
|||
String deviceId = device.getId().getId().toString(); |
|||
|
|||
long start = System.currentTimeMillis(); |
|||
long end = System.currentTimeMillis() + 5000; |
|||
|
|||
List<String> actualKeys = null; |
|||
while (start <= end) { |
|||
actualKeys = doGetAsyncTyped("/api/plugins/telemetry/DEVICE/" + deviceId + "/keys/timeseries", new TypeReference<>() { |
|||
}); |
|||
if (actualKeys.size() == expectedKeys.size()) { |
|||
break; |
|||
} |
|||
Thread.sleep(100); |
|||
start += 100; |
|||
} |
|||
if (ok) { |
|||
assertNotNull(actualKeys); |
|||
|
|||
Set<String> actualKeySet = new HashSet<>(actualKeys); |
|||
Set<String> expectedKeySet = new HashSet<>(expectedKeys); |
|||
|
|||
assertEquals(expectedKeySet, actualKeySet); |
|||
} else { |
|||
assertNull(actualKeys); |
|||
} |
|||
client.disconnect().waitForCompletion(); |
|||
} |
|||
|
|||
@After |
|||
public void after() throws Exception { |
|||
processAfterTest(); |
|||
} |
|||
|
|||
protected MqttAsyncClient getMqttAsyncClient(String clientId, String username, String password) throws MqttException { |
|||
if (StringUtils.isEmpty(clientId)) { |
|||
clientId = MqttAsyncClient.generateClientId(); |
|||
} |
|||
MqttAsyncClient client = new MqttAsyncClient(MQTT_URL, clientId, new MemoryPersistence()); |
|||
|
|||
MqttConnectOptions options = new MqttConnectOptions(); |
|||
if (StringUtils.isNotEmpty(username)) { |
|||
options.setUserName(username); |
|||
} |
|||
if (StringUtils.isNotEmpty(password)) { |
|||
options.setPassword(password.toCharArray()); |
|||
} |
|||
client.connect(options).waitForCompletion(); |
|||
return client; |
|||
} |
|||
|
|||
private Device createDevice(String deviceName, BasicMqttCredentials clientIdCredValue) throws Exception { |
|||
Device device = new Device(); |
|||
device.setName(deviceName); |
|||
device.setType("default"); |
|||
|
|||
device = doPost("/api/device", device, Device.class); |
|||
|
|||
DeviceCredentials clientIdCred = |
|||
doGet("/api/device/" + device.getId().getId().toString() + "/credentials", DeviceCredentials.class); |
|||
|
|||
clientIdCred.setCredentialsType(DeviceCredentialsType.MQTT_BASIC); |
|||
|
|||
|
|||
clientIdCred.setCredentialsValue(JacksonUtil.toString(clientIdCredValue)); |
|||
doPost("/api/device/credentials", clientIdCred).andExpect(status().isOk()); |
|||
return device; |
|||
} |
|||
|
|||
private Device createDevice(String deviceName, String accessToken) throws Exception { |
|||
Device device = new Device(); |
|||
device.setName(deviceName); |
|||
device.setType("default"); |
|||
|
|||
device = doPost("/api/device", device, Device.class); |
|||
|
|||
DeviceCredentials clientIdCred = |
|||
doGet("/api/device/" + device.getId().getId().toString() + "/credentials", DeviceCredentials.class); |
|||
|
|||
clientIdCred.setCredentialsType(DeviceCredentialsType.ACCESS_TOKEN); |
|||
clientIdCred.setCredentialsId(accessToken); |
|||
doPost("/api/device/credentials", clientIdCred).andExpect(status().isOk()); |
|||
return device; |
|||
} |
|||
} |
|||
Loading…
Reference in new issue