Merge PR #594: Bind a client connection to the token given in the hello call.

This commit is contained in:
jenkins 2023-02-21 17:43:20 +01:00
commit 0ca34e91c3
8 changed files with 120 additions and 63 deletions

View File

@ -296,6 +296,8 @@ JsonReply *JsonRPCServerImplementation::Hello(const QVariantMap &params, 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 &params, 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 &params,
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));
} }

View File

@ -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;

View File

@ -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()) {

View File

@ -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();

View File

@ -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();

View File

@ -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);
} }

View File

@ -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 &params, const QUuid &clientId) QVariant NymeaTestBase::injectAndWait(const QString &method, const QVariantMap &params, 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)));

View File

@ -56,7 +56,7 @@ protected slots:
void cleanup(); void cleanup();
protected: protected:
QVariant injectAndWait(const QString &method, const QVariantMap &params = QVariantMap(), const QUuid &clientId = QUuid()); QVariant injectAndWait(const QString &method, const QVariantMap &params = QVariantMap(), const QUuid &clientId = QUuid(), const QByteArray &tokenOverride = "default");
QVariant checkNotification(const QSignalSpy &spy, const QString &notification); QVariant checkNotification(const QSignalSpy &spy, const QString &notification);
QVariantList checkNotifications(const QSignalSpy &spy, const QString &notification); QVariantList checkNotifications(const QSignalSpy &spy, const QString &notification);