fix initialSetupRequired still being true, even if we already gave out tokens by pushbuttonAuth

This commit is contained in:
Michael Zanetti 2018-04-10 21:50:24 +02:00
parent 6608748f83
commit 44dd09d227
4 changed files with 135 additions and 10 deletions

View File

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

View File

@ -51,6 +51,23 @@ UserManager::UserManager(QObject *parent) : QObject(parent)
m_pushButtonTransaction = qMakePair<int, QString>(-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;
}

View File

@ -47,6 +47,7 @@ public:
explicit UserManager(QObject *parent = 0);
bool initRequired() const;
QStringList users() const;
UserError createUser(const QString &username, const QString &password);

View File

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