From 44dd09d22725a9ed1f0279f16bc63a2c0c6a6ad9 Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Tue, 10 Apr 2018 21:50:24 +0200 Subject: [PATCH] fix initialSetupRequired still being true, even if we already gave out tokens by pushbuttonAuth --- libnymea-core/jsonrpc/jsonrpcserver.cpp | 4 +- libnymea-core/usermanager.cpp | 37 +++++++-- libnymea-core/usermanager.h | 1 + tests/auto/jsonrpc/testjsonrpc.cpp | 103 +++++++++++++++++++++++- 4 files changed, 135 insertions(+), 10 deletions(-) diff --git a/libnymea-core/jsonrpc/jsonrpcserver.cpp b/libnymea-core/jsonrpc/jsonrpcserver.cpp index 17474a93..74add4de 100644 --- a/libnymea-core/jsonrpc/jsonrpcserver.cpp +++ b/libnymea-core/jsonrpc/jsonrpcserver.cpp @@ -436,7 +436,7 @@ QVariantMap JsonRPCServer::createWelcomeMessage(TransportInterface *interface) c handshake.insert("uuid", NymeaCore::instance()->configuration()->serverUuid().toString()); handshake.insert("language", NymeaCore::instance()->configuration()->locale().name()); handshake.insert("protocol version", JSON_PROTOCOL_VERSION); - handshake.insert("initialSetupRequired", (interface->configuration().authenticationEnabled ? NymeaCore::instance()->userManager()->users().isEmpty() : false)); + handshake.insert("initialSetupRequired", (interface->configuration().authenticationEnabled ? NymeaCore::instance()->userManager()->initRequired() : false)); handshake.insert("authenticationRequired", interface->configuration().authenticationEnabled); handshake.insert("pushButtonAuthAvailable", NymeaCore::instance()->userManager()->pushButtonAuthAvailable()); return handshake; @@ -497,7 +497,7 @@ void JsonRPCServer::processData(const QUuid &clientId, const QByteArray &data) QStringList authExemptMethodsNoUser = {"Introspect", "Hello", "CreateUser", "RequestPushButtonAuth"}; QStringList authExemptMethodsWithUser = {"Introspect", "Hello", "Authenticate", "RequestPushButtonAuth"}; // if there is no user in the system yet, let's fail unless this is special method for authentication itself - if (NymeaCore::instance()->userManager()->users().isEmpty()) { + if (NymeaCore::instance()->userManager()->initRequired()) { if (!(targetNamespace == "JSONRPC" && authExemptMethodsNoUser.contains(method)) && (token.isEmpty() || !NymeaCore::instance()->userManager()->verifyToken(token))) { sendUnauthorizedResponse(interface, clientId, commandId, "Initial setup required. Call CreateUser first."); return; diff --git a/libnymea-core/usermanager.cpp b/libnymea-core/usermanager.cpp index 6fe28cfa..c7304c82 100644 --- a/libnymea-core/usermanager.cpp +++ b/libnymea-core/usermanager.cpp @@ -51,6 +51,23 @@ UserManager::UserManager(QObject *parent) : QObject(parent) m_pushButtonTransaction = qMakePair(-1, QString()); } +/** Will return true if the database is working fine but doesn't have any information on users whatsoever. + * That is, neither a user nor an anonimous token. + * This may be used to determine whether a first-time setup is required. + */ +bool UserManager::initRequired() const +{ + QString getTokensQuery = QString("SELECT id, username, creationdate, deviceName FROM tokens;"); + QSqlQuery result = m_db.exec(getTokensQuery); + if (m_db.lastError().type() != QSqlError::NoError) { + qCWarning(dcUserManager) << "Query for tokens failed:" << m_db.lastError().databaseText() << m_db.lastError().driverText() << getTokensQuery; + // Note: do not return true in case the database access fails. + return false; + } + + return users().isEmpty() && !result.first(); +} + QStringList UserManager::users() const { QString userQuery("SELECT username FROM users;"); @@ -96,16 +113,22 @@ UserManager::UserError UserManager::createUser(const QString &username, const QS return UserErrorNoError; } +/** Remove the given user and all of its tokens. If the username is empty, all anonymous tokens (e.g. issued by pushbutton auth) will be cleared. */ UserManager::UserError UserManager::removeUser(const QString &username) { - QString dropUserQuery = QString("DELETE FROM users WHERE lower(username) =\"%1\";").arg(username.toLower()); - QSqlQuery result = m_db.exec(dropUserQuery); - if (result.numRowsAffected() == 0) { - return UserErrorInvalidUserId; - } + if (!username.isEmpty()) { + QString dropUserQuery = QString("DELETE FROM users WHERE lower(username) =\"%1\";").arg(username.toLower()); + QSqlQuery result = m_db.exec(dropUserQuery); + if (result.numRowsAffected() == 0) { + return UserErrorInvalidUserId; + } - QString dropTokensQuery = QString("DELETE FROM tokens WHERE lower(username) = \"%1\";").arg(username.toLower()); - m_db.exec(dropTokensQuery); + QString dropTokensQuery = QString("DELETE FROM tokens WHERE lower(username) = \"%1\";").arg(username.toLower()); + m_db.exec(dropTokensQuery); + } else { + QString dropTokensQuery = QString("DELETE FROM tokens WHERE username = \"\";").arg(username.toLower()); + m_db.exec(dropTokensQuery); + } return UserErrorNoError; } diff --git a/libnymea-core/usermanager.h b/libnymea-core/usermanager.h index 59ea5b74..1041978f 100644 --- a/libnymea-core/usermanager.h +++ b/libnymea-core/usermanager.h @@ -47,6 +47,7 @@ public: explicit UserManager(QObject *parent = 0); + bool initRequired() const; QStringList users() const; UserError createUser(const QString &username, const QString &password); diff --git a/tests/auto/jsonrpc/testjsonrpc.cpp b/tests/auto/jsonrpc/testjsonrpc.cpp index 4d1efd77..fd4dcb52 100644 --- a/tests/auto/jsonrpc/testjsonrpc.cpp +++ b/tests/auto/jsonrpc/testjsonrpc.cpp @@ -99,6 +99,7 @@ private slots: void testPushButtonAuthConnectionDrop(); + void testInitialSetupWithPushButtonAuth(); private: QStringList extractRefs(const QVariant &variant); @@ -169,6 +170,9 @@ void TestJSONRPC::testInitialSetup() foreach (const QString &user, NymeaCore::instance()->userManager()->users()) { NymeaCore::instance()->userManager()->removeUser(user); } + NymeaCore::instance()->userManager()->removeUser(""); + + QVERIFY(NymeaCore::instance()->userManager()->initRequired()); QCOMPARE(NymeaCore::instance()->userManager()->users().count(), 0); QSignalSpy spy(m_mockTcpServer, SIGNAL(outgoingData(QUuid,QByteArray))); @@ -197,6 +201,7 @@ void TestJSONRPC::testInitialSetup() response = jsonDoc.toVariant().toMap(); qWarning() << "Calling Hello on uninitialized instance:" << response.value("status").toString() << response.value("error").toString(); QCOMPARE(response.value("status").toString(), QStringLiteral("success")); + QCOMPARE(response.value("params").toMap().value("initialSetupRequired").toBool(), true); // Any other call should fail with "unauthorized" even if we use a previously valid token spy.clear(); @@ -251,6 +256,19 @@ void TestJSONRPC::testInitialSetup() QCOMPARE(response.value("status").toString(), QStringLiteral("success")); QCOMPARE(NymeaCore::instance()->userManager()->users().count(), 1); + // Now that we have a user, initialSetup should be false in the Hello call + spy.clear(); + m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"method\": \"JSONRPC.Hello\"}"); + if (spy.count() == 0) { + spy.wait(); + } + QVERIFY(spy.count() == 1); + jsonDoc = QJsonDocument::fromJson(spy.first().at(1).toByteArray()); + response = jsonDoc.toVariant().toMap(); + qWarning() << "Calling Hello on initialized instance:" << response.value("status").toString() << response.value("error").toString(); + QCOMPARE(response.value("status").toString(), QStringLiteral("success")); + QCOMPARE(response.value("params").toMap().value("initialSetupRequired").toBool(), false); + // Calls should still fail, given we didn't get a new token yet spy.clear(); m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"token\": \"" + m_apiToken + "\", \"method\": \"JSONRPC.Version\"}"); @@ -1075,7 +1093,7 @@ void TestJSONRPC::testPushButtonAuthConnectionDrop() QUuid bobId = QUuid::createUuid(); m_mockTcpServer->clientConnected(bobId); - // request push button auth for client 1 (alice) and check for OK reply + // request push button auth for client 2 (bob) and check for OK reply params.clear(); params.insert("deviceName", "bob"); response = injectAndWait("JSONRPC.RequestPushButtonAuth", params, bobId); @@ -1102,6 +1120,89 @@ void TestJSONRPC::testPushButtonAuthConnectionDrop() } +void TestJSONRPC::testInitialSetupWithPushButtonAuth() +{ + foreach (const QString &user, NymeaCore::instance()->userManager()->users()) { + NymeaCore::instance()->userManager()->removeUser(user); + } + NymeaCore::instance()->userManager()->removeUser(""); + QVERIFY(NymeaCore::instance()->userManager()->initRequired()); + + QSignalSpy spy(m_mockTcpServer, SIGNAL(outgoingData(QUuid,QByteArray))); + QVERIFY(spy.isValid()); + + PushButtonAgent pushButtonAgent; + pushButtonAgent.init(); + + // Hello call should work in any case, telling us initial setup is required + spy.clear(); + m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"method\": \"JSONRPC.Hello\"}"); + if (spy.count() == 0) { + spy.wait(); + } + QVERIFY(spy.count() == 1); + QJsonDocument jsonDoc = QJsonDocument::fromJson(spy.first().at(1).toByteArray()); + QVariant response = jsonDoc.toVariant(); + qWarning() << "Calling Hello on uninitialized instance:" << response.toMap().value("status").toString() << response.toMap().value("error").toString(); + QCOMPARE(response.toMap().value("status").toString(), QStringLiteral("success")); + QCOMPARE(response.toMap().value("params").toMap().value("initialSetupRequired").toBool(), true); + + // request push button auth for alice and check for OK reply + QUuid aliceId = QUuid::createUuid(); + m_mockTcpServer->clientConnected(aliceId); + + QVariantMap params; + params.insert("deviceName", "alice"); + response = injectAndWait("JSONRPC.RequestPushButtonAuth", params, aliceId); + QCOMPARE(response.toMap().value("params").toMap().value("success").toBool(), true); + int transactionId = response.toMap().value("params").toMap().value("transactionId").toInt(); + + spy.clear(); + pushButtonAgent.sendButtonPressed(); + + // Wait for things to happen + if (spy.count() == 0) { + spy.wait(); + } + + // There should have been only exactly one message sent, the token for alice + QCOMPARE(spy.count(), 1); + QVariantMap notification = QJsonDocument::fromJson(spy.first().at(1).toByteArray()).toVariant().toMap(); + QCOMPARE(spy.first().first().toUuid(), aliceId); + QCOMPARE(notification.value("notification").toString(), QLatin1String("JSONRPC.PushButtonAuthFinished")); + QCOMPARE(notification.value("params").toMap().value("transactionId").toInt(), transactionId); + 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"); + + // initialSetupRequired should be false in Hello call now + spy.clear(); + m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"method\": \"JSONRPC.Hello\"}"); + if (spy.count() == 0) { + spy.wait(); + } + QVERIFY(spy.count() == 1); + jsonDoc = QJsonDocument::fromJson(spy.first().at(1).toByteArray()); + response = jsonDoc.toVariant(); + qWarning() << "Calling Hello on uninitialized instance:" << response.toMap().value("status").toString() << response.toMap().value("error").toString(); + QCOMPARE(response.toMap().value("status").toString(), QStringLiteral("success")); + QCOMPARE(response.toMap().value("params").toMap().value("initialSetupRequired").toBool(), false); + + + // CreateUser without a token should fail now even though there are 0 users in the DB + spy.clear(); + m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"method\": \"JSONRPC.CreateUser\", \"params\": {\"username\": \"Dummy@guh.io\", \"password\": \"DummyPW1!\"}}"); + if (spy.count() == 0) { + spy.wait(); + } + QVERIFY(spy.count() == 1); + jsonDoc = QJsonDocument::fromJson(spy.first().at(1).toByteArray()); + response = jsonDoc.toVariant(); + qWarning() << "Calling CreateUser on uninitialized instance:" << response.toMap().value("status").toString() << response.toMap().value("error").toString(); + QCOMPARE(response.toMap().value("status").toString(), QStringLiteral("unauthorized")); + QCOMPARE(NymeaCore::instance()->userManager()->users().count(), 0); + +} + #include "testjsonrpc.moc" QTEST_MAIN(TestJSONRPC)