Merge PR #594: Bind a client connection to the token given in the hello call.
This commit is contained in:
commit
0ca34e91c3
@ -296,6 +296,8 @@ JsonReply *JsonRPCServerImplementation::Hello(const QVariantMap ¶ms, const J
|
|||||||
handshake.insert("cacheHashes", cacheHashes);
|
handshake.insert("cacheHashes", cacheHashes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
m_clientTokens[context.clientId()] = context.token();
|
||||||
|
|
||||||
bool badToken = false;
|
bool badToken = false;
|
||||||
if (!context.token().isEmpty()) {
|
if (!context.token().isEmpty()) {
|
||||||
TokenInfo tokenInfo = NymeaCore::instance()->userManager()->tokenInfo(context.token());
|
TokenInfo tokenInfo = NymeaCore::instance()->userManager()->tokenInfo(context.token());
|
||||||
@ -303,7 +305,7 @@ JsonReply *JsonRPCServerImplementation::Hello(const QVariantMap ¶ms, const J
|
|||||||
badToken = tokenInfo.id().isNull();
|
badToken = tokenInfo.id().isNull();
|
||||||
handshake.insert("authenticated", !badToken);
|
handshake.insert("authenticated", !badToken);
|
||||||
handshake.insert("permissionScopes", Types::scopesToStringList(userInfo.scopes()));
|
handshake.insert("permissionScopes", Types::scopesToStringList(userInfo.scopes()));
|
||||||
handshake.insert("username", userInfo.username());
|
handshake.insert("username", userInfo.username());
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the connection is locked down already (because of a previous failed attempt) and authentication failed
|
// If the connection is locked down already (because of a previous failed attempt) and authentication failed
|
||||||
@ -412,6 +414,8 @@ JsonReply *JsonRPCServerImplementation::Authenticate(const QVariantMap ¶ms,
|
|||||||
m_connectionLockdownTimer.start();
|
m_connectionLockdownTimer.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
m_clientTokens[context.clientId()] = token;
|
||||||
|
|
||||||
return createReply(ret);
|
return createReply(ret);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -596,11 +600,24 @@ void JsonRPCServerImplementation::processJsonPacket(TransportInterface *interfac
|
|||||||
QString targetNamespace = commandList.first();
|
QString targetNamespace = commandList.first();
|
||||||
QString method = commandList.last();
|
QString method = commandList.last();
|
||||||
|
|
||||||
|
// We'll only allow setting a new token in the hello call. All other calls must match the token given in the hello
|
||||||
|
QByteArray token = m_clientTokens.value(clientId);
|
||||||
|
if (methodString == "JSONRPC.Hello") {
|
||||||
|
token = message.value("token").toByteArray();
|
||||||
|
} else if (message.value("token").toByteArray() != token) {
|
||||||
|
qCWarning(dcJsonRpc()) << "Client changed token without redoing the handshake.";
|
||||||
|
qCDebug(dcJsonRpc) << "Old token:" << token << "new token:" << message.value("token").toByteArray();
|
||||||
|
sendUnauthorizedResponse(interface, clientId, commandId, "Changing the user (token) requires a new handshake. Call JSONRPC.Hello.");
|
||||||
|
interface->terminateClientConnection(clientId);
|
||||||
|
qCWarning(dcJsonRpc()) << "Staring connection lockdown timer";
|
||||||
|
m_connectionLockdownTimer.start();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// check if authentication is required for this transport
|
// check if authentication is required for this transport
|
||||||
if (interface->configuration().authenticationEnabled) {
|
if (interface->configuration().authenticationEnabled) {
|
||||||
QByteArray token = message.value("token").toByteArray();
|
QStringList authExemptMethodsNoUser = {"JSONRPC.Hello", "JSONRPC.RequestPushButtonAuth", "JSONRPC.CreateUser"};
|
||||||
QStringList authExemptMethodsNoUser = {"JSONRPC.Introspect", "JSONRPC.Hello", "JSONRPC.RequestPushButtonAuth", "JSONRPC.CreateUser"};
|
QStringList authExemptMethodsWithUser = {"JSONRPC.Hello", "JSONRPC.Authenticate", "JSONRPC.RequestPushButtonAuth"};
|
||||||
QStringList authExemptMethodsWithUser = {"JSONRPC.Introspect", "JSONRPC.Hello", "JSONRPC.Authenticate", "JSONRPC.RequestPushButtonAuth"};
|
|
||||||
// if there is no user in the system yet, let's fail unless this is a special method for authentication itself
|
// if there is no user in the system yet, let's fail unless this is a special method for authentication itself
|
||||||
if (NymeaCore::instance()->userManager()->initRequired()) {
|
if (NymeaCore::instance()->userManager()->initRequired()) {
|
||||||
if (!authExemptMethodsNoUser.contains(methodString) && (token.isEmpty() || !NymeaCore::instance()->userManager()->verifyToken(token))) {
|
if (!authExemptMethodsNoUser.contains(methodString) && (token.isEmpty() || !NymeaCore::instance()->userManager()->verifyToken(token))) {
|
||||||
@ -677,7 +694,7 @@ void JsonRPCServerImplementation::processJsonPacket(TransportInterface *interfac
|
|||||||
}
|
}
|
||||||
|
|
||||||
JsonContext callContext(clientId, m_clientLocales.value(clientId));
|
JsonContext callContext(clientId, m_clientLocales.value(clientId));
|
||||||
callContext.setToken(message.value("token").toByteArray());
|
callContext.setToken(token);
|
||||||
|
|
||||||
qCDebug(dcJsonRpc()) << "Invoking method" << targetNamespace + '.' + method << "from client" << clientId;
|
qCDebug(dcJsonRpc()) << "Invoking method" << targetNamespace + '.' + method << "from client" << clientId;
|
||||||
|
|
||||||
@ -833,6 +850,7 @@ void JsonRPCServerImplementation::onPushButtonAuthFinished(int transactionId, bo
|
|||||||
params.insert("success", success);
|
params.insert("success", success);
|
||||||
if (success) {
|
if (success) {
|
||||||
params.insert("token", token);
|
params.insert("token", token);
|
||||||
|
m_clientTokens[clientId] = token;
|
||||||
}
|
}
|
||||||
|
|
||||||
emit PushButtonAuthFinished(clientId, params);
|
emit PushButtonAuthFinished(clientId, params);
|
||||||
@ -981,6 +999,7 @@ void JsonRPCServerImplementation::clientDisconnected(const QUuid &clientId)
|
|||||||
m_clientNotifications.remove(clientId);
|
m_clientNotifications.remove(clientId);
|
||||||
m_clientBuffers.remove(clientId);
|
m_clientBuffers.remove(clientId);
|
||||||
m_clientLocales.remove(clientId);
|
m_clientLocales.remove(clientId);
|
||||||
|
m_clientTokens.remove(clientId);
|
||||||
if (m_pushButtonTransactions.values().contains(clientId)) {
|
if (m_pushButtonTransactions.values().contains(clientId)) {
|
||||||
NymeaCore::instance()->userManager()->cancelPushButtonAuth(m_pushButtonTransactions.key(clientId));
|
NymeaCore::instance()->userManager()->cancelPushButtonAuth(m_pushButtonTransactions.key(clientId));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -113,6 +113,7 @@ private:
|
|||||||
QHash<QUuid, QByteArray> m_clientBuffers;
|
QHash<QUuid, QByteArray> m_clientBuffers;
|
||||||
QHash<QUuid, QStringList> m_clientNotifications;
|
QHash<QUuid, QStringList> m_clientNotifications;
|
||||||
QHash<QUuid, QLocale> m_clientLocales;
|
QHash<QUuid, QLocale> m_clientLocales;
|
||||||
|
QHash<QUuid, QByteArray> m_clientTokens;
|
||||||
QHash<int, QUuid> m_pushButtonTransactions;
|
QHash<int, QUuid> m_pushButtonTransactions;
|
||||||
QHash<QUuid, QTimer*> m_newConnectionWaitTimers;
|
QHash<QUuid, QTimer*> m_newConnectionWaitTimers;
|
||||||
|
|
||||||
|
|||||||
@ -164,6 +164,7 @@ UserManager::UserError UserManager::createUser(const QString &username, const QS
|
|||||||
|
|
||||||
QSqlQuery checkForDuplicateUserQuery(m_db);
|
QSqlQuery checkForDuplicateUserQuery(m_db);
|
||||||
checkForDuplicateUserQuery.prepare("SELECT * FROM users WHERE lower(username) = ?;");
|
checkForDuplicateUserQuery.prepare("SELECT * FROM users WHERE lower(username) = ?;");
|
||||||
|
// Note: We're using toLower() on the username mainly for the reason that in old versions the username used to be an email address
|
||||||
checkForDuplicateUserQuery.addBindValue(username.toLower());
|
checkForDuplicateUserQuery.addBindValue(username.toLower());
|
||||||
checkForDuplicateUserQuery.exec();
|
checkForDuplicateUserQuery.exec();
|
||||||
if (checkForDuplicateUserQuery.first()) {
|
if (checkForDuplicateUserQuery.first()) {
|
||||||
|
|||||||
@ -248,20 +248,10 @@ void TestJSONRPC::testInitialSetup()
|
|||||||
QVERIFY(spy.isValid());
|
QVERIFY(spy.isValid());
|
||||||
QSignalSpy disconnectedSpy(m_mockTcpServer, &MockTcpServer::clientDisconnected);
|
QSignalSpy disconnectedSpy(m_mockTcpServer, &MockTcpServer::clientDisconnected);
|
||||||
|
|
||||||
// Introspect call should work in any case
|
QJsonDocument jsonDoc;
|
||||||
qCDebug(dcTests()) << "Calling Introspect, expecting success";
|
QVariantMap response;
|
||||||
m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"method\": \"JSONRPC.Introspect\"}\n");
|
|
||||||
if (spy.count() == 0) {
|
|
||||||
spy.wait();
|
|
||||||
}
|
|
||||||
QVERIFY(spy.count() == 1);
|
|
||||||
QJsonDocument jsonDoc = QJsonDocument::fromJson(spy.first().at(1).toByteArray());
|
|
||||||
QVariantMap response = jsonDoc.toVariant().toMap();
|
|
||||||
qCDebug(dcTests()) << "Result:" << response.value("status").toString() << response.value("error").toString();
|
|
||||||
QCOMPARE(response.value("status").toString(), QStringLiteral("success"));
|
|
||||||
|
|
||||||
|
// Hello call should work in any case
|
||||||
// Hello call should work in any case too
|
|
||||||
spy.clear();
|
spy.clear();
|
||||||
qCDebug(dcTests()) << "Calling Hello, expecting success";
|
qCDebug(dcTests()) << "Calling Hello, expecting success";
|
||||||
m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"method\": \"JSONRPC.Hello\"}\n");
|
m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"method\": \"JSONRPC.Hello\"}\n");
|
||||||
@ -335,10 +325,10 @@ void TestJSONRPC::testInitialSetup()
|
|||||||
QCOMPARE(response.value("status").toString(), QStringLiteral("success"));
|
QCOMPARE(response.value("status").toString(), QStringLiteral("success"));
|
||||||
QCOMPARE(NymeaCore::instance()->userManager()->users().count(), 0);
|
QCOMPARE(NymeaCore::instance()->userManager()->users().count(), 0);
|
||||||
|
|
||||||
// Now lets play by the rules (with an uppercase email)
|
// Now lets play by the rules (with an uppercase username)
|
||||||
spy.clear();
|
spy.clear();
|
||||||
qCDebug(dcTests()) << "Calling CreateUser, expecting success";
|
qCDebug(dcTests()) << "Calling CreateUser, expecting success";
|
||||||
m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"method\": \"JSONRPC.CreateUser\", \"params\": {\"username\": \"Dummy@guh.io\", \"password\": \"DummyPW1!\"}}\n");
|
m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"method\": \"JSONRPC.CreateUser\", \"params\": {\"username\": \"Dummy\", \"password\": \"DummyPW1!\"}}\n");
|
||||||
if (spy.count() == 0) {
|
if (spy.count() == 0) {
|
||||||
spy.wait();
|
spy.wait();
|
||||||
}
|
}
|
||||||
@ -396,7 +386,7 @@ void TestJSONRPC::testInitialSetup()
|
|||||||
// Now lets authenticate with a wrong user
|
// Now lets authenticate with a wrong user
|
||||||
spy.clear();
|
spy.clear();
|
||||||
qCDebug(dcTests()) << "Calling Authenticate, expecting failure (bad user)";
|
qCDebug(dcTests()) << "Calling Authenticate, expecting failure (bad user)";
|
||||||
m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"method\": \"JSONRPC.Authenticate\", \"params\": {\"username\": \"Dummy@wrong.domain\", \"password\": \"DummyPW1!\", \"deviceName\": \"testcase\"}}\n");
|
m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"method\": \"JSONRPC.Authenticate\", \"params\": {\"username\": \"Dummy-wrong\", \"password\": \"DummyPW1!\", \"deviceName\": \"testcase\"}}\n");
|
||||||
if (spy.count() == 0) {
|
if (spy.count() == 0) {
|
||||||
spy.wait();
|
spy.wait();
|
||||||
}
|
}
|
||||||
@ -413,7 +403,7 @@ void TestJSONRPC::testInitialSetup()
|
|||||||
spy.clear();
|
spy.clear();
|
||||||
disconnectedSpy.clear();
|
disconnectedSpy.clear();
|
||||||
qCDebug(dcTests()) << "Calling Authenticate, expecting failure (bad password)";
|
qCDebug(dcTests()) << "Calling Authenticate, expecting failure (bad password)";
|
||||||
m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"method\": \"JSONRPC.Authenticate\", \"params\": {\"username\": \"Dummy@guh.io\", \"password\": \"wrongpw\", \"deviceName\": \"testcase\"}}\n");
|
m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"method\": \"JSONRPC.Authenticate\", \"params\": {\"username\": \"Dummy\", \"password\": \"wrongpw\", \"deviceName\": \"testcase\"}}\n");
|
||||||
if (spy.count() == 0) {
|
if (spy.count() == 0) {
|
||||||
spy.wait();
|
spy.wait();
|
||||||
}
|
}
|
||||||
@ -439,10 +429,10 @@ void TestJSONRPC::testInitialSetup()
|
|||||||
}
|
}
|
||||||
QVERIFY(spy.count() == 1);
|
QVERIFY(spy.count() == 1);
|
||||||
|
|
||||||
// Now lets authenticate for real (but intentionally use a lowercase email here, should still work)
|
// Now lets authenticate for real (but intentionally use a lowercase username here, should still work)
|
||||||
spy.clear();
|
spy.clear();
|
||||||
qCDebug(dcTests()) << "Calling Authenticate, expecting success";
|
qCDebug(dcTests()) << "Calling Authenticate, expecting success";
|
||||||
m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"method\": \"JSONRPC.Authenticate\", \"params\": {\"username\": \"dummy@guh.io\", \"password\": \"DummyPW1!\", \"deviceName\": \"testcase\"}}\n");
|
m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"method\": \"JSONRPC.Authenticate\", \"params\": {\"username\": \"dummy\", \"password\": \"DummyPW1!\", \"deviceName\": \"testcase\"}}\n");
|
||||||
if (spy.count() == 0) {
|
if (spy.count() == 0) {
|
||||||
spy.wait();
|
spy.wait();
|
||||||
}
|
}
|
||||||
@ -491,11 +481,12 @@ void TestJSONRPC::testRevokeToken()
|
|||||||
QVariantList tokenList = response.value("params").toMap().value("tokenInfoList").toList();
|
QVariantList tokenList = response.value("params").toMap().value("tokenInfoList").toList();
|
||||||
QCOMPARE(tokenList.count(), 1);
|
QCOMPARE(tokenList.count(), 1);
|
||||||
QUuid oldTokenId = tokenList.first().toMap().value("id").toUuid();
|
QUuid oldTokenId = tokenList.first().toMap().value("id").toUuid();
|
||||||
|
qCDebug(dcTests()) << "have tokens:" << tokenList;
|
||||||
|
|
||||||
// Authenticate and create a new token
|
// Authenticate and create a new token
|
||||||
spy.clear();
|
spy.clear();
|
||||||
qCDebug(dcTests()) << "Calling Authenticate with valid credentials" ;
|
qCDebug(dcTests()) << "Calling Authenticate with valid credentials" ;
|
||||||
m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"method\": \"JSONRPC.Authenticate\", \"params\": {\"username\": \"dummy@guh.io\", \"password\": \"DummyPW1!\", \"deviceName\": \"testcase\"}}\n");
|
m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"token\": \"" + m_apiToken + "\", \"method\": \"JSONRPC.Authenticate\", \"params\": {\"username\": \"dummy\", \"password\": \"DummyPW1!\", \"deviceName\": \"testcase-revoke-token\"}}\n");
|
||||||
if (spy.count() == 0) {
|
if (spy.count() == 0) {
|
||||||
spy.wait();
|
spy.wait();
|
||||||
}
|
}
|
||||||
@ -504,12 +495,15 @@ void TestJSONRPC::testRevokeToken()
|
|||||||
response = jsonDoc.toVariant().toMap();
|
response = jsonDoc.toVariant().toMap();
|
||||||
QCOMPARE(response.value("status").toString(), QStringLiteral("success"));
|
QCOMPARE(response.value("status").toString(), QStringLiteral("success"));
|
||||||
QCOMPARE(response.value("params").toMap().value("success").toBool(), true);
|
QCOMPARE(response.value("params").toMap().value("success").toBool(), true);
|
||||||
QByteArray newToken = response.value("params").toMap().value("token").toByteArray();
|
// After an authenticate call, we need to continue with the new token
|
||||||
QVERIFY(!newToken.isEmpty());
|
QByteArray oldToken = m_apiToken;
|
||||||
|
m_apiToken = response.value("params").toMap().value("token").toByteArray();
|
||||||
|
QVERIFY(!m_apiToken.isEmpty());
|
||||||
|
|
||||||
|
|
||||||
// Now do a Version call with the new token and it should work
|
// Now do a Version call with the new token and it should work
|
||||||
spy.clear();
|
spy.clear();
|
||||||
m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"token\": \"" + newToken + "\", \"method\": \"JSONRPC.Version\"}\n");
|
m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"token\": \"" + m_apiToken + "\", \"method\": \"JSONRPC.Version\"}\n");
|
||||||
if (spy.count() == 0) {
|
if (spy.count() == 0) {
|
||||||
spy.wait();
|
spy.wait();
|
||||||
}
|
}
|
||||||
@ -519,7 +513,7 @@ void TestJSONRPC::testRevokeToken()
|
|||||||
qCDebug(dcTests()) << "Calling Version with valid token:" << response.value("status").toString() << response.value("error").toString();
|
qCDebug(dcTests()) << "Calling Version with valid token:" << response.value("status").toString() << response.value("error").toString();
|
||||||
QCOMPARE(response.value("status").toString(), QStringLiteral("success"));
|
QCOMPARE(response.value("status").toString(), QStringLiteral("success"));
|
||||||
|
|
||||||
// Now get all the tokens using the old token
|
// Now get all the tokens
|
||||||
spy.clear();
|
spy.clear();
|
||||||
qCDebug(dcTests()) << "Calling Tokens";
|
qCDebug(dcTests()) << "Calling Tokens";
|
||||||
m_mockTcpServer->injectData(m_clientId, "{\"id\": 123, \"token\": \"" + m_apiToken + "\", \"method\": \"Users.GetTokens\"}\n");
|
m_mockTcpServer->injectData(m_clientId, "{\"id\": 123, \"token\": \"" + m_apiToken + "\", \"method\": \"Users.GetTokens\"}\n");
|
||||||
@ -531,21 +525,14 @@ void TestJSONRPC::testRevokeToken()
|
|||||||
response = jsonDoc.toVariant().toMap();
|
response = jsonDoc.toVariant().toMap();
|
||||||
QCOMPARE(response.value("status").toString(), QStringLiteral("success"));
|
QCOMPARE(response.value("status").toString(), QStringLiteral("success"));
|
||||||
tokenList = response.value("params").toMap().value("tokenInfoList").toList();
|
tokenList = response.value("params").toMap().value("tokenInfoList").toList();
|
||||||
|
qCDebug(dcTests()) << "have tokens:" << tokenList;
|
||||||
QCOMPARE(tokenList.count(), 2);
|
QCOMPARE(tokenList.count(), 2);
|
||||||
|
|
||||||
// find the new token
|
|
||||||
QUuid newTokenId;
|
|
||||||
foreach (const QVariant &tokenInfo, tokenList) {
|
|
||||||
if (tokenInfo.toMap().value("id").toUuid() != oldTokenId) {
|
|
||||||
newTokenId = tokenInfo.toMap().value("id").toUuid();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Revoke the new token
|
// Revoke the old token
|
||||||
spy.clear();
|
spy.clear();
|
||||||
qCDebug(dcTests()) << "Calling RemoveToken";
|
qCDebug(dcTests()) << "Calling RemoveToken";
|
||||||
m_mockTcpServer->injectData(m_clientId, "{\"id\": 123, \"token\": \"" + m_apiToken + "\", \"method\": \"Users.RemoveToken\", \"params\": {\"tokenId\": \"" + newTokenId.toByteArray() + "\"}}\n");
|
m_mockTcpServer->injectData(m_clientId, "{\"id\": 123, \"token\": \"" + m_apiToken + "\", \"method\": \"Users.RemoveToken\", \"params\": {\"tokenId\": \"" + oldTokenId.toByteArray() + "\"}}\n");
|
||||||
if (spy.count() == 0) {
|
if (spy.count() == 0) {
|
||||||
spy.wait();
|
spy.wait();
|
||||||
}
|
}
|
||||||
@ -554,11 +541,40 @@ void TestJSONRPC::testRevokeToken()
|
|||||||
response = jsonDoc.toVariant().toMap();
|
response = jsonDoc.toVariant().toMap();
|
||||||
QCOMPARE(response.value("status").toString(), QStringLiteral("success"));
|
QCOMPARE(response.value("status").toString(), QStringLiteral("success"));
|
||||||
|
|
||||||
// Do a call with the now removed token, it should be forbidden
|
// Get all the tokens and see if it's down by one.
|
||||||
|
spy.clear();
|
||||||
|
qCDebug(dcTests()) << "Calling Tokens";
|
||||||
|
m_mockTcpServer->injectData(m_clientId, "{\"id\": 123, \"token\": \"" + m_apiToken + "\", \"method\": \"Users.GetTokens\"}\n");
|
||||||
|
if (spy.count() == 0) {
|
||||||
|
spy.wait();
|
||||||
|
}
|
||||||
|
QVERIFY(spy.count() == 1);
|
||||||
|
jsonDoc = QJsonDocument::fromJson(spy.first().at(1).toByteArray());
|
||||||
|
response = jsonDoc.toVariant().toMap();
|
||||||
|
QCOMPARE(response.value("status").toString(), QStringLiteral("success"));
|
||||||
|
tokenList = response.value("params").toMap().value("tokenInfoList").toList();
|
||||||
|
qCDebug(dcTests()) << "have tokens:" << tokenList;
|
||||||
|
QCOMPARE(tokenList.count(), 1);
|
||||||
|
|
||||||
|
// Do a handshake with the old removed token, it should work, but telling us the token is invalid
|
||||||
|
spy.clear();
|
||||||
|
disconnectedSpy.clear();
|
||||||
|
qCDebug(dcTests()) << "Calling Hello with now removed token";
|
||||||
|
m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"token\": \"" + oldToken + "\", \"method\": \"JSONRPC.Hello\"}\n");
|
||||||
|
if (spy.count() == 0) {
|
||||||
|
spy.wait();
|
||||||
|
}
|
||||||
|
QVERIFY(spy.count() == 1);
|
||||||
|
jsonDoc = QJsonDocument::fromJson(spy.first().at(1).toByteArray());
|
||||||
|
response = jsonDoc.toVariant().toMap();
|
||||||
|
QCOMPARE(response.value("status").toString(), QStringLiteral("success"));
|
||||||
|
QCOMPARE(response.value("params").toMap().value("authenticated").toBool(), false);
|
||||||
|
|
||||||
|
// Do a version call with the old removed token, it should be forbidden
|
||||||
spy.clear();
|
spy.clear();
|
||||||
disconnectedSpy.clear();
|
disconnectedSpy.clear();
|
||||||
qCDebug(dcTests()) << "Calling Version with now removed token";
|
qCDebug(dcTests()) << "Calling Version with now removed token";
|
||||||
m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"token\": \"" + newToken + "\", \"method\": \"JSONRPC.Version\"}\n");
|
m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"token\": \"" + oldToken + "\", \"method\": \"JSONRPC.Version\"}\n");
|
||||||
if (spy.count() == 0) {
|
if (spy.count() == 0) {
|
||||||
spy.wait();
|
spy.wait();
|
||||||
}
|
}
|
||||||
@ -584,16 +600,16 @@ void TestJSONRPC::testBasicCall_data()
|
|||||||
QTest::addColumn<bool>("idValid");
|
QTest::addColumn<bool>("idValid");
|
||||||
QTest::addColumn<bool>("valid");
|
QTest::addColumn<bool>("valid");
|
||||||
|
|
||||||
QTest::newRow("valid call 1") << QByteArray("{\"id\":42, \"method\":\"JSONRPC.Introspect\"}") << true << true;
|
QTest::newRow("valid call 1") << QByteArray("{\"id\":42, \"method\":\"JSONRPC.Introspect\", \"token\": \"" + m_apiToken + "\"}") << true << true;
|
||||||
QTest::newRow("valid call 2") << QByteArray("{\"id\":42, \"method\":\"JSONRPC.Introspect\"}\n") << true << true;
|
QTest::newRow("valid call 2") << QByteArray("{\"id\":42, \"method\":\"JSONRPC.Introspect\", \"token\": \"" + m_apiToken + "\"}\n") << true << true;
|
||||||
QTest::newRow("valid call 3") << QByteArray("{\"id\":42, \"method\":\"JSONRPC.Introspect\"}\n\n\n\n") << true << true;
|
QTest::newRow("valid call 3") << QByteArray("{\"id\":42, \"method\":\"JSONRPC.Introspect\", \"token\": \"" + m_apiToken + "\"}\n\n\n\n") << true << true;
|
||||||
QTest::newRow("missing id") << QByteArray("{\"method\":\"JSONRPC.Introspect\"}\n") << false << false;
|
QTest::newRow("missing id") << QByteArray("{\"method\":\"JSONRPC.Introspect\", \"token\": \"" + m_apiToken + "\"}\n") << false << false;
|
||||||
QTest::newRow("missing method") << QByteArray("{\"id\":42}\n") << true << false;
|
QTest::newRow("missing method") << QByteArray("{\"id\":42, \"token\": \"" + m_apiToken + "\"}\n") << true << false;
|
||||||
QTest::newRow("borked") << QByteArray("{\"id\":42, \"method\":\"JSO}\n") << false << false;
|
QTest::newRow("borked") << QByteArray("{\"id\":42,, \"token\": \"" + m_apiToken + "\" \"method\":\"JSO}\n") << false << false;
|
||||||
QTest::newRow("invalid function") << QByteArray("{\"id\":42, \"method\":\"JSONRPC.Foobar\", \"token\": \"" + m_apiToken + "\"}\n") << true << false;
|
QTest::newRow("invalid function") << QByteArray("{\"id\":42, \"method\":\"JSONRPC.Foobar\", \"token\": \"" + m_apiToken + "\"}\n") << true << false;
|
||||||
QTest::newRow("invalid namespace") << QByteArray("{\"id\":42, \"method\":\"FOO.Introspect\", \"token\": \"" + m_apiToken + "\"}\n") << true << false;
|
QTest::newRow("invalid namespace") << QByteArray("{\"id\":42, \"method\":\"FOO.Introspect\", \"token\": \"" + m_apiToken + "\"}\n") << true << false;
|
||||||
QTest::newRow("missing dot") << QByteArray("{\"id\":42, \"method\":\"JSONRPCIntrospect\", \"token\": \"" + m_apiToken + "\"}\n") << true << false;
|
QTest::newRow("missing dot") << QByteArray("{\"id\":42, \"method\":\"JSONRPCIntrospect\", \"token\": \"" + m_apiToken + "\"}\n") << true << false;
|
||||||
QTest::newRow("invalid params") << QByteArray("{\"id\":42, \"method\":\"JSONRPC.Introspect\", \"params\":{\"törööö\":\"chooo-chooo\"}}\n") << true << false;
|
QTest::newRow("invalid params") << QByteArray("{\"id\":42, \"method\":\"JSONRPC.Introspect\", \"token\": \"" + m_apiToken + "\", \"params\":{\"törööö\":\"chooo-chooo\"}}\n") << true << false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void TestJSONRPC::testBasicCall()
|
void TestJSONRPC::testBasicCall()
|
||||||
@ -980,7 +996,7 @@ void TestJSONRPC::testPushButtonAuth()
|
|||||||
int transactionId = response.toMap().value("params").toMap().value("transactionId").toInt();
|
int transactionId = response.toMap().value("params").toMap().value("transactionId").toInt();
|
||||||
|
|
||||||
// Setup connection to mock client
|
// Setup connection to mock client
|
||||||
QSignalSpy clientSpy(m_mockTcpServer, SIGNAL(outgoingData(QUuid,QByteArray)));
|
QSignalSpy clientSpy(m_mockTcpServer, &MockTcpServer::outgoingData);
|
||||||
|
|
||||||
pushButtonAgent.sendButtonPressed();
|
pushButtonAgent.sendButtonPressed();
|
||||||
|
|
||||||
@ -989,8 +1005,14 @@ void TestJSONRPC::testPushButtonAuth()
|
|||||||
|
|
||||||
QCOMPARE(rsp.value("params").toMap().value("transactionId").toInt(), transactionId);
|
QCOMPARE(rsp.value("params").toMap().value("transactionId").toInt(), transactionId);
|
||||||
QVERIFY2(!rsp.value("params").toMap().value("token").toByteArray().isEmpty(), "Token not in push button auth notification");
|
QVERIFY2(!rsp.value("params").toMap().value("token").toByteArray().isEmpty(), "Token not in push button auth notification");
|
||||||
}
|
|
||||||
|
|
||||||
|
m_apiToken = rsp.value("params").toMap().value("token").toByteArray();
|
||||||
|
|
||||||
|
qCDebug(dcTests()) << "Invoking Version";
|
||||||
|
// Test a regular call to verify we're actually authenticated
|
||||||
|
response = injectAndWait("JSONRPC.Version");
|
||||||
|
QVERIFY2(response.toMap().value("status").toString() == "success", "JSONRPC.Version call failed after push button auth!");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
void TestJSONRPC::testPushButtonAuthInterrupt()
|
void TestJSONRPC::testPushButtonAuthInterrupt()
|
||||||
@ -1024,7 +1046,7 @@ void TestJSONRPC::testPushButtonAuthInterrupt()
|
|||||||
clientSpy.clear();
|
clientSpy.clear();
|
||||||
params.clear();
|
params.clear();
|
||||||
params.insert("deviceName", "mallory");
|
params.insert("deviceName", "mallory");
|
||||||
response = injectAndWait("JSONRPC.RequestPushButtonAuth", params, malloryId);
|
response = injectAndWait("JSONRPC.RequestPushButtonAuth", params, malloryId, "");
|
||||||
QCOMPARE(response.toMap().value("params").toMap().value("success").toBool(), true);
|
QCOMPARE(response.toMap().value("params").toMap().value("success").toBool(), true);
|
||||||
int transactionId2 = response.toMap().value("params").toMap().value("transactionId").toInt();
|
int transactionId2 = response.toMap().value("params").toMap().value("transactionId").toInt();
|
||||||
|
|
||||||
@ -1097,6 +1119,7 @@ void TestJSONRPC::testPushButtonAuthInterrupt()
|
|||||||
QCOMPARE(notification.value("params").toMap().value("transactionId").toInt(), transactionId3);
|
QCOMPARE(notification.value("params").toMap().value("transactionId").toInt(), transactionId3);
|
||||||
QCOMPARE(notification.value("params").toMap().value("success").toBool(), true);
|
QCOMPARE(notification.value("params").toMap().value("success").toBool(), true);
|
||||||
QVERIFY2(!notification.value("params").toMap().value("token").toByteArray().isEmpty(), "Token is empty while it shouldn't be");
|
QVERIFY2(!notification.value("params").toMap().value("token").toByteArray().isEmpty(), "Token is empty while it shouldn't be");
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void TestJSONRPC::testPushButtonAuthConnectionDrop()
|
void TestJSONRPC::testPushButtonAuthConnectionDrop()
|
||||||
@ -1116,7 +1139,7 @@ void TestJSONRPC::testPushButtonAuthConnectionDrop()
|
|||||||
// request push button auth for client 1 (alice) and check for OK reply
|
// request push button auth for client 1 (alice) and check for OK reply
|
||||||
QVariantMap params;
|
QVariantMap params;
|
||||||
params.insert("deviceName", "alice");
|
params.insert("deviceName", "alice");
|
||||||
QVariant response = injectAndWait("JSONRPC.RequestPushButtonAuth", params, aliceId);
|
QVariant response = injectAndWait("JSONRPC.RequestPushButtonAuth", params, aliceId, "");
|
||||||
QCOMPARE(response.toMap().value("params").toMap().value("success").toBool(), true);
|
QCOMPARE(response.toMap().value("params").toMap().value("success").toBool(), true);
|
||||||
|
|
||||||
// Disconnect alice
|
// Disconnect alice
|
||||||
@ -1133,7 +1156,7 @@ void TestJSONRPC::testPushButtonAuthConnectionDrop()
|
|||||||
// request push button auth for client 2 (bob) and check for OK reply
|
// request push button auth for client 2 (bob) and check for OK reply
|
||||||
params.clear();
|
params.clear();
|
||||||
params.insert("deviceName", "bob");
|
params.insert("deviceName", "bob");
|
||||||
response = injectAndWait("JSONRPC.RequestPushButtonAuth", params, bobId);
|
response = injectAndWait("JSONRPC.RequestPushButtonAuth", params, bobId, "");
|
||||||
QCOMPARE(response.toMap().value("params").toMap().value("success").toBool(), true);
|
QCOMPARE(response.toMap().value("params").toMap().value("success").toBool(), true);
|
||||||
int transactionId = response.toMap().value("params").toMap().value("transactionId").toInt();
|
int transactionId = response.toMap().value("params").toMap().value("transactionId").toInt();
|
||||||
|
|
||||||
@ -1154,7 +1177,6 @@ void TestJSONRPC::testPushButtonAuthConnectionDrop()
|
|||||||
QCOMPARE(notification.value("params").toMap().value("transactionId").toInt(), transactionId);
|
QCOMPARE(notification.value("params").toMap().value("transactionId").toInt(), transactionId);
|
||||||
QCOMPARE(notification.value("params").toMap().value("success").toBool(), true);
|
QCOMPARE(notification.value("params").toMap().value("success").toBool(), true);
|
||||||
QVERIFY2(!notification.value("params").toMap().value("token").toByteArray().isEmpty(), "Token is empty while it shouldn't be");
|
QVERIFY2(!notification.value("params").toMap().value("token").toByteArray().isEmpty(), "Token is empty while it shouldn't be");
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void TestJSONRPC::testInitialSetupWithPushButtonAuth()
|
void TestJSONRPC::testInitialSetupWithPushButtonAuth()
|
||||||
@ -1194,7 +1216,7 @@ void TestJSONRPC::testInitialSetupWithPushButtonAuth()
|
|||||||
|
|
||||||
QVariantMap params;
|
QVariantMap params;
|
||||||
params.insert("deviceName", "alice");
|
params.insert("deviceName", "alice");
|
||||||
response = injectAndWait("JSONRPC.RequestPushButtonAuth", params, aliceId);
|
response = injectAndWait("JSONRPC.RequestPushButtonAuth", params, aliceId, "");
|
||||||
QCOMPARE(response.toMap().value("params").toMap().value("success").toBool(), true);
|
QCOMPARE(response.toMap().value("params").toMap().value("success").toBool(), true);
|
||||||
int transactionId = response.toMap().value("params").toMap().value("transactionId").toInt();
|
int transactionId = response.toMap().value("params").toMap().value("transactionId").toInt();
|
||||||
|
|
||||||
|
|||||||
@ -197,6 +197,8 @@ void TestUsermanager::createUser()
|
|||||||
void TestUsermanager::authenticate()
|
void TestUsermanager::authenticate()
|
||||||
{
|
{
|
||||||
m_apiToken.clear();
|
m_apiToken.clear();
|
||||||
|
injectAndWait("JSONRPC.Hello");
|
||||||
|
|
||||||
createUser();
|
createUser();
|
||||||
|
|
||||||
QVariantMap params;
|
QVariantMap params;
|
||||||
@ -237,6 +239,13 @@ void TestUsermanager::authenticatePushButton()
|
|||||||
|
|
||||||
QCOMPARE(rsp.value("params").toMap().value("transactionId").toInt(), transactionId);
|
QCOMPARE(rsp.value("params").toMap().value("transactionId").toInt(), transactionId);
|
||||||
QVERIFY2(!rsp.value("params").toMap().value("token").toByteArray().isEmpty(), "Token not in push button auth notification");
|
QVERIFY2(!rsp.value("params").toMap().value("token").toByteArray().isEmpty(), "Token not in push button auth notification");
|
||||||
|
|
||||||
|
m_apiToken = rsp.value("params").toMap().value("token").toByteArray();
|
||||||
|
|
||||||
|
qCDebug(dcTests()) << "Invoking Version";
|
||||||
|
// Test a regular call to verify we're actually authenticated
|
||||||
|
response = injectAndWait("JSONRPC.Version");
|
||||||
|
QVERIFY2(response.toMap().value("status").toString() == "success", "JSONRPC.Version call failed after push button auth!");
|
||||||
}
|
}
|
||||||
|
|
||||||
void TestUsermanager::authenticatePushButtonAuthInterrupt()
|
void TestUsermanager::authenticatePushButtonAuthInterrupt()
|
||||||
@ -269,7 +278,7 @@ void TestUsermanager::authenticatePushButtonAuthInterrupt()
|
|||||||
clientSpy.clear();
|
clientSpy.clear();
|
||||||
params.clear();
|
params.clear();
|
||||||
params.insert("deviceName", "mallory");
|
params.insert("deviceName", "mallory");
|
||||||
response = injectAndWait("JSONRPC.RequestPushButtonAuth", params, malloryId);
|
response = injectAndWait("JSONRPC.RequestPushButtonAuth", params, malloryId, "");
|
||||||
QCOMPARE(response.toMap().value("params").toMap().value("success").toBool(), true);
|
QCOMPARE(response.toMap().value("params").toMap().value("success").toBool(), true);
|
||||||
int transactionId2 = response.toMap().value("params").toMap().value("transactionId").toInt();
|
int transactionId2 = response.toMap().value("params").toMap().value("transactionId").toInt();
|
||||||
|
|
||||||
@ -361,7 +370,7 @@ void TestUsermanager::authenticatePushButtonAuthConnectionDrop()
|
|||||||
// request push button auth for client 1 (alice) and check for OK reply
|
// request push button auth for client 1 (alice) and check for OK reply
|
||||||
QVariantMap params;
|
QVariantMap params;
|
||||||
params.insert("deviceName", "alice");
|
params.insert("deviceName", "alice");
|
||||||
QVariant response = injectAndWait("JSONRPC.RequestPushButtonAuth", params, aliceId);
|
QVariant response = injectAndWait("JSONRPC.RequestPushButtonAuth", params, aliceId, "");
|
||||||
QCOMPARE(response.toMap().value("params").toMap().value("success").toBool(), true);
|
QCOMPARE(response.toMap().value("params").toMap().value("success").toBool(), true);
|
||||||
|
|
||||||
// Disconnect alice
|
// Disconnect alice
|
||||||
@ -378,7 +387,7 @@ void TestUsermanager::authenticatePushButtonAuthConnectionDrop()
|
|||||||
// request push button auth for client 2 (bob) and check for OK reply
|
// request push button auth for client 2 (bob) and check for OK reply
|
||||||
params.clear();
|
params.clear();
|
||||||
params.insert("deviceName", "bob");
|
params.insert("deviceName", "bob");
|
||||||
response = injectAndWait("JSONRPC.RequestPushButtonAuth", params, bobId);
|
response = injectAndWait("JSONRPC.RequestPushButtonAuth", params, bobId, "");
|
||||||
QCOMPARE(response.toMap().value("params").toMap().value("success").toBool(), true);
|
QCOMPARE(response.toMap().value("params").toMap().value("success").toBool(), true);
|
||||||
int transactionId = response.toMap().value("params").toMap().value("transactionId").toInt();
|
int transactionId = response.toMap().value("params").toMap().value("transactionId").toInt();
|
||||||
|
|
||||||
|
|||||||
@ -83,7 +83,7 @@ void TestWebSocketServer::initTestCase()
|
|||||||
config.address = "127.0.0.1";
|
config.address = "127.0.0.1";
|
||||||
config.port = 4444;
|
config.port = 4444;
|
||||||
config.sslEnabled = true;
|
config.sslEnabled = true;
|
||||||
config.authenticationEnabled = true;
|
config.authenticationEnabled = false;
|
||||||
NymeaCore::instance()->configuration()->setWebSocketServerConfiguration(config);
|
NymeaCore::instance()->configuration()->setWebSocketServerConfiguration(config);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -87,6 +87,11 @@ void NymeaTestBase::initTestCase(const QString &loggingRules)
|
|||||||
qApp->processEvents();
|
qApp->processEvents();
|
||||||
qCDebug(dcTests()) << "Nymea core instance initialized. Creating dummy user.";
|
qCDebug(dcTests()) << "Nymea core instance initialized. Creating dummy user.";
|
||||||
|
|
||||||
|
foreach (const UserInfo &userInfo, NymeaCore::instance()->userManager()->users()) {
|
||||||
|
NymeaCore::instance()->userManager()->removeUser(userInfo.username());
|
||||||
|
}
|
||||||
|
NymeaCore::instance()->userManager()->removeUser("");
|
||||||
|
|
||||||
// Yes, we're intentionally mixing upper/lower case email here... username should not be case sensitive
|
// Yes, we're intentionally mixing upper/lower case email here... username should not be case sensitive
|
||||||
NymeaCore::instance()->userManager()->removeUser("dummy");
|
NymeaCore::instance()->userManager()->removeUser("dummy");
|
||||||
NymeaCore::instance()->userManager()->createUser("dummy", "DummyPW1!", "dummy@guh.io", "Dummy", Types::PermissionScopeAdmin);
|
NymeaCore::instance()->userManager()->createUser("dummy", "DummyPW1!", "dummy@guh.io", "Dummy", Types::PermissionScopeAdmin);
|
||||||
@ -128,13 +133,13 @@ void NymeaTestBase::cleanup()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QVariant NymeaTestBase::injectAndWait(const QString &method, const QVariantMap ¶ms, const QUuid &clientId)
|
QVariant NymeaTestBase::injectAndWait(const QString &method, const QVariantMap ¶ms, const QUuid &clientId, const QByteArray &tokenOverride)
|
||||||
{
|
{
|
||||||
QVariantMap call;
|
QVariantMap call;
|
||||||
call.insert("id", m_commandId);
|
call.insert("id", m_commandId);
|
||||||
call.insert("method", method);
|
call.insert("method", method);
|
||||||
call.insert("params", params);
|
call.insert("params", params);
|
||||||
call.insert("token", m_apiToken);
|
call.insert("token", tokenOverride == "default" ? m_apiToken : tokenOverride);
|
||||||
|
|
||||||
QJsonDocument jsonDoc = QJsonDocument::fromVariant(call);
|
QJsonDocument jsonDoc = QJsonDocument::fromVariant(call);
|
||||||
QSignalSpy spy(m_mockTcpServer, SIGNAL(outgoingData(QUuid,QByteArray)));
|
QSignalSpy spy(m_mockTcpServer, SIGNAL(outgoingData(QUuid,QByteArray)));
|
||||||
|
|||||||
@ -56,7 +56,7 @@ protected slots:
|
|||||||
void cleanup();
|
void cleanup();
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
QVariant injectAndWait(const QString &method, const QVariantMap ¶ms = QVariantMap(), const QUuid &clientId = QUuid());
|
QVariant injectAndWait(const QString &method, const QVariantMap ¶ms = QVariantMap(), const QUuid &clientId = QUuid(), const QByteArray &tokenOverride = "default");
|
||||||
QVariant checkNotification(const QSignalSpy &spy, const QString ¬ification);
|
QVariant checkNotification(const QSignalSpy &spy, const QString ¬ification);
|
||||||
QVariantList checkNotifications(const QSignalSpy &spy, const QString ¬ification);
|
QVariantList checkNotifications(const QSignalSpy &spy, const QString ¬ification);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user